From 1b0f54a61b63ee2bc034f1660e68213b43c89499 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Mon, 24 Jul 2023 14:55:42 +0000 Subject: [PATCH 01/25] Add new tracing API --- api/api_full.go | 7 + api/api_gateway.go | 2 + api/eth_aliases.go | 3 + api/mocks/mock_full.go | 30 +++ api/proxy_gen.go | 52 ++++ chain/types/cbor_gen.go | 64 ++++- chain/types/execresult.go | 5 + documentation/en/api-v0-methods.md | 42 ++- documentation/en/api-v1-unstable-methods.md | 83 +++++- gateway/node.go | 2 + gateway/proxy_eth.go | 40 ++- node/builder_chain.go | 3 + node/impl/full.go | 1 + node/impl/full/dummy.go | 9 + node/impl/full/trace.go | 273 ++++++++++++++++++++ node/modules/trace.go | 19 ++ 16 files changed, 613 insertions(+), 22 deletions(-) create mode 100644 node/impl/full/trace.go create mode 100644 node/modules/trace.go diff --git a/api/api_full.go b/api/api_full.go index 0e128b398b5..95d86ce6f34 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -868,6 +868,13 @@ type FullNode interface { // Returns the client version Web3ClientVersion(ctx context.Context) (string, error) //perm:read + // TraceAPI related methods + // + // Returns traces created at given block + TraceBlock(ctx context.Context, blkNum string) (interface{}, error) //perm:read + // Replays all transactions in a block returning the requested traces for each transaction + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) //perm:read + // CreateBackup creates node backup onder the specified file name. The // method requires that the lotus daemon is running with the // LOTUS_BACKUP_BASE_PATH environment variable set to some path, and that diff --git a/api/api_gateway.go b/api/api_gateway.go index 61cb4814699..0f2e66709cc 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -127,4 +127,6 @@ type Gateway interface { EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) Web3ClientVersion(ctx context.Context) (string, error) + TraceBlock(ctx context.Context, blkNum string) (interface{}, error) + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) } diff --git a/api/eth_aliases.go b/api/eth_aliases.go index ca0f861ac73..fe761c545f4 100644 --- a/api/eth_aliases.go +++ b/api/eth_aliases.go @@ -40,6 +40,9 @@ func CreateEthRPCAliases(as apitypes.Aliaser) { as.AliasMethod("eth_subscribe", "Filecoin.EthSubscribe") as.AliasMethod("eth_unsubscribe", "Filecoin.EthUnsubscribe") + as.AliasMethod("trace_block", "Filecoin.TraceBlock") + as.AliasMethod("trace_replayBlockTransactions", "Filecoin.TraceReplayBlockTransactions") + as.AliasMethod("net_version", "Filecoin.NetVersion") as.AliasMethod("net_listening", "Filecoin.NetListening") diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index d2f2e528e49..882aacebb06 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -4023,6 +4023,36 @@ func (mr *MockFullNodeMockRecorder) SyncValidateTipset(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncValidateTipset", reflect.TypeOf((*MockFullNode)(nil).SyncValidateTipset), arg0, arg1) } +// TraceBlock mocks base method. +func (m *MockFullNode) TraceBlock(arg0 context.Context, arg1 string) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TraceBlock", arg0, arg1) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TraceBlock indicates an expected call of TraceBlock. +func (mr *MockFullNodeMockRecorder) TraceBlock(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TraceBlock", reflect.TypeOf((*MockFullNode)(nil).TraceBlock), arg0, arg1) +} + +// TraceReplayBlockTransactions mocks base method. +func (m *MockFullNode) TraceReplayBlockTransactions(arg0 context.Context, arg1 string, arg2 []string) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TraceReplayBlockTransactions", arg0, arg1, arg2) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TraceReplayBlockTransactions indicates an expected call of TraceReplayBlockTransactions. +func (mr *MockFullNodeMockRecorder) TraceReplayBlockTransactions(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TraceReplayBlockTransactions", reflect.TypeOf((*MockFullNode)(nil).TraceReplayBlockTransactions), arg0, arg1, arg2) +} + // Version mocks base method. func (m *MockFullNode) Version(arg0 context.Context) (api.APIVersion, error) { m.ctrl.T.Helper() diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 95668596d58..6f0487b0be8 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -596,6 +596,10 @@ type FullNodeMethods struct { SyncValidateTipset func(p0 context.Context, p1 types.TipSetKey) (bool, error) `perm:"read"` + TraceBlock func(p0 context.Context, p1 string) (interface{}, error) `perm:"read"` + + TraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) (interface{}, error) `perm:"read"` + WalletBalance func(p0 context.Context, p1 address.Address) (types.BigInt, error) `perm:"read"` WalletDefaultAddress func(p0 context.Context) (address.Address, error) `perm:"write"` @@ -814,6 +818,10 @@ type GatewayMethods struct { StateWaitMsg func(p0 context.Context, p1 cid.Cid, p2 uint64, p3 abi.ChainEpoch, p4 bool) (*MsgLookup, error) `` + TraceBlock func(p0 context.Context, p1 string) (interface{}, error) `` + + TraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) (interface{}, error) `` + Version func(p0 context.Context) (APIVersion, error) `` WalletBalance func(p0 context.Context, p1 address.Address) (types.BigInt, error) `` @@ -3997,6 +4005,28 @@ func (s *FullNodeStub) SyncValidateTipset(p0 context.Context, p1 types.TipSetKey return false, ErrNotSupported } +func (s *FullNodeStruct) TraceBlock(p0 context.Context, p1 string) (interface{}, error) { + if s.Internal.TraceBlock == nil { + return nil, ErrNotSupported + } + return s.Internal.TraceBlock(p0, p1) +} + +func (s *FullNodeStub) TraceBlock(p0 context.Context, p1 string) (interface{}, error) { + return nil, ErrNotSupported +} + +func (s *FullNodeStruct) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) (interface{}, error) { + if s.Internal.TraceReplayBlockTransactions == nil { + return nil, ErrNotSupported + } + return s.Internal.TraceReplayBlockTransactions(p0, p1, p2) +} + +func (s *FullNodeStub) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) (interface{}, error) { + return nil, ErrNotSupported +} + func (s *FullNodeStruct) WalletBalance(p0 context.Context, p1 address.Address) (types.BigInt, error) { if s.Internal.WalletBalance == nil { return *new(types.BigInt), ErrNotSupported @@ -5130,6 +5160,28 @@ func (s *GatewayStub) StateWaitMsg(p0 context.Context, p1 cid.Cid, p2 uint64, p3 return nil, ErrNotSupported } +func (s *GatewayStruct) TraceBlock(p0 context.Context, p1 string) (interface{}, error) { + if s.Internal.TraceBlock == nil { + return nil, ErrNotSupported + } + return s.Internal.TraceBlock(p0, p1) +} + +func (s *GatewayStub) TraceBlock(p0 context.Context, p1 string) (interface{}, error) { + return nil, ErrNotSupported +} + +func (s *GatewayStruct) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) (interface{}, error) { + if s.Internal.TraceReplayBlockTransactions == nil { + return nil, ErrNotSupported + } + return s.Internal.TraceReplayBlockTransactions(p0, p1, p2) +} + +func (s *GatewayStub) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) (interface{}, error) { + return nil, ErrNotSupported +} + func (s *GatewayStruct) Version(p0 context.Context) (APIVersion, error) { if s.Internal.Version == nil { return *new(APIVersion), ErrNotSupported diff --git a/chain/types/cbor_gen.go b/chain/types/cbor_gen.go index 90d1a14c598..a9040613f35 100644 --- a/chain/types/cbor_gen.go +++ b/chain/types/cbor_gen.go @@ -2289,7 +2289,7 @@ func (t *GasTrace) UnmarshalCBOR(r io.Reader) (err error) { return nil } -var lengthBufMessageTrace = []byte{134} +var lengthBufMessageTrace = []byte{137} func (t *MessageTrace) MarshalCBOR(w io.Writer) error { if t == nil { @@ -2343,6 +2343,23 @@ func (t *MessageTrace) MarshalCBOR(w io.Writer) error { return err } + // t.GasLimit (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.GasLimit)); err != nil { + return err + } + + // t.ReadOnly (bool) (bool) + if err := cbg.WriteBool(w, t.ReadOnly); err != nil { + return err + } + + // t.CodeCid (cid.Cid) (struct) + + if err := cbg.WriteCid(cw, t.CodeCid); err != nil { + return xerrors.Errorf("failed to write cid field t.CodeCid: %w", err) + } + return nil } @@ -2365,7 +2382,7 @@ func (t *MessageTrace) UnmarshalCBOR(r io.Reader) (err error) { return fmt.Errorf("cbor input should be of type array") } - if extra != 6 { + if extra != 9 { return fmt.Errorf("cbor input had wrong number of fields") } @@ -2444,6 +2461,49 @@ func (t *MessageTrace) UnmarshalCBOR(r io.Reader) (err error) { } t.ParamsCodec = uint64(extra) + } + // t.GasLimit (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.GasLimit = uint64(extra) + + } + // t.ReadOnly (bool) (bool) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajOther { + return fmt.Errorf("booleans must be major type 7") + } + switch extra { + case 20: + t.ReadOnly = false + case 21: + t.ReadOnly = true + default: + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) + } + // t.CodeCid (cid.Cid) (struct) + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.CodeCid: %w", err) + } + + t.CodeCid = c + } return nil } diff --git a/chain/types/execresult.go b/chain/types/execresult.go index 2a25d22e28a..4556f7b88ec 100644 --- a/chain/types/execresult.go +++ b/chain/types/execresult.go @@ -4,6 +4,8 @@ import ( "encoding/json" "time" + "github.com/ipfs/go-cid" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/exitcode" @@ -24,6 +26,9 @@ type MessageTrace struct { Method abi.MethodNum Params []byte ParamsCodec uint64 + GasLimit uint64 + ReadOnly bool + CodeCid cid.Cid } type ReturnTrace struct { diff --git a/documentation/en/api-v0-methods.md b/documentation/en/api-v0-methods.md index 13626d702e1..742f3de8e0c 100644 --- a/documentation/en/api-v0-methods.md +++ b/documentation/en/api-v0-methods.md @@ -4873,7 +4873,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -4897,7 +4902,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -5103,7 +5113,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -5127,7 +5142,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -6493,7 +6513,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -6517,7 +6542,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index 2049e217541..273f20dc947 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -287,6 +287,9 @@ * [SyncUnmarkAllBad](#SyncUnmarkAllBad) * [SyncUnmarkBad](#SyncUnmarkBad) * [SyncValidateTipset](#SyncValidateTipset) +* [Trace](#Trace) + * [TraceBlock](#TraceBlock) + * [TraceReplayBlockTransactions](#TraceReplayBlockTransactions) * [Wallet](#Wallet) * [WalletBalance](#WalletBalance) * [WalletDefaultAddress](#WalletDefaultAddress) @@ -6312,7 +6315,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -6336,7 +6344,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -6542,7 +6555,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -6566,7 +6584,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -8061,7 +8084,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -8085,7 +8113,12 @@ Response: "Value": "0", "Method": 1, "Params": "Ynl0ZSBhcnJheQ==", - "ParamsCodec": 42 + "ParamsCodec": 42, + "GasLimit": 42, + "ReadOnly": true, + "CodeCid": { + "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" + } }, "MsgRct": { "ExitCode": 0, @@ -8790,6 +8823,44 @@ Inputs: Response: `true` +## Trace + + +### TraceBlock +TraceAPI related methods + +Returns traces created at given block + + +Perms: read + +Inputs: +```json +[ + "string value" +] +``` + +Response: `{}` + +### TraceReplayBlockTransactions +Replays all transactions in a block returning the requested traces for each transaction + + +Perms: read + +Inputs: +```json +[ + "string value", + [ + "string value" + ] +] +``` + +Response: `{}` + ## Wallet diff --git a/gateway/node.go b/gateway/node.go index bcc4af9ac04..bbda71fcf24 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -144,6 +144,8 @@ type TargetAPI interface { EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) Web3ClientVersion(ctx context.Context) (string, error) + TraceBlock(ctx context.Context, blkNum string) (interface{}, error) + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) } var _ TargetAPI = *new(api.FullNode) // gateway depends on latest diff --git a/gateway/proxy_eth.go b/gateway/proxy_eth.go index e5954c2ff00..f8b950011d6 100644 --- a/gateway/proxy_eth.go +++ b/gateway/proxy_eth.go @@ -21,14 +21,6 @@ import ( "github.com/filecoin-project/lotus/chain/types/ethtypes" ) -func (gw *Node) Web3ClientVersion(ctx context.Context) (string, error) { - if err := gw.limit(ctx, basicRateLimitTokens); err != nil { - return "", err - } - - return gw.target.Web3ClientVersion(ctx) -} - func (gw *Node) EthAccounts(ctx context.Context) ([]ethtypes.EthAddress, error) { // gateway provides public API, so it can't hold user accounts return []ethtypes.EthAddress{}, nil @@ -582,6 +574,38 @@ func (gw *Node) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionI return ok, nil } +func (gw *Node) Web3ClientVersion(ctx context.Context) (string, error) { + if err := gw.limit(ctx, basicRateLimitTokens); err != nil { + return "", err + } + + return gw.target.Web3ClientVersion(ctx) +} + +func (gw *Node) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { + if err := gw.limit(ctx, stateRateLimitTokens); err != nil { + return 0, err + } + + if err := gw.checkBlkParam(ctx, blkNum, 0); err != nil { + return ethtypes.EthBlock{}, err + } + + return gw.target.TraceBlock(ctx, blkNum) +} + +func (gw *Node) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { + if err := gw.limit(ctx, stateRateLimitTokens); err != nil { + return 0, err + } + + if err := gw.checkBlkParam(ctx, blkNum, 0); err != nil { + return ethtypes.EthBlock{}, err + } + + return gw.target.TraceReplayBlockTransactions(ctx, blkNum, traceTypes) +} + var EthMaxFiltersPerConn = 16 // todo make this configurable func addUserFilterLimited(ctx context.Context, cb func() (ethtypes.EthFilterID, error)) (ethtypes.EthFilterID, error) { diff --git a/node/builder_chain.go b/node/builder_chain.go index 267659f0091..0681020ed25 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -155,6 +155,7 @@ var ChainNode = Options( Override(new(stmgr.StateManagerAPI), rpcstmgr.NewRPCStateManager), Override(new(full.EthModuleAPI), From(new(api.Gateway))), Override(new(full.EthEventAPI), From(new(api.Gateway))), + Override(new(full.EthTraceAPI), From(new(api.Gateway))), ), // Full node API / service startup @@ -270,10 +271,12 @@ func ConfigFullNode(c interface{}) Option { If(cfg.Fevm.EnableEthRPC, Override(new(full.EthModuleAPI), modules.EthModuleAPI(cfg.Fevm)), Override(new(full.EthEventAPI), modules.EthEventAPI(cfg.Fevm)), + Override(new(full.EthTraceAPI), modules.EthTraceAPI()), ), If(!cfg.Fevm.EnableEthRPC, Override(new(full.EthModuleAPI), &full.EthModuleDummy{}), Override(new(full.EthEventAPI), &full.EthModuleDummy{}), + Override(new(full.EthTraceAPI), &full.EthModuleDummy{}), ), ), diff --git a/node/impl/full.go b/node/impl/full.go index affcc960e09..0f87cfe292c 100644 --- a/node/impl/full.go +++ b/node/impl/full.go @@ -36,6 +36,7 @@ type FullNodeAPI struct { full.SyncAPI full.RaftAPI full.EthAPI + full.EthTraceAPI DS dtypes.MetadataDS NetworkName dtypes.NetworkName diff --git a/node/impl/full/dummy.go b/node/impl/full/dummy.go index c4bda6428da..7412ed7549b 100644 --- a/node/impl/full/dummy.go +++ b/node/impl/full/dummy.go @@ -178,5 +178,14 @@ func (e *EthModuleDummy) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubs return false, ErrModuleDisabled } +func (e *EthModuleDummy) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { + return nil, ErrModuleDisabled +} + +func (e *EthModuleDummy) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { + return nil, ErrModuleDisabled +} + var _ EthModuleAPI = &EthModuleDummy{} var _ EthEventAPI = &EthModuleDummy{} +var _ EthTraceAPI = &EthModuleDummy{} diff --git a/node/impl/full/trace.go b/node/impl/full/trace.go new file mode 100644 index 00000000000..cc31103f9e4 --- /dev/null +++ b/node/impl/full/trace.go @@ -0,0 +1,273 @@ +package full + +import ( + "context" + "encoding/hex" + "fmt" + + "go.uber.org/fx" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-state-types/abi" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +type EthTraceAPI interface { + TraceBlock(ctx context.Context, blkNum string) (interface{}, error) + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) +} + +var ( + _ EthTraceAPI = *new(api.FullNode) +) + +type EthTrace struct { + fx.In + + Chain *store.ChainStore + StateManager *stmgr.StateManager + + ChainAPI + EthModuleAPI +} + +var _ EthTraceAPI = (*EthTrace)(nil) + +type Trace struct { + Action Action `json:"action"` + Result Result `json:"result"` + Subtraces int `json:"subtraces"` + TraceAddress []int `json:"traceAddress"` + Type string `json:"Type"` +} + +type TraceBlock struct { + *Trace + BlockHash ethtypes.EthHash `json:"blockHash"` + BlockNumber int64 `json:"blockNumber"` + TransactionHash ethtypes.EthHash `json:"transactionHash"` + TransactionPosition int `json:"transactionPosition"` +} + +type TraceReplayBlockTransaction struct { + Output string `json:"output"` + StateDiff *string `json:"stateDiff"` + Trace []*Trace `json:"trace"` + TransactionHash ethtypes.EthHash `json:"transactionHash"` + VmTrace *string `json:"vmTrace"` +} + +type Action struct { + CallType string `json:"callType"` + From string `json:"from"` + To string `json:"to"` + Gas ethtypes.EthUint64 `json:"gas"` + Input string `json:"input"` + Value ethtypes.EthBigInt `json:"value"` +} + +type Result struct { + GasUsed ethtypes.EthUint64 `json:"gasUsed"` + Output string `json:"output"` +} + +func (e *EthTrace) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { + ts, err := e.getTipsetByBlockNr(ctx, blkNum, false) + if err != nil { + return nil, err + } + + _, trace, err := e.StateManager.ExecutionTrace(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("failed to compute base state: %w", err) + } + + tsParent, err := e.ChainAPI.ChainGetTipSetByHeight(ctx, ts.Height()+1, e.Chain.GetHeaviestTipSet().Key()) + if err != nil { + return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height()+1) + } + + msgs, err := e.ChainGetParentMessages(ctx, tsParent.Blocks()[0].Cid()) + if err != nil { + return nil, err + } + + cid, err := ts.Key().Cid() + if err != nil { + return nil, err + } + + blkHash, err := ethtypes.EthHashFromCid(cid) + if err != nil { + return nil, err + } + + allTraces := make([]*TraceBlock, 0, len(trace)) + for _, ir := range trace { + // ignore messages from f00 + if ir.Msg.From.String() == "f00" { + continue + } + + idx := -1 + for msgIdx, msg := range msgs { + if ir.Msg.From == msg.Message.From { + idx = msgIdx + break + } + } + if idx == -1 { + log.Warnf("cannot resolve message index for cid: %s", ir.MsgCid) + continue + } + + txHash, err := e.EthGetTransactionHashByCid(ctx, ir.MsgCid) + if err != nil { + return nil, err + } + if txHash == nil { + log.Warnf("cannot find transaction hash for cid %s", ir.MsgCid) + continue + } + + traces := []*Trace{} + buildTraces(&traces, []int{}, ir.ExecutionTrace) + + traceBlocks := make([]*TraceBlock, 0, len(trace)) + for _, trace := range traces { + traceBlocks = append(traceBlocks, &TraceBlock{ + Trace: trace, + BlockHash: blkHash, + BlockNumber: int64(ts.Height()), + TransactionHash: *txHash, + TransactionPosition: idx, + }) + } + + allTraces = append(allTraces, traceBlocks...) + } + + return allTraces, nil +} + +func (e *EthTrace) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { + if len(traceTypes) != 1 || traceTypes[0] != "trace" { + return nil, fmt.Errorf("only 'trace' is supported") + } + + ts, err := e.getTipsetByBlockNr(ctx, blkNum, false) + if err != nil { + return nil, err + } + + _, trace, err := e.StateManager.ExecutionTrace(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) + } + + allTraces := make([]*TraceReplayBlockTransaction, 0, len(trace)) + for _, ir := range trace { + // ignore messages from f00 + if ir.Msg.From.String() == "f00" { + continue + } + + txHash, err := e.EthGetTransactionHashByCid(ctx, ir.MsgCid) + if err != nil { + return nil, err + } + if txHash == nil { + log.Warnf("cannot find transaction hash for cid %s", ir.MsgCid) + continue + } + + t := TraceReplayBlockTransaction{ + Output: hex.EncodeToString(ir.MsgRct.Return), + TransactionHash: *txHash, + StateDiff: nil, + VmTrace: nil, + } + + buildTraces(&t.Trace, []int{}, ir.ExecutionTrace) + + allTraces = append(allTraces, &t) + } + + return allTraces, nil +} + +// buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls +func buildTraces(traces *[]*Trace, addr []int, et types.ExecutionTrace) { + callType := "call" + if et.Msg.ReadOnly { + callType = "staticcall" + } + + // TODO: add check for determining if this this should be delegatecall + if false { + callType = "delegatecall" + } + + *traces = append(*traces, &Trace{ + Action: Action{ + CallType: callType, + From: et.Msg.From.String(), + To: et.Msg.To.String(), + Gas: ethtypes.EthUint64(et.Msg.GasLimit), + Input: hex.EncodeToString(et.Msg.Params), + Value: ethtypes.EthBigInt(et.Msg.Value), + }, + Result: Result{ + GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), + Output: hex.EncodeToString(et.MsgRct.Return), + }, + Subtraces: len(et.Subcalls), + TraceAddress: addr, + Type: callType, + }) + + for i, call := range et.Subcalls { + buildTraces(traces, append(addr, i), call) + } +} + +// TODO: refactor this to be shared code +func (e *EthTrace) getTipsetByBlockNr(ctx context.Context, blkParam string, strict bool) (*types.TipSet, error) { + if blkParam == "earliest" { + return nil, fmt.Errorf("block param \"earliest\" is not supported") + } + + head := e.Chain.GetHeaviestTipSet() + switch blkParam { + case "pending": + return head, nil + case "latest": + parent, err := e.Chain.GetTipSetFromKey(ctx, head.Parents()) + if err != nil { + return nil, fmt.Errorf("cannot get parent tipset") + } + return parent, nil + default: + var num ethtypes.EthUint64 + err := num.UnmarshalJSON([]byte(`"` + blkParam + `"`)) + if err != nil { + return nil, fmt.Errorf("cannot parse block number: %v", err) + } + if abi.ChainEpoch(num) > head.Height()-1 { + return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") + } + ts, err := e.ChainAPI.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(num), head.Key()) + if err != nil { + return nil, fmt.Errorf("cannot get tipset at height: %v", num) + } + if strict && ts.Height() != abi.ChainEpoch(num) { + return nil, ErrNullRound + } + return ts, nil + } +} diff --git a/node/modules/trace.go b/node/modules/trace.go new file mode 100644 index 00000000000..aea7fc02f72 --- /dev/null +++ b/node/modules/trace.go @@ -0,0 +1,19 @@ +package modules + +import ( + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/node/impl/full" +) + +func EthTraceAPI() func(*store.ChainStore, *stmgr.StateManager, full.EthModuleAPI, full.ChainAPI) (*full.EthTrace, error) { + return func(cs *store.ChainStore, sm *stmgr.StateManager, evapi full.EthModuleAPI, chainapi full.ChainAPI) (*full.EthTrace, error) { + return &full.EthTrace{ + Chain: cs, + StateManager: sm, + + ChainAPI: chainapi, + EthModuleAPI: evapi, + }, nil + } +} From c1eaa2f864b415660ccad94d989b434729623bd0 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 1 Aug 2023 19:15:58 +0000 Subject: [PATCH 02/25] Translate call input/output into Solidity ABI --- node/impl/full/trace.go | 172 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 161 insertions(+), 11 deletions(-) diff --git a/node/impl/full/trace.go b/node/impl/full/trace.go index cc31103f9e4..df3fe90a938 100644 --- a/node/impl/full/trace.go +++ b/node/impl/full/trace.go @@ -1,14 +1,22 @@ package full import ( + "bytes" "context" + "encoding/binary" "encoding/hex" "fmt" + "io" "go.uber.org/fx" "golang.org/x/xerrors" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/exitcode" + "github.com/ipfs/go-cid" + + builtin2 "github.com/filecoin-project/go-state-types/builtin" + builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/stmgr" @@ -69,6 +77,9 @@ type Action struct { Gas ethtypes.EthUint64 `json:"gas"` Input string `json:"input"` Value ethtypes.EthBigInt `json:"value"` + + Method abi.MethodNum `json:"method"` + CodeCid cid.Cid `json:"codeCid"` } type Result struct { @@ -136,7 +147,7 @@ func (e *EthTrace) TraceBlock(ctx context.Context, blkNum string) (interface{}, } traces := []*Trace{} - buildTraces(&traces, []int{}, ir.ExecutionTrace) + buildTraces(&traces, []int{}, ir.ExecutionTrace, nil, int64(ts.Height())) traceBlocks := make([]*TraceBlock, 0, len(trace)) for _, trace := range traces { @@ -193,7 +204,7 @@ func (e *EthTrace) TraceReplayBlockTransactions(ctx context.Context, blkNum stri VmTrace: nil, } - buildTraces(&t.Trace, []int{}, ir.ExecutionTrace) + buildTraces(&t.Trace, []int{}, ir.ExecutionTrace, nil, int64(ts.Height())) allTraces = append(allTraces, &t) } @@ -201,19 +212,97 @@ func (e *EthTrace) TraceReplayBlockTransactions(ctx context.Context, blkNum stri return allTraces, nil } +func write_padded[T any](w io.Writer, data T, size int) error { + tmp := &bytes.Buffer{} + + // first write data to tmp buffer to get the size + err := binary.Write(tmp, binary.BigEndian, data) + if err != nil { + return err + } + + if tmp.Len() > size { + return fmt.Errorf("data is larger than size") + } + + // write tailing zeros to pad up to size + cnt := size - tmp.Len() + for i := 0; i < cnt; i++ { + err = binary.Write(w, binary.BigEndian, uint8(0)) + if err != nil { + return err + } + } + + // finally write the actual value + err = binary.Write(w, binary.BigEndian, tmp.Bytes()) + if err != nil { + return err + } + + return nil +} + +func handle_filecoin_method_input(method abi.MethodNum, codec uint64, params []byte) ([]byte, error) { + NATIVE_METHOD_SELECTOR := []byte{0x86, 0x8e, 0x10, 0xc4} + EVM_WORD_SIZE := 32 + + staticArgs := []uint64{ + uint64(method), + codec, + uint64(EVM_WORD_SIZE) * 3, + uint64(len(params)), + } + totalWords := len(staticArgs) + (len(params) / EVM_WORD_SIZE) + if len(params)%EVM_WORD_SIZE != 0 { + totalWords += 1 + } + len := 4 + totalWords*EVM_WORD_SIZE + + w := &bytes.Buffer{} + err := binary.Write(w, binary.BigEndian, NATIVE_METHOD_SELECTOR) + if err != nil { + return nil, err + } + + for _, arg := range staticArgs { + err := write_padded(w, arg, 32) + if err != nil { + return nil, err + } + } + binary.Write(w, binary.BigEndian, params) + remain := len - w.Len() + for i := 0; i < remain; i++ { + binary.Write(w, binary.BigEndian, uint8(0)) + } + + return w.Bytes(), nil +} + +func handle_filecoin_method_output(exitCode exitcode.ExitCode, codec uint64, data []byte) ([]byte, error) { + w := &bytes.Buffer{} + + values := []interface{}{uint32(exitCode), codec, uint32(w.Len()), uint32(len(data))} + for _, v := range values { + err := write_padded(w, v, 32) + if err != nil { + return nil, err + } + } + binary.Write(w, binary.BigEndian, []byte(data)) + + return w.Bytes(), nil +} + // buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls -func buildTraces(traces *[]*Trace, addr []int, et types.ExecutionTrace) { +func buildTraces(traces *[]*Trace, addr []int, et types.ExecutionTrace, parentEt *types.ExecutionTrace, height int64) { callType := "call" if et.Msg.ReadOnly { callType = "staticcall" } - // TODO: add check for determining if this this should be delegatecall - if false { - callType = "delegatecall" - } - - *traces = append(*traces, &Trace{ + trace := &Trace{ Action: Action{ CallType: callType, From: et.Msg.From.String(), @@ -221,6 +310,8 @@ func buildTraces(traces *[]*Trace, addr []int, et types.ExecutionTrace) { Gas: ethtypes.EthUint64(et.Msg.GasLimit), Input: hex.EncodeToString(et.Msg.Params), Value: ethtypes.EthBigInt(et.Msg.Value), + Method: et.Msg.Method, + CodeCid: et.Msg.CodeCid, }, Result: Result{ GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), @@ -229,10 +320,69 @@ func buildTraces(traces *[]*Trace, addr []int, et types.ExecutionTrace) { Subtraces: len(et.Subcalls), TraceAddress: addr, Type: callType, - }) + } + + // Native calls + // + // When an EVM actor is invoked with a method number above 1023 that's not frc42(InvokeEVM) + // then we need to format native calls in a way that makes sense to Ethereum tooling (convert + // the input & output to solidity ABI format). + if parentEt != nil { + if builtinactors.IsEvmActor(parentEt.Msg.CodeCid) && et.Msg.Method > 1023 && et.Msg.Method != builtin2.MethodsEVM.InvokeContract { + log.Infof("found Native call! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + input, _ := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + trace.Action.Input = hex.EncodeToString(input) + output, _ := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + trace.Result.Output = hex.EncodeToString(output) + } + } + + // Native actor creation + // + // TODO... + + // EVM contract creation + // + // TODO... + + // EVM call special casing + // + // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions + // and should be dropped from the trace. + if parentEt != nil { + if builtinactors.IsEvmActor(parentEt.Msg.CodeCid) && et.Msg.Method > 0 && et.Msg.Method <= 1023 { + log.Infof("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + + // skip current trace but process subcalls + for i, call := range et.Subcalls { + buildTraces(traces, append(addr, i), call, &et, height) + } + + return + } + } + + // EVM -> EVM calls + // + // Check for normal EVM to EVM calls and decode the params and return values + if parentEt != nil { + if builtinactors.IsEvmActor(parentEt.Msg.CodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin2.MethodsEVM.InvokeContract { + log.Infof("evm to evm! ! ") + input, _ := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + trace.Action.Input = hex.EncodeToString(input) + output, _ := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + trace.Result.Output = hex.EncodeToString(output) + } + } + + if et.Msg.From == et.Msg.To && et.Msg.Method == builtin2.MethodsEVM.InvokeContractDelegate { + log.Info("from and to are the same, and method is InvokeContractDelegate!!!!!!!!, height:%d", height) + } + + *traces = append(*traces, trace) for i, call := range et.Subcalls { - buildTraces(traces, append(addr, i), call) + buildTraces(traces, append(addr, i), call, &et, height) } } From 2c902db0e1bac3662fe0057dd0a04dd66e8ababc Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Thu, 3 Aug 2023 15:18:01 +0000 Subject: [PATCH 03/25] Handle more edge cases --- node/impl/full/trace.go | 146 ++++++++++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 50 deletions(-) diff --git a/node/impl/full/trace.go b/node/impl/full/trace.go index df3fe90a938..2b207abc537 100644 --- a/node/impl/full/trace.go +++ b/node/impl/full/trace.go @@ -12,6 +12,7 @@ import ( "golang.org/x/xerrors" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/exitcode" "github.com/ipfs/go-cid" @@ -52,6 +53,13 @@ type Trace struct { Subtraces int `json:"subtraces"` TraceAddress []int `json:"traceAddress"` Type string `json:"Type"` + + parent *Trace +} + +func (t *Trace) setCallType(callType string) { + t.Action.CallType = callType + t.Type = callType } type TraceBlock struct { @@ -147,7 +155,7 @@ func (e *EthTrace) TraceBlock(ctx context.Context, blkNum string) (interface{}, } traces := []*Trace{} - buildTraces(&traces, []int{}, ir.ExecutionTrace, nil, int64(ts.Height())) + buildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) traceBlocks := make([]*TraceBlock, 0, len(trace)) for _, trace := range traces { @@ -204,7 +212,7 @@ func (e *EthTrace) TraceReplayBlockTransactions(ctx context.Context, blkNum stri VmTrace: nil, } - buildTraces(&t.Trace, []int{}, ir.ExecutionTrace, nil, int64(ts.Height())) + buildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) allTraces = append(allTraces, &t) } @@ -296,22 +304,16 @@ func handle_filecoin_method_output(exitCode exitcode.ExitCode, codec uint64, dat } // buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls -func buildTraces(traces *[]*Trace, addr []int, et types.ExecutionTrace, parentEt *types.ExecutionTrace, height int64) { - callType := "call" - if et.Msg.ReadOnly { - callType = "staticcall" - } - +func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.ExecutionTrace, height int64) { trace := &Trace{ Action: Action{ - CallType: callType, - From: et.Msg.From.String(), - To: et.Msg.To.String(), - Gas: ethtypes.EthUint64(et.Msg.GasLimit), - Input: hex.EncodeToString(et.Msg.Params), - Value: ethtypes.EthBigInt(et.Msg.Value), - Method: et.Msg.Method, - CodeCid: et.Msg.CodeCid, + From: et.Msg.From.String(), + To: et.Msg.To.String(), + Gas: ethtypes.EthUint64(et.Msg.GasLimit), + Input: hex.EncodeToString(et.Msg.Params), + Value: ethtypes.EthBigInt(et.Msg.Value), + Method: et.Msg.Method, + CodeCid: et.Msg.CodeCid, }, Result: Result{ GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), @@ -319,70 +321,114 @@ func buildTraces(traces *[]*Trace, addr []int, et types.ExecutionTrace, parentEt }, Subtraces: len(et.Subcalls), TraceAddress: addr, - Type: callType, + + parent: parent, } - // Native calls - // - // When an EVM actor is invoked with a method number above 1023 that's not frc42(InvokeEVM) - // then we need to format native calls in a way that makes sense to Ethereum tooling (convert - // the input & output to solidity ABI format). - if parentEt != nil { - if builtinactors.IsEvmActor(parentEt.Msg.CodeCid) && et.Msg.Method > 1023 && et.Msg.Method != builtin2.MethodsEVM.InvokeContract { + trace.setCallType("call") + if et.Msg.ReadOnly { + trace.setCallType("staticcall") + } + + // there are several edge cases thar require special handling when displaying the traces + if parent != nil { + // Handle Native calls + // + // When an EVM actor is invoked with a method number above 1023 that's not frc42(InvokeEVM) + // then we need to format native calls in a way that makes sense to Ethereum tooling (convert + // the input & output to solidity ABI format). + if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 1023 && et.Msg.Method != builtin2.MethodsEVM.InvokeContract { log.Infof("found Native call! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) input, _ := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) trace.Action.Input = hex.EncodeToString(input) output, _ := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) trace.Result.Output = hex.EncodeToString(output) } - } - // Native actor creation - // - // TODO... + // Handle Native actor creation + // + // Actor A calls to the init actor on method 2 and The init actor creates the target actor B then calls it on method 1 + if parent.Action.To == builtin.InitActorAddr.String() && parent.Action.Method == 2 && et.Msg.Method == 1 { + log.Infof("Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + parent.setCallType("create") + parent.Action.To = et.Msg.To.String() + parent.Action.Input = hex.EncodeToString([]byte{0x0, 0x0, 0x0, 0xFE}) + parent.Result.Output = "" + + // there should never be any subcalls when creating a native actor + return + } - // EVM contract creation - // - // TODO... + // Handle EVM contract creation + // + // To detect EVM contract creation we need to check for the following sequence of events: + // + // 1) EVM contract A calls the EAM (Ethereum Address Manager) on method 2 (create) or 3 (create2). + // 2) The EAM calls the init actor on method 3 (Exec4). + // 3) The init actor creates the target actor B then calls it on method 1. + if parent.parent != nil { + calledCreateOnEAM := parent.parent.Action.To == builtin.EthereumAddressManagerActorAddr.String() && (parent.parent.Action.Method == builtin2.MethodsEAM.Create || + parent.parent.Action.Method == builtin2.MethodsEAM.Create2) + eamCalledInitOnExec4 := parent.Action.To == builtin.InitActorAddr.String() && parent.Action.Method == builtin.MethodsInit.Exec4 + initCreatedActor := trace.Action.Method == builtin.MethodsInit.Constructor + + if calledCreateOnEAM && eamCalledInitOnExec4 && initCreatedActor { + log.Infof("EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + + if parent.parent.Action.Method == builtin2.MethodsEAM.Create { + parent.parent.setCallType("CREATE") + } else { + parent.parent.setCallType("CREATE2") + } + + // update the parent.parent to make this + parent.parent.Action.To = trace.Action.To + parent.parent.Subtraces = 0 + + // delete the parent (the EAM) and skip the current trace (init) + *traces = (*traces)[:len(*traces)-1] + + return + } + } - // EVM call special casing - // - // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions - // and should be dropped from the trace. - if parentEt != nil { - if builtinactors.IsEvmActor(parentEt.Msg.CodeCid) && et.Msg.Method > 0 && et.Msg.Method <= 1023 { + // Handle EVM call special casing + // + // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions + // and should be dropped from the trace. + if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 0 && et.Msg.Method <= 1023 { log.Infof("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - // skip current trace but process subcalls for i, call := range et.Subcalls { - buildTraces(traces, append(addr, i), call, &et, height) + buildTraces(traces, trace, append(addr, i), call, height) } return } - } - // EVM -> EVM calls - // - // Check for normal EVM to EVM calls and decode the params and return values - if parentEt != nil { - if builtinactors.IsEvmActor(parentEt.Msg.CodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin2.MethodsEVM.InvokeContract { - log.Infof("evm to evm! ! ") + // EVM -> EVM calls + // + // Check for normal EVM to EVM calls and decode the params and return values + if builtinactors.IsEvmActor(parent.Action.CodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin2.MethodsEVM.InvokeContract { + log.Infof("found evm to evm! ! height: %d", height) input, _ := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) trace.Action.Input = hex.EncodeToString(input) output, _ := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) trace.Result.Output = hex.EncodeToString(output) } - } - if et.Msg.From == et.Msg.To && et.Msg.Method == builtin2.MethodsEVM.InvokeContractDelegate { - log.Info("from and to are the same, and method is InvokeContractDelegate!!!!!!!!, height:%d", height) + // Handle delegate calls + // + // 1) Look for from an EVM actor to itself on InvokeContractDelegate, method 6. + // 2) Search backwards in the trace for a call to another actor (A) on method 3 (GetBytecode) + // 3) Treat this as a delegate call to actor A. + // TODO: implement this } *traces = append(*traces, trace) for i, call := range et.Subcalls { - buildTraces(traces, append(addr, i), call, &et, height) + buildTraces(traces, trace, append(addr, i), call, height) } } From 392ef1beb7db78b3c01a68524a9c32dafad0d3e4 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Thu, 3 Aug 2023 17:24:22 +0000 Subject: [PATCH 04/25] Handle delegatecall --- node/impl/full/trace.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/node/impl/full/trace.go b/node/impl/full/trace.go index 2b207abc537..7399b0e8b3a 100644 --- a/node/impl/full/trace.go +++ b/node/impl/full/trace.go @@ -86,6 +86,7 @@ type Action struct { Input string `json:"input"` Value ethtypes.EthBigInt `json:"value"` + // TODO: remove these fields from json output Method abi.MethodNum `json:"method"` CodeCid cid.Cid `json:"codeCid"` } @@ -326,6 +327,8 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution } trace.setCallType("call") + + // TODO: is it OK to check this here or is this only specific to certain edge case (evm to evm)? if et.Msg.ReadOnly { trace.setCallType("staticcall") } @@ -398,12 +401,7 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // and should be dropped from the trace. if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 0 && et.Msg.Method <= 1023 { log.Infof("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - - for i, call := range et.Subcalls { - buildTraces(traces, trace, append(addr, i), call, height) - } - - return + // TODO: if I handle this case and drop this call from the trace then I am not able to detect delegate calls } // EVM -> EVM calls @@ -422,7 +420,14 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // 1) Look for from an EVM actor to itself on InvokeContractDelegate, method 6. // 2) Search backwards in the trace for a call to another actor (A) on method 3 (GetBytecode) // 3) Treat this as a delegate call to actor A. - // TODO: implement this + if trace.Action.From == trace.Action.To && trace.Action.Method == builtin2.MethodsEVM.InvokeContractDelegate && len(*traces) > 0 { + // the previous trace should be the GetBytecode call + prev := (*traces)[len(*traces)-1] + if prev.Action.From == trace.Action.From && prev.Action.Method == builtin2.MethodsEVM.GetBytecode { + trace.setCallType("delegatecall") + trace.Action.To = prev.Action.To + } + } } *traces = append(*traces, trace) From abeb842d93a8b314c16ca0274385380cfc8ddc53 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Fri, 4 Aug 2023 11:35:49 +0000 Subject: [PATCH 05/25] Address lint errors --- node/impl/full/trace.go | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/node/impl/full/trace.go b/node/impl/full/trace.go index 7399b0e8b3a..d26d37ca951 100644 --- a/node/impl/full/trace.go +++ b/node/impl/full/trace.go @@ -8,18 +8,16 @@ import ( "fmt" "io" + "github.com/ipfs/go-cid" "go.uber.org/fx" "golang.org/x/xerrors" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/exitcode" - "github.com/ipfs/go-cid" - - builtin2 "github.com/filecoin-project/go-state-types/builtin" - builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/api" + builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/stmgr" "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" @@ -264,7 +262,7 @@ func handle_filecoin_method_input(method abi.MethodNum, codec uint64, params []b } totalWords := len(staticArgs) + (len(params) / EVM_WORD_SIZE) if len(params)%EVM_WORD_SIZE != 0 { - totalWords += 1 + totalWords++ } len := 4 + totalWords*EVM_WORD_SIZE @@ -280,10 +278,16 @@ func handle_filecoin_method_input(method abi.MethodNum, codec uint64, params []b return nil, err } } - binary.Write(w, binary.BigEndian, params) + err = binary.Write(w, binary.BigEndian, params) + if err != nil { + return nil, err + } remain := len - w.Len() for i := 0; i < remain; i++ { - binary.Write(w, binary.BigEndian, uint8(0)) + err = binary.Write(w, binary.BigEndian, uint8(0)) + if err != nil { + return nil, err + } } return w.Bytes(), nil @@ -299,7 +303,11 @@ func handle_filecoin_method_output(exitCode exitcode.ExitCode, codec uint64, dat return nil, err } } - binary.Write(w, binary.BigEndian, []byte(data)) + + err := binary.Write(w, binary.BigEndian, []byte(data)) + if err != nil { + return nil, err + } return w.Bytes(), nil } @@ -340,7 +348,7 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // When an EVM actor is invoked with a method number above 1023 that's not frc42(InvokeEVM) // then we need to format native calls in a way that makes sense to Ethereum tooling (convert // the input & output to solidity ABI format). - if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 1023 && et.Msg.Method != builtin2.MethodsEVM.InvokeContract { + if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 1023 && et.Msg.Method != builtin.MethodsEVM.InvokeContract { log.Infof("found Native call! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) input, _ := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) trace.Action.Input = hex.EncodeToString(input) @@ -370,15 +378,15 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // 2) The EAM calls the init actor on method 3 (Exec4). // 3) The init actor creates the target actor B then calls it on method 1. if parent.parent != nil { - calledCreateOnEAM := parent.parent.Action.To == builtin.EthereumAddressManagerActorAddr.String() && (parent.parent.Action.Method == builtin2.MethodsEAM.Create || - parent.parent.Action.Method == builtin2.MethodsEAM.Create2) + calledCreateOnEAM := parent.parent.Action.To == builtin.EthereumAddressManagerActorAddr.String() && (parent.parent.Action.Method == builtin.MethodsEAM.Create || + parent.parent.Action.Method == builtin.MethodsEAM.Create2) eamCalledInitOnExec4 := parent.Action.To == builtin.InitActorAddr.String() && parent.Action.Method == builtin.MethodsInit.Exec4 initCreatedActor := trace.Action.Method == builtin.MethodsInit.Constructor if calledCreateOnEAM && eamCalledInitOnExec4 && initCreatedActor { log.Infof("EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - if parent.parent.Action.Method == builtin2.MethodsEAM.Create { + if parent.parent.Action.Method == builtin.MethodsEAM.Create { parent.parent.setCallType("CREATE") } else { parent.parent.setCallType("CREATE2") @@ -407,7 +415,7 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // EVM -> EVM calls // // Check for normal EVM to EVM calls and decode the params and return values - if builtinactors.IsEvmActor(parent.Action.CodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin2.MethodsEVM.InvokeContract { + if builtinactors.IsEvmActor(parent.Action.CodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin.MethodsEVM.InvokeContract { log.Infof("found evm to evm! ! height: %d", height) input, _ := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) trace.Action.Input = hex.EncodeToString(input) @@ -420,10 +428,10 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // 1) Look for from an EVM actor to itself on InvokeContractDelegate, method 6. // 2) Search backwards in the trace for a call to another actor (A) on method 3 (GetBytecode) // 3) Treat this as a delegate call to actor A. - if trace.Action.From == trace.Action.To && trace.Action.Method == builtin2.MethodsEVM.InvokeContractDelegate && len(*traces) > 0 { + if trace.Action.From == trace.Action.To && trace.Action.Method == builtin.MethodsEVM.InvokeContractDelegate && len(*traces) > 0 { // the previous trace should be the GetBytecode call prev := (*traces)[len(*traces)-1] - if prev.Action.From == trace.Action.From && prev.Action.Method == builtin2.MethodsEVM.GetBytecode { + if prev.Action.From == trace.Action.From && prev.Action.Method == builtin.MethodsEVM.GetBytecode { trace.setCallType("delegatecall") trace.Action.To = prev.Action.To } From fd69f8bbd88854c6c2fc881ad529a11c68129c89 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 8 Aug 2023 11:16:43 +0000 Subject: [PATCH 06/25] Check all errors --- node/impl/full/trace.go | 85 ++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/node/impl/full/trace.go b/node/impl/full/trace.go index d26d37ca951..cab153e53b8 100644 --- a/node/impl/full/trace.go +++ b/node/impl/full/trace.go @@ -154,7 +154,10 @@ func (e *EthTrace) TraceBlock(ctx context.Context, blkNum string) (interface{}, } traces := []*Trace{} - buildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) + err = buildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) + if err != nil { + return nil, xerrors.Errorf("failed when building traces: %w", err) + } traceBlocks := make([]*TraceBlock, 0, len(trace)) for _, trace := range traces { @@ -211,7 +214,10 @@ func (e *EthTrace) TraceReplayBlockTransactions(ctx context.Context, blkNum stri VmTrace: nil, } - buildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) + err = buildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) + if err != nil { + return nil, xerrors.Errorf("failed when building traces: %w", err) + } allTraces = append(allTraces, &t) } @@ -225,11 +231,11 @@ func write_padded[T any](w io.Writer, data T, size int) error { // first write data to tmp buffer to get the size err := binary.Write(tmp, binary.BigEndian, data) if err != nil { - return err + return fmt.Errorf("write_padded: failed writing tmp data to buffer: %w", err) } if tmp.Len() > size { - return fmt.Errorf("data is larger than size") + return fmt.Errorf("write_padded: data is larger than size") } // write tailing zeros to pad up to size @@ -237,14 +243,14 @@ func write_padded[T any](w io.Writer, data T, size int) error { for i := 0; i < cnt; i++ { err = binary.Write(w, binary.BigEndian, uint8(0)) if err != nil { - return err + return fmt.Errorf("write_padded: failed writing tailing zeros to buffer: %w", err) } } // finally write the actual value err = binary.Write(w, binary.BigEndian, tmp.Bytes()) if err != nil { - return err + return fmt.Errorf("write_padded: failed writing data to buffer: %w", err) } return nil @@ -269,24 +275,24 @@ func handle_filecoin_method_input(method abi.MethodNum, codec uint64, params []b w := &bytes.Buffer{} err := binary.Write(w, binary.BigEndian, NATIVE_METHOD_SELECTOR) if err != nil { - return nil, err + return nil, fmt.Errorf("handle_filecoin_method_input: failed writing method selector: %w", err) } for _, arg := range staticArgs { err := write_padded(w, arg, 32) if err != nil { - return nil, err + return nil, fmt.Errorf("handle_filecoin_method_input: %w", err) } } err = binary.Write(w, binary.BigEndian, params) if err != nil { - return nil, err + return nil, fmt.Errorf("handle_filecoin_method_input: failed writing params: %w", err) } remain := len - w.Len() for i := 0; i < remain; i++ { err = binary.Write(w, binary.BigEndian, uint8(0)) if err != nil { - return nil, err + return nil, fmt.Errorf("handle_filecoin_method_input: failed writing tailing zeros: %w", err) } } @@ -300,20 +306,20 @@ func handle_filecoin_method_output(exitCode exitcode.ExitCode, codec uint64, dat for _, v := range values { err := write_padded(w, v, 32) if err != nil { - return nil, err + return nil, fmt.Errorf("handle_filecoin_method_output: %w", err) } } err := binary.Write(w, binary.BigEndian, []byte(data)) if err != nil { - return nil, err + return nil, fmt.Errorf("handle_filecoin_method_output: failed writing data: %w", err) } return w.Bytes(), nil } // buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls -func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.ExecutionTrace, height int64) { +func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.ExecutionTrace, height int64) error { trace := &Trace{ Action: Action{ From: et.Msg.From.String(), @@ -349,10 +355,16 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // then we need to format native calls in a way that makes sense to Ethereum tooling (convert // the input & output to solidity ABI format). if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 1023 && et.Msg.Method != builtin.MethodsEVM.InvokeContract { - log.Infof("found Native call! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - input, _ := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + log.Debugf("found Native call! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + input, err := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + if err != nil { + return err + } trace.Action.Input = hex.EncodeToString(input) - output, _ := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + output, err := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + if err != nil { + return err + } trace.Result.Output = hex.EncodeToString(output) } @@ -360,14 +372,14 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // // Actor A calls to the init actor on method 2 and The init actor creates the target actor B then calls it on method 1 if parent.Action.To == builtin.InitActorAddr.String() && parent.Action.Method == 2 && et.Msg.Method == 1 { - log.Infof("Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + log.Debugf("Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) parent.setCallType("create") parent.Action.To = et.Msg.To.String() parent.Action.Input = hex.EncodeToString([]byte{0x0, 0x0, 0x0, 0xFE}) parent.Result.Output = "" // there should never be any subcalls when creating a native actor - return + return nil } // Handle EVM contract creation @@ -384,12 +396,12 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution initCreatedActor := trace.Action.Method == builtin.MethodsInit.Constructor if calledCreateOnEAM && eamCalledInitOnExec4 && initCreatedActor { - log.Infof("EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + log.Debugf("EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) if parent.parent.Action.Method == builtin.MethodsEAM.Create { - parent.parent.setCallType("CREATE") + parent.parent.setCallType("create") } else { - parent.parent.setCallType("CREATE2") + parent.parent.setCallType("create2") } // update the parent.parent to make this @@ -399,7 +411,7 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // delete the parent (the EAM) and skip the current trace (init) *traces = (*traces)[:len(*traces)-1] - return + return nil } } @@ -408,7 +420,7 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions // and should be dropped from the trace. if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 0 && et.Msg.Method <= 1023 { - log.Infof("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + log.Debugf("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.CodeCid.String(), height) // TODO: if I handle this case and drop this call from the trace then I am not able to detect delegate calls } @@ -416,22 +428,28 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // // Check for normal EVM to EVM calls and decode the params and return values if builtinactors.IsEvmActor(parent.Action.CodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin.MethodsEVM.InvokeContract { - log.Infof("found evm to evm! ! height: %d", height) - input, _ := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + log.Debugf("found evm to evm call, code:%s, height: %d", et.Msg.CodeCid.String(), height) + input, err := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + if err != nil { + return err + } trace.Action.Input = hex.EncodeToString(input) - output, _ := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + output, err := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + if err != nil { + return err + } trace.Result.Output = hex.EncodeToString(output) } // Handle delegate calls // - // 1) Look for from an EVM actor to itself on InvokeContractDelegate, method 6. - // 2) Search backwards in the trace for a call to another actor (A) on method 3 (GetBytecode) + // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate, method 6. + // 2) Check that the previous trace calls another actor on method 3 (GetByteCode) and they are at the same level (same parent) // 3) Treat this as a delegate call to actor A. if trace.Action.From == trace.Action.To && trace.Action.Method == builtin.MethodsEVM.InvokeContractDelegate && len(*traces) > 0 { - // the previous trace should be the GetBytecode call + log.Debugf("found delegate call, height: %d", height) prev := (*traces)[len(*traces)-1] - if prev.Action.From == trace.Action.From && prev.Action.Method == builtin.MethodsEVM.GetBytecode { + if prev.Action.From == trace.Action.From && prev.Action.Method == builtin.MethodsEVM.GetBytecode && prev.parent == trace.parent { trace.setCallType("delegatecall") trace.Action.To = prev.Action.To } @@ -441,8 +459,13 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution *traces = append(*traces, trace) for i, call := range et.Subcalls { - buildTraces(traces, trace, append(addr, i), call, height) + err := buildTraces(traces, trace, append(addr, i), call, height) + if err != nil { + return err + } } + + return nil } // TODO: refactor this to be shared code From 7f99d1507106d2ef75ffcab1e9326f4832cf94a0 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 8 Aug 2023 11:44:34 +0000 Subject: [PATCH 07/25] Small refactor and cleanup --- node/impl/full/trace.go | 186 +++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 88 deletions(-) diff --git a/node/impl/full/trace.go b/node/impl/full/trace.go index cab153e53b8..02ffdd46b9a 100644 --- a/node/impl/full/trace.go +++ b/node/impl/full/trace.go @@ -84,9 +84,8 @@ type Action struct { Input string `json:"input"` Value ethtypes.EthBigInt `json:"value"` - // TODO: remove these fields from json output - Method abi.MethodNum `json:"method"` - CodeCid cid.Cid `json:"codeCid"` + method abi.MethodNum `json:"-"` + codeCid cid.Cid `json:"-"` } type Result struct { @@ -225,17 +224,17 @@ func (e *EthTrace) TraceReplayBlockTransactions(ctx context.Context, blkNum stri return allTraces, nil } -func write_padded[T any](w io.Writer, data T, size int) error { +func writePadded[T any](w io.Writer, data T, size int) error { tmp := &bytes.Buffer{} // first write data to tmp buffer to get the size err := binary.Write(tmp, binary.BigEndian, data) if err != nil { - return fmt.Errorf("write_padded: failed writing tmp data to buffer: %w", err) + return fmt.Errorf("writePadded: failed writing tmp data to buffer: %w", err) } if tmp.Len() > size { - return fmt.Errorf("write_padded: data is larger than size") + return fmt.Errorf("writePadded: data is larger than size") } // write tailing zeros to pad up to size @@ -243,81 +242,19 @@ func write_padded[T any](w io.Writer, data T, size int) error { for i := 0; i < cnt; i++ { err = binary.Write(w, binary.BigEndian, uint8(0)) if err != nil { - return fmt.Errorf("write_padded: failed writing tailing zeros to buffer: %w", err) + return fmt.Errorf("writePadded: failed writing tailing zeros to buffer: %w", err) } } // finally write the actual value err = binary.Write(w, binary.BigEndian, tmp.Bytes()) if err != nil { - return fmt.Errorf("write_padded: failed writing data to buffer: %w", err) + return fmt.Errorf("writePadded: failed writing data to buffer: %w", err) } return nil } -func handle_filecoin_method_input(method abi.MethodNum, codec uint64, params []byte) ([]byte, error) { - NATIVE_METHOD_SELECTOR := []byte{0x86, 0x8e, 0x10, 0xc4} - EVM_WORD_SIZE := 32 - - staticArgs := []uint64{ - uint64(method), - codec, - uint64(EVM_WORD_SIZE) * 3, - uint64(len(params)), - } - totalWords := len(staticArgs) + (len(params) / EVM_WORD_SIZE) - if len(params)%EVM_WORD_SIZE != 0 { - totalWords++ - } - len := 4 + totalWords*EVM_WORD_SIZE - - w := &bytes.Buffer{} - err := binary.Write(w, binary.BigEndian, NATIVE_METHOD_SELECTOR) - if err != nil { - return nil, fmt.Errorf("handle_filecoin_method_input: failed writing method selector: %w", err) - } - - for _, arg := range staticArgs { - err := write_padded(w, arg, 32) - if err != nil { - return nil, fmt.Errorf("handle_filecoin_method_input: %w", err) - } - } - err = binary.Write(w, binary.BigEndian, params) - if err != nil { - return nil, fmt.Errorf("handle_filecoin_method_input: failed writing params: %w", err) - } - remain := len - w.Len() - for i := 0; i < remain; i++ { - err = binary.Write(w, binary.BigEndian, uint8(0)) - if err != nil { - return nil, fmt.Errorf("handle_filecoin_method_input: failed writing tailing zeros: %w", err) - } - } - - return w.Bytes(), nil -} - -func handle_filecoin_method_output(exitCode exitcode.ExitCode, codec uint64, data []byte) ([]byte, error) { - w := &bytes.Buffer{} - - values := []interface{}{uint32(exitCode), codec, uint32(w.Len()), uint32(len(data))} - for _, v := range values { - err := write_padded(w, v, 32) - if err != nil { - return nil, fmt.Errorf("handle_filecoin_method_output: %w", err) - } - } - - err := binary.Write(w, binary.BigEndian, []byte(data)) - if err != nil { - return nil, fmt.Errorf("handle_filecoin_method_output: failed writing data: %w", err) - } - - return w.Bytes(), nil -} - // buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.ExecutionTrace, height int64) error { trace := &Trace{ @@ -327,8 +264,8 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution Gas: ethtypes.EthUint64(et.Msg.GasLimit), Input: hex.EncodeToString(et.Msg.Params), Value: ethtypes.EthBigInt(et.Msg.Value), - Method: et.Msg.Method, - CodeCid: et.Msg.CodeCid, + method: et.Msg.Method, + codeCid: et.Msg.CodeCid, }, Result: Result{ GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), @@ -354,14 +291,16 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // When an EVM actor is invoked with a method number above 1023 that's not frc42(InvokeEVM) // then we need to format native calls in a way that makes sense to Ethereum tooling (convert // the input & output to solidity ABI format). - if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 1023 && et.Msg.Method != builtin.MethodsEVM.InvokeContract { + if builtinactors.IsEvmActor(parent.Action.codeCid) && + et.Msg.Method > 1023 && + et.Msg.Method != builtin.MethodsEVM.InvokeContract { log.Debugf("found Native call! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - input, err := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + input, err := handleFilecoinMethodInput(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) if err != nil { return err } trace.Action.Input = hex.EncodeToString(input) - output, err := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + output, err := handleFilecoinMethodOutput(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) if err != nil { return err } @@ -371,7 +310,9 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // Handle Native actor creation // // Actor A calls to the init actor on method 2 and The init actor creates the target actor B then calls it on method 1 - if parent.Action.To == builtin.InitActorAddr.String() && parent.Action.Method == 2 && et.Msg.Method == 1 { + if parent.Action.To == builtin.InitActorAddr.String() && + parent.Action.method == builtin.MethodsInit.Exec && + et.Msg.Method == builtin.MethodConstructor { log.Debugf("Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) parent.setCallType("create") parent.Action.To = et.Msg.To.String() @@ -390,15 +331,16 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // 2) The EAM calls the init actor on method 3 (Exec4). // 3) The init actor creates the target actor B then calls it on method 1. if parent.parent != nil { - calledCreateOnEAM := parent.parent.Action.To == builtin.EthereumAddressManagerActorAddr.String() && (parent.parent.Action.Method == builtin.MethodsEAM.Create || - parent.parent.Action.Method == builtin.MethodsEAM.Create2) - eamCalledInitOnExec4 := parent.Action.To == builtin.InitActorAddr.String() && parent.Action.Method == builtin.MethodsInit.Exec4 - initCreatedActor := trace.Action.Method == builtin.MethodsInit.Constructor + calledCreateOnEAM := parent.parent.Action.To == builtin.EthereumAddressManagerActorAddr.String() && + (parent.parent.Action.method == builtin.MethodsEAM.Create || parent.parent.Action.method == builtin.MethodsEAM.Create2) + eamCalledInitOnExec4 := parent.Action.To == builtin.InitActorAddr.String() && + parent.Action.method == builtin.MethodsInit.Exec4 + initCreatedActor := trace.Action.method == builtin.MethodConstructor if calledCreateOnEAM && eamCalledInitOnExec4 && initCreatedActor { log.Debugf("EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - if parent.parent.Action.Method == builtin.MethodsEAM.Create { + if parent.parent.Action.method == builtin.MethodsEAM.Create { parent.parent.setCallType("create") } else { parent.parent.setCallType("create2") @@ -419,22 +361,26 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions // and should be dropped from the trace. - if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 0 && et.Msg.Method <= 1023 { - log.Debugf("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.CodeCid.String(), height) + if builtinactors.IsEvmActor(parent.Action.codeCid) && + et.Msg.Method > 0 && + et.Msg.Method <= 1023 { + log.Debugf("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.codeCid.String(), height) // TODO: if I handle this case and drop this call from the trace then I am not able to detect delegate calls } // EVM -> EVM calls // // Check for normal EVM to EVM calls and decode the params and return values - if builtinactors.IsEvmActor(parent.Action.CodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin.MethodsEVM.InvokeContract { + if builtinactors.IsEvmActor(parent.Action.codeCid) && + builtinactors.IsEthAccountActor(et.Msg.CodeCid) && + et.Msg.Method == builtin.MethodsEVM.InvokeContract { log.Debugf("found evm to evm call, code:%s, height: %d", et.Msg.CodeCid.String(), height) - input, err := handle_filecoin_method_input(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + input, err := handleFilecoinMethodInput(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) if err != nil { return err } trace.Action.Input = hex.EncodeToString(input) - output, err := handle_filecoin_method_output(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + output, err := handleFilecoinMethodOutput(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) if err != nil { return err } @@ -446,10 +392,12 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate, method 6. // 2) Check that the previous trace calls another actor on method 3 (GetByteCode) and they are at the same level (same parent) // 3) Treat this as a delegate call to actor A. - if trace.Action.From == trace.Action.To && trace.Action.Method == builtin.MethodsEVM.InvokeContractDelegate && len(*traces) > 0 { + if trace.Action.From == trace.Action.To && + trace.Action.method == builtin.MethodsEVM.InvokeContractDelegate && + len(*traces) > 0 { log.Debugf("found delegate call, height: %d", height) prev := (*traces)[len(*traces)-1] - if prev.Action.From == trace.Action.From && prev.Action.Method == builtin.MethodsEVM.GetBytecode && prev.parent == trace.parent { + if prev.Action.From == trace.Action.From && prev.Action.method == builtin.MethodsEVM.GetBytecode && prev.parent == trace.parent { trace.setCallType("delegatecall") trace.Action.To = prev.Action.To } @@ -468,6 +416,68 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution return nil } +func handleFilecoinMethodInput(method abi.MethodNum, codec uint64, params []byte) ([]byte, error) { + NATIVE_METHOD_SELECTOR := []byte{0x86, 0x8e, 0x10, 0xc4} + EVM_WORD_SIZE := 32 + + staticArgs := []uint64{ + uint64(method), + codec, + uint64(EVM_WORD_SIZE) * 3, + uint64(len(params)), + } + totalWords := len(staticArgs) + (len(params) / EVM_WORD_SIZE) + if len(params)%EVM_WORD_SIZE != 0 { + totalWords++ + } + len := 4 + totalWords*EVM_WORD_SIZE + + w := &bytes.Buffer{} + err := binary.Write(w, binary.BigEndian, NATIVE_METHOD_SELECTOR) + if err != nil { + return nil, fmt.Errorf("handleFilecoinMethodInput: failed writing method selector: %w", err) + } + + for _, arg := range staticArgs { + err := writePadded(w, arg, 32) + if err != nil { + return nil, fmt.Errorf("handleFilecoinMethodInput: %w", err) + } + } + err = binary.Write(w, binary.BigEndian, params) + if err != nil { + return nil, fmt.Errorf("handleFilecoinMethodInput: failed writing params: %w", err) + } + remain := len - w.Len() + for i := 0; i < remain; i++ { + err = binary.Write(w, binary.BigEndian, uint8(0)) + if err != nil { + return nil, fmt.Errorf("handleFilecoinMethodInput: failed writing tailing zeros: %w", err) + } + } + + return w.Bytes(), nil +} + +func handleFilecoinMethodOutput(exitCode exitcode.ExitCode, codec uint64, data []byte) ([]byte, error) { + w := &bytes.Buffer{} + + values := []interface{}{uint32(exitCode), codec, uint32(w.Len()), uint32(len(data))} + for _, v := range values { + err := writePadded(w, v, 32) + if err != nil { + return nil, fmt.Errorf("handleFilecoinMethodOutput: %w", err) + } + } + + err := binary.Write(w, binary.BigEndian, data) + if err != nil { + return nil, fmt.Errorf("handleFilecoinMethodOutput: failed writing data: %w", err) + } + + return w.Bytes(), nil +} + // TODO: refactor this to be shared code func (e *EthTrace) getTipsetByBlockNr(ctx context.Context, blkParam string, strict bool) (*types.TipSet, error) { if blkParam == "earliest" { From ba1ee60d1b4f85b4b202b8c6076484ffbdd39cd8 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Wed, 16 Aug 2023 18:36:37 +0000 Subject: [PATCH 08/25] Refactor eth.go --- node/builder_chain.go | 3 - node/impl/full.go | 1 - node/impl/full/dummy.go | 1 - node/impl/full/eth.go | 1306 +++------------------ node/impl/full/eth_event.go | 382 ++++++ node/impl/full/{trace.go => eth_trace.go} | 256 +--- node/impl/full/eth_utils.go | 689 +++++++++++ node/impl/full/txhashmanager.go | 129 ++ node/modules/trace.go | 19 - 9 files changed, 1377 insertions(+), 1409 deletions(-) create mode 100644 node/impl/full/eth_event.go rename node/impl/full/{trace.go => eth_trace.go} (68%) create mode 100644 node/impl/full/eth_utils.go create mode 100644 node/impl/full/txhashmanager.go delete mode 100644 node/modules/trace.go diff --git a/node/builder_chain.go b/node/builder_chain.go index 0681020ed25..267659f0091 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -155,7 +155,6 @@ var ChainNode = Options( Override(new(stmgr.StateManagerAPI), rpcstmgr.NewRPCStateManager), Override(new(full.EthModuleAPI), From(new(api.Gateway))), Override(new(full.EthEventAPI), From(new(api.Gateway))), - Override(new(full.EthTraceAPI), From(new(api.Gateway))), ), // Full node API / service startup @@ -271,12 +270,10 @@ func ConfigFullNode(c interface{}) Option { If(cfg.Fevm.EnableEthRPC, Override(new(full.EthModuleAPI), modules.EthModuleAPI(cfg.Fevm)), Override(new(full.EthEventAPI), modules.EthEventAPI(cfg.Fevm)), - Override(new(full.EthTraceAPI), modules.EthTraceAPI()), ), If(!cfg.Fevm.EnableEthRPC, Override(new(full.EthModuleAPI), &full.EthModuleDummy{}), Override(new(full.EthEventAPI), &full.EthModuleDummy{}), - Override(new(full.EthTraceAPI), &full.EthModuleDummy{}), ), ), diff --git a/node/impl/full.go b/node/impl/full.go index 0f87cfe292c..affcc960e09 100644 --- a/node/impl/full.go +++ b/node/impl/full.go @@ -36,7 +36,6 @@ type FullNodeAPI struct { full.SyncAPI full.RaftAPI full.EthAPI - full.EthTraceAPI DS dtypes.MetadataDS NetworkName dtypes.NetworkName diff --git a/node/impl/full/dummy.go b/node/impl/full/dummy.go index 7412ed7549b..c06e5c084e5 100644 --- a/node/impl/full/dummy.go +++ b/node/impl/full/dummy.go @@ -188,4 +188,3 @@ func (e *EthModuleDummy) TraceReplayBlockTransactions(ctx context.Context, blkNu var _ EthModuleAPI = &EthModuleDummy{} var _ EthEventAPI = &EthModuleDummy{} -var _ EthTraceAPI = &EthModuleDummy{} diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 424756f8140..a2f406b7c2d 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -3,20 +3,17 @@ package full import ( "bytes" "context" - "encoding/json" + "encoding/hex" "errors" "fmt" "os" "sort" "strconv" "strings" - "sync" "time" - "github.com/google/uuid" "github.com/ipfs/go-cid" cbg "github.com/whyrusleeping/cbor-gen" - "github.com/zyedidia/generic/queue" "go.uber.org/fx" "golang.org/x/xerrors" @@ -25,9 +22,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" builtintypes "github.com/filecoin-project/go-state-types/builtin" - "github.com/filecoin-project/go-state-types/builtin/v10/eam" "github.com/filecoin-project/go-state-types/builtin/v10/evm" - "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-project/go-state-types/exitcode" "github.com/filecoin-project/lotus/api" @@ -42,7 +37,6 @@ import ( "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" - "github.com/filecoin-project/lotus/chain/vm" "github.com/filecoin-project/lotus/node/modules/dtypes" ) @@ -77,6 +71,8 @@ type EthModuleAPI interface { EthMaxPriorityFeePerGas(ctx context.Context) (ethtypes.EthBigInt, error) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) Web3ClientVersion(ctx context.Context) (string, error) + TraceBlock(ctx context.Context, blkNum string) (interface{}, error) + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) } type EthEventAPI interface { @@ -241,101 +237,8 @@ func (a *EthModule) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthH return newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, a.Chain, a.StateAPI) } -func (a *EthModule) getTipsetByEthBlockNumberOrHash(ctx context.Context, blkParam ethtypes.EthBlockNumberOrHash) (*types.TipSet, error) { - head := a.Chain.GetHeaviestTipSet() - - predefined := blkParam.PredefinedBlock - if predefined != nil { - if *predefined == "earliest" { - return nil, fmt.Errorf("block param \"earliest\" is not supported") - } else if *predefined == "pending" { - return head, nil - } else if *predefined == "latest" { - parent, err := a.Chain.GetTipSetFromKey(ctx, head.Parents()) - if err != nil { - return nil, fmt.Errorf("cannot get parent tipset") - } - return parent, nil - } else { - return nil, fmt.Errorf("unknown predefined block %s", *predefined) - } - } - - if blkParam.BlockNumber != nil { - height := abi.ChainEpoch(*blkParam.BlockNumber) - if height > head.Height()-1 { - return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") - } - ts, err := a.ChainAPI.ChainGetTipSetByHeight(ctx, height, head.Key()) - if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", height) - } - return ts, nil - } - - if blkParam.BlockHash != nil { - ts, err := a.Chain.GetTipSetByCid(ctx, blkParam.BlockHash.ToCid()) - if err != nil { - return nil, fmt.Errorf("cannot get tipset by hash: %v", err) - } - - // verify that the tipset is in the canonical chain - if blkParam.RequireCanonical { - // walk up the current chain (our head) until we reach ts.Height() - walkTs, err := a.ChainAPI.ChainGetTipSetByHeight(ctx, ts.Height(), head.Key()) - if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height()) - } - - // verify that it equals the expected tipset - if !walkTs.Equals(ts) { - return nil, fmt.Errorf("tipset is not canonical") - } - } - - return ts, nil - } - - return nil, errors.New("invalid block param") -} - -func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string, strict bool) (*types.TipSet, error) { - if blkParam == "earliest" { - return nil, fmt.Errorf("block param \"earliest\" is not supported") - } - - head := a.Chain.GetHeaviestTipSet() - switch blkParam { - case "pending": - return head, nil - case "latest": - parent, err := a.Chain.GetTipSetFromKey(ctx, head.Parents()) - if err != nil { - return nil, fmt.Errorf("cannot get parent tipset") - } - return parent, nil - default: - var num ethtypes.EthUint64 - err := num.UnmarshalJSON([]byte(`"` + blkParam + `"`)) - if err != nil { - return nil, fmt.Errorf("cannot parse block number: %v", err) - } - if abi.ChainEpoch(num) > head.Height()-1 { - return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") - } - ts, err := a.ChainAPI.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(num), head.Key()) - if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", num) - } - if strict && ts.Height() != abi.ChainEpoch(num) { - return nil, ErrNullRound - } - return ts, nil - } -} - func (a *EthModule) EthGetBlockByNumber(ctx context.Context, blkParam string, fullTxInfo bool) (ethtypes.EthBlock, error) { - ts, err := a.parseBlkParam(ctx, blkParam, true) + ts, err := getTipsetByBlockNr(ctx, a.Chain, blkParam, true) if err != nil { return ethtypes.EthBlock{}, err } @@ -431,7 +334,7 @@ func (a *EthModule) EthGetMessageCidByTransactionHash(ctx context.Context, txHas } func (a *EthModule) EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) { - hash, err := EthTxHashFromMessageCid(ctx, cid, a.StateAPI) + hash, err := ethTxHashFromMessageCid(ctx, cid, a.StateAPI) if hash == ethtypes.EmptyEthHash { // not found return nil, nil @@ -446,7 +349,7 @@ func (a *EthModule) EthGetTransactionCount(ctx context.Context, sender ethtypes. return ethtypes.EthUint64(0), nil } - ts, err := a.getTipsetByEthBlockNumberOrHash(ctx, blkParam) + ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) if err != nil { return ethtypes.EthUint64(0), xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) } @@ -534,7 +437,7 @@ func (a *EthModule) EthGetCode(ctx context.Context, ethAddr ethtypes.EthAddress, return nil, xerrors.Errorf("cannot get Filecoin address: %w", err) } - ts, err := a.getTipsetByEthBlockNumberOrHash(ctx, blkParam) + ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) if err != nil { return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) } @@ -613,7 +516,7 @@ func (a *EthModule) EthGetCode(ctx context.Context, ethAddr ethtypes.EthAddress, } func (a *EthModule) EthGetStorageAt(ctx context.Context, ethAddr ethtypes.EthAddress, position ethtypes.EthBytes, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { - ts, err := a.getTipsetByEthBlockNumberOrHash(ctx, blkParam) + ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) if err != nil { return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) } @@ -709,7 +612,7 @@ func (a *EthModule) EthGetBalance(ctx context.Context, address ethtypes.EthAddre return ethtypes.EthBigInt{}, err } - ts, err := a.getTipsetByEthBlockNumberOrHash(ctx, blkParam) + ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) if err != nil { return ethtypes.EthBigInt{}, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) } @@ -790,7 +693,7 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth } } - ts, err := a.parseBlkParam(ctx, params.NewestBlkNum, false) + ts, err := getTipsetByBlockNr(ctx, a.Chain, params.NewestBlkNum, false) if err != nil { return ethtypes.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err) } @@ -922,62 +825,135 @@ func (a *EthModule) Web3ClientVersion(ctx context.Context) (string, error) { return build.UserVersion(), nil } -func (a *EthModule) ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types.Message, error) { - var from address.Address - if tx.From == nil || *tx.From == (ethtypes.EthAddress{}) { - // Send from the filecoin "system" address. - var err error - from, err = (ethtypes.EthAddress{}).ToFilecoinAddress() +func (e *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { + ts, err := getTipsetByBlockNr(ctx, e.Chain, blkNum, false) + if err != nil { + return nil, err + } + + _, trace, err := e.StateManager.ExecutionTrace(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("failed to compute base state: %w", err) + } + + tsParent, err := e.ChainAPI.ChainGetTipSetByHeight(ctx, ts.Height()+1, e.Chain.GetHeaviestTipSet().Key()) + if err != nil { + return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height()+1) + } + + msgs, err := e.ChainGetParentMessages(ctx, tsParent.Blocks()[0].Cid()) + if err != nil { + return nil, err + } + + cid, err := ts.Key().Cid() + if err != nil { + return nil, err + } + + blkHash, err := ethtypes.EthHashFromCid(cid) + if err != nil { + return nil, err + } + + allTraces := make([]*TraceBlock, 0, len(trace)) + for _, ir := range trace { + // ignore messages from f00 + if ir.Msg.From.String() == "f00" { + continue + } + + idx := -1 + for msgIdx, msg := range msgs { + if ir.Msg.From == msg.Message.From { + idx = msgIdx + break + } + } + if idx == -1 { + log.Warnf("cannot resolve message index for cid: %s", ir.MsgCid) + continue + } + + txHash, err := e.EthGetTransactionHashByCid(ctx, ir.MsgCid) if err != nil { - return nil, fmt.Errorf("failed to construct the ethereum system address: %w", err) + return nil, err } - } else { - // The from address must be translatable to an f4 address. - var err error - from, err = tx.From.ToFilecoinAddress() + if txHash == nil { + log.Warnf("cannot find transaction hash for cid %s", ir.MsgCid) + continue + } + + traces := []*Trace{} + err = buildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) if err != nil { - return nil, fmt.Errorf("failed to translate sender address (%s): %w", tx.From.String(), err) + return nil, xerrors.Errorf("failed when building traces: %w", err) } - if p := from.Protocol(); p != address.Delegated { - return nil, fmt.Errorf("expected a class 4 address, got: %d: %w", p, err) + + traceBlocks := make([]*TraceBlock, 0, len(trace)) + for _, trace := range traces { + traceBlocks = append(traceBlocks, &TraceBlock{ + Trace: trace, + BlockHash: blkHash, + BlockNumber: int64(ts.Height()), + TransactionHash: *txHash, + TransactionPosition: idx, + }) } + + allTraces = append(allTraces, traceBlocks...) + } + + return allTraces, nil +} + +func (e *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { + if len(traceTypes) != 1 || traceTypes[0] != "trace" { + return nil, fmt.Errorf("only 'trace' is supported") + } + + ts, err := getTipsetByBlockNr(ctx, e.Chain, blkNum, false) + if err != nil { + return nil, err + } + + _, trace, err := e.StateManager.ExecutionTrace(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) } - var params []byte - if len(tx.Data) > 0 { - initcode := abi.CborBytes(tx.Data) - params2, err := actors.SerializeParams(&initcode) + allTraces := make([]*TraceReplayBlockTransaction, 0, len(trace)) + for _, ir := range trace { + // ignore messages from f00 + if ir.Msg.From.String() == "f00" { + continue + } + + txHash, err := e.EthGetTransactionHashByCid(ctx, ir.MsgCid) if err != nil { - return nil, fmt.Errorf("failed to serialize params: %w", err) + return nil, err + } + if txHash == nil { + log.Warnf("cannot find transaction hash for cid %s", ir.MsgCid) + continue } - params = params2 - } - var to address.Address - var method abi.MethodNum - if tx.To == nil { - // this is a contract creation - to = builtintypes.EthereumAddressManagerActorAddr - method = builtintypes.MethodsEAM.CreateExternal - } else { - addr, err := tx.To.ToFilecoinAddress() + t := TraceReplayBlockTransaction{ + Output: hex.EncodeToString(ir.MsgRct.Return), + TransactionHash: *txHash, + StateDiff: nil, + VmTrace: nil, + } + + err = buildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) if err != nil { - return nil, xerrors.Errorf("cannot get Filecoin address: %w", err) + return nil, xerrors.Errorf("failed when building traces: %w", err) } - to = addr - method = builtintypes.MethodsEVM.InvokeContract + + allTraces = append(allTraces, &t) } - return &types.Message{ - From: from, - To: to, - Value: big.Int(tx.Value), - Method: method, - Params: params, - GasLimit: build.BlockGasLimit, - GasFeeCap: big.Zero(), - GasPremium: big.Zero(), - }, nil + return allTraces, nil } func (a *EthModule) applyMessage(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (res *api.InvocResult, err error) { @@ -1013,7 +989,7 @@ func (a *EthModule) applyMessage(ctx context.Context, msg *types.Message, tsk ty } func (a *EthModule) EthEstimateGas(ctx context.Context, tx ethtypes.EthCall) (ethtypes.EthUint64, error) { - msg, err := a.ethCallToFilecoinMessage(ctx, tx) + msg, err := ethCallToFilecoinMessage(ctx, tx) if err != nil { return ethtypes.EthUint64(0), err } @@ -1171,12 +1147,12 @@ func ethGasSearch( } func (a *EthModule) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { - msg, err := a.ethCallToFilecoinMessage(ctx, tx) + msg, err := ethCallToFilecoinMessage(ctx, tx) if err != nil { return nil, xerrors.Errorf("failed to convert ethcall to filecoin message: %w", err) } - ts, err := a.getTipsetByEthBlockNumberOrHash(ctx, blkParam) + ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) if err != nil { return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) } @@ -1577,1027 +1553,37 @@ func (e *EthEvent) GC(ctx context.Context, ttl time.Duration) { } } -type filterEventCollector interface { - TakeCollectedEvents(context.Context) []*filter.CollectedEvent -} - -type filterMessageCollector interface { - TakeCollectedMessages(context.Context) []*types.SignedMessage -} - -type filterTipSetCollector interface { - TakeCollectedTipSets(context.Context) []types.TipSetKey -} - -func ethLogFromEvent(entries []types.EventEntry) (data []byte, topics []ethtypes.EthHash, ok bool) { - var ( - topicsFound [4]bool - topicsFoundCount int - dataFound bool - ) - // Topics must be non-nil, even if empty. So we might as well pre-allocate for 4 (the max). - topics = make([]ethtypes.EthHash, 0, 4) - for _, entry := range entries { - // Drop events with non-raw topics to avoid mistakes. - if entry.Codec != cid.Raw { - log.Warnw("did not expect an event entry with a non-raw codec", "codec", entry.Codec, "key", entry.Key) - return nil, nil, false - } - // Check if the key is t1..t4 - if len(entry.Key) == 2 && "t1" <= entry.Key && entry.Key <= "t4" { - // '1' - '1' == 0, etc. - idx := int(entry.Key[1] - '1') - - // Drop events with mis-sized topics. - if len(entry.Value) != 32 { - log.Warnw("got an EVM event topic with an invalid size", "key", entry.Key, "size", len(entry.Value)) - return nil, nil, false - } - - // Drop events with duplicate topics. - if topicsFound[idx] { - log.Warnw("got a duplicate EVM event topic", "key", entry.Key) - return nil, nil, false - } - topicsFound[idx] = true - topicsFoundCount++ - - // Extend the topics array - for len(topics) <= idx { - topics = append(topics, ethtypes.EthHash{}) - } - copy(topics[idx][:], entry.Value) - } else if entry.Key == "d" { - // Drop events with duplicate data fields. - if dataFound { - log.Warnw("got duplicate EVM event data") - return nil, nil, false - } - - dataFound = true - data = entry.Value - } else { - // Skip entries we don't understand (makes it easier to extend things). - // But we warn for now because we don't expect them. - log.Warnw("unexpected event entry", "key", entry.Key) - } - - } - - // Drop events with skipped topics. - if len(topics) != topicsFoundCount { - log.Warnw("EVM event topic length mismatch", "expected", len(topics), "actual", topicsFoundCount) - return nil, nil, false - } - return data, topics, true -} - -func ethFilterResultFromEvents(evs []*filter.CollectedEvent, sa StateAPI) (*ethtypes.EthFilterResult, error) { - res := ðtypes.EthFilterResult{} - for _, ev := range evs { - log := ethtypes.EthLog{ - Removed: ev.Reverted, - LogIndex: ethtypes.EthUint64(ev.EventIdx), - TransactionIndex: ethtypes.EthUint64(ev.MsgIdx), - BlockNumber: ethtypes.EthUint64(ev.Height), - } - var ( - err error - ok bool - ) - - log.Data, log.Topics, ok = ethLogFromEvent(ev.Entries) - if !ok { - continue - } - - log.Address, err = ethtypes.EthAddressFromFilecoinAddress(ev.EmitterAddr) - if err != nil { - return nil, err - } - - log.TransactionHash, err = EthTxHashFromMessageCid(context.TODO(), ev.MsgCid, sa) - if err != nil { - return nil, err - } - c, err := ev.TipSetKey.Cid() - if err != nil { - return nil, err - } - log.BlockHash, err = ethtypes.EthHashFromCid(c) - if err != nil { - return nil, err - } - - res.Results = append(res.Results, log) - } - - return res, nil -} - -func ethFilterResultFromTipSets(tsks []types.TipSetKey) (*ethtypes.EthFilterResult, error) { - res := ðtypes.EthFilterResult{} - - for _, tsk := range tsks { - c, err := tsk.Cid() - if err != nil { - return nil, err - } - hash, err := ethtypes.EthHashFromCid(c) - if err != nil { - return nil, err - } - - res.Results = append(res.Results, hash) - } - - return res, nil -} - -func ethFilterResultFromMessages(cs []*types.SignedMessage, sa StateAPI) (*ethtypes.EthFilterResult, error) { - res := ðtypes.EthFilterResult{} - - for _, c := range cs { - hash, err := EthTxHashFromSignedMessage(context.TODO(), c, sa) - if err != nil { - return nil, err - } - - res.Results = append(res.Results, hash) - } - - return res, nil -} - -type EthSubscriptionManager struct { - Chain *store.ChainStore - StateAPI StateAPI - ChainAPI ChainAPI - mu sync.Mutex - subs map[ethtypes.EthSubscriptionID]*ethSubscription -} - -func (e *EthSubscriptionManager) StartSubscription(ctx context.Context, out ethSubscriptionCallback, dropFilter func(context.Context, filter.Filter) error) (*ethSubscription, error) { // nolint - rawid, err := uuid.NewRandom() - if err != nil { - return nil, xerrors.Errorf("new uuid: %w", err) - } - id := ethtypes.EthSubscriptionID{} - copy(id[:], rawid[:]) // uuid is 16 bytes - - ctx, quit := context.WithCancel(ctx) - - sub := ðSubscription{ - Chain: e.Chain, - StateAPI: e.StateAPI, - ChainAPI: e.ChainAPI, - uninstallFilter: dropFilter, - id: id, - in: make(chan interface{}, 200), - out: out, - quit: quit, - - toSend: queue.New[[]byte](), - sendCond: make(chan struct{}, 1), +func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRewardSorter) ([]ethtypes.EthBigInt, int64) { + var gasUsedTotal int64 + for _, tx := range txGasRewards { + gasUsedTotal += tx.gasUsed } - e.mu.Lock() - if e.subs == nil { - e.subs = make(map[ethtypes.EthSubscriptionID]*ethSubscription) + rewards := make([]ethtypes.EthBigInt, len(rewardPercentiles)) + for i := range rewards { + rewards[i] = ethtypes.EthBigInt(types.NewInt(MinGasPremium)) } - e.subs[sub.id] = sub - e.mu.Unlock() - - go sub.start(ctx) - go sub.startOut(ctx) - - return sub, nil -} - -func (e *EthSubscriptionManager) StopSubscription(ctx context.Context, id ethtypes.EthSubscriptionID) error { - e.mu.Lock() - defer e.mu.Unlock() - sub, ok := e.subs[id] - if !ok { - return xerrors.Errorf("subscription not found") + if len(txGasRewards) == 0 { + return rewards, gasUsedTotal } - sub.stop() - delete(e.subs, id) - - return nil -} - -type ethSubscriptionCallback func(context.Context, jsonrpc.RawParams) error - -const maxSendQueue = 20000 - -type ethSubscription struct { - Chain *store.ChainStore - StateAPI StateAPI - ChainAPI ChainAPI - uninstallFilter func(context.Context, filter.Filter) error - id ethtypes.EthSubscriptionID - in chan interface{} - out ethSubscriptionCallback - - mu sync.Mutex - filters []filter.Filter - quit func() - - sendLk sync.Mutex - sendQueueLen int - toSend *queue.Queue[[]byte] - sendCond chan struct{} -} - -func (e *ethSubscription) addFilter(ctx context.Context, f filter.Filter) { - e.mu.Lock() - defer e.mu.Unlock() - - f.SetSubChannel(e.in) - e.filters = append(e.filters, f) -} -// sendOut processes the final subscription queue. It's here in case the subscriber -// is slow, and we need to buffer the messages. -func (e *ethSubscription) startOut(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case <-e.sendCond: - e.sendLk.Lock() - - for !e.toSend.Empty() { - front := e.toSend.Dequeue() - e.sendQueueLen-- - - e.sendLk.Unlock() - - if err := e.out(ctx, front); err != nil { - log.Warnw("error sending subscription response, killing subscription", "sub", e.id, "error", err) - e.stop() - return - } - - e.sendLk.Lock() - } + sort.Stable(txGasRewards) - e.sendLk.Unlock() + var idx int + var sum int64 + for i, percentile := range rewardPercentiles { + threshold := int64(float64(gasUsedTotal) * percentile / 100) + for sum < threshold && idx < len(txGasRewards)-1 { + sum += txGasRewards[idx].gasUsed + idx++ } - } -} - -func (e *ethSubscription) send(ctx context.Context, v interface{}) { - resp := ethtypes.EthSubscriptionResponse{ - SubscriptionID: e.id, - Result: v, - } - - outParam, err := json.Marshal(resp) - if err != nil { - log.Warnw("marshaling subscription response", "sub", e.id, "error", err) - return - } - - e.sendLk.Lock() - defer e.sendLk.Unlock() - - e.toSend.Enqueue(outParam) - - e.sendQueueLen++ - if e.sendQueueLen > maxSendQueue { - log.Warnw("subscription send queue full, killing subscription", "sub", e.id) - e.stop() - return - } - - select { - case e.sendCond <- struct{}{}: - default: // already signalled, and we're holding the lock so we know that the event will be processed - } -} - -func (e *ethSubscription) start(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case v := <-e.in: - switch vt := v.(type) { - case *filter.CollectedEvent: - evs, err := ethFilterResultFromEvents([]*filter.CollectedEvent{vt}, e.StateAPI) - if err != nil { - continue - } - - for _, r := range evs.Results { - e.send(ctx, r) - } - case *types.TipSet: - ev, err := newEthBlockFromFilecoinTipSet(ctx, vt, true, e.Chain, e.StateAPI) - if err != nil { - break - } - - e.send(ctx, ev) - case *types.SignedMessage: // mpool txid - evs, err := ethFilterResultFromMessages([]*types.SignedMessage{vt}, e.StateAPI) - if err != nil { - continue - } - - for _, r := range evs.Results { - e.send(ctx, r) - } - default: - log.Warnf("unexpected subscription value type: %T", vt) - } - } - } -} - -func (e *ethSubscription) stop() { - e.mu.Lock() - if e.quit == nil { - e.mu.Unlock() - return - } - - if e.quit != nil { - e.quit() - e.quit = nil - e.mu.Unlock() - - for _, f := range e.filters { - // note: the context in actually unused in uninstallFilter - if err := e.uninstallFilter(context.TODO(), f); err != nil { - // this will leave the filter a zombie, collecting events up to the maximum allowed - log.Warnf("failed to remove filter when unsubscribing: %v", err) - } - } - } -} - -func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTxInfo bool, cs *store.ChainStore, sa StateAPI) (ethtypes.EthBlock, error) { - parentKeyCid, err := ts.Parents().Cid() - if err != nil { - return ethtypes.EthBlock{}, err - } - parentBlkHash, err := ethtypes.EthHashFromCid(parentKeyCid) - if err != nil { - return ethtypes.EthBlock{}, err - } - - bn := ethtypes.EthUint64(ts.Height()) - - blkCid, err := ts.Key().Cid() - if err != nil { - return ethtypes.EthBlock{}, err - } - blkHash, err := ethtypes.EthHashFromCid(blkCid) - if err != nil { - return ethtypes.EthBlock{}, err - } - - msgs, rcpts, err := messagesAndReceipts(ctx, ts, cs, sa) - if err != nil { - return ethtypes.EthBlock{}, xerrors.Errorf("failed to retrieve messages and receipts: %w", err) - } - - block := ethtypes.NewEthBlock(len(msgs) > 0) - - gasUsed := int64(0) - for i, msg := range msgs { - rcpt := rcpts[i] - ti := ethtypes.EthUint64(i) - gasUsed += rcpt.GasUsed - var smsg *types.SignedMessage - switch msg := msg.(type) { - case *types.SignedMessage: - smsg = msg - case *types.Message: - smsg = &types.SignedMessage{ - Message: *msg, - Signature: crypto.Signature{ - Type: crypto.SigTypeBLS, - }, - } - default: - return ethtypes.EthBlock{}, xerrors.Errorf("failed to get signed msg %s: %w", msg.Cid(), err) - } - tx, err := newEthTxFromSignedMessage(ctx, smsg, sa) - if err != nil { - return ethtypes.EthBlock{}, xerrors.Errorf("failed to convert msg to ethTx: %w", err) - } - - tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId) - tx.BlockHash = &blkHash - tx.BlockNumber = &bn - tx.TransactionIndex = &ti - - if fullTxInfo { - block.Transactions = append(block.Transactions, tx) - } else { - block.Transactions = append(block.Transactions, tx.Hash.String()) - } - } - - block.Hash = blkHash - block.Number = bn - block.ParentHash = parentBlkHash - block.Timestamp = ethtypes.EthUint64(ts.Blocks()[0].Timestamp) - block.BaseFeePerGas = ethtypes.EthBigInt{Int: ts.Blocks()[0].ParentBaseFee.Int} - block.GasUsed = ethtypes.EthUint64(gasUsed) - return block, nil -} - -func messagesAndReceipts(ctx context.Context, ts *types.TipSet, cs *store.ChainStore, sa StateAPI) ([]types.ChainMsg, []types.MessageReceipt, error) { - msgs, err := cs.MessagesForTipset(ctx, ts) - if err != nil { - return nil, nil, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) - } - - _, rcptRoot, err := sa.StateManager.TipSetState(ctx, ts) - if err != nil { - return nil, nil, xerrors.Errorf("failed to compute state: %w", err) - } - - rcpts, err := cs.ReadReceipts(ctx, rcptRoot) - if err != nil { - return nil, nil, xerrors.Errorf("error loading receipts for tipset: %v: %w", ts, err) - } - - if len(msgs) != len(rcpts) { - return nil, nil, xerrors.Errorf("receipts and message array lengths didn't match for tipset: %v: %w", ts, err) - } - - return msgs, rcpts, nil -} - -// lookupEthAddress makes its best effort at finding the Ethereum address for a -// Filecoin address. It does the following: -// -// 1. If the supplied address is an f410 address, we return its payload as the EthAddress. -// 2. Otherwise (f0, f1, f2, f3), we look up the actor on the state tree. If it has a delegated address, we return it if it's f410 address. -// 3. Otherwise, we fall back to returning a masked ID Ethereum address. If the supplied address is an f0 address, we -// use that ID to form the masked ID address. -// 4. Otherwise, we fetch the actor's ID from the state tree and form the masked ID with it. -func lookupEthAddress(ctx context.Context, addr address.Address, sa StateAPI) (ethtypes.EthAddress, error) { - // BLOCK A: We are trying to get an actual Ethereum address from an f410 address. - // Attempt to convert directly, if it's an f4 address. - ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(addr) - if err == nil && !ethAddr.IsMaskedID() { - return ethAddr, nil - } - - // Lookup on the target actor and try to get an f410 address. - if actor, err := sa.StateGetActor(ctx, addr, types.EmptyTSK); err != nil { - return ethtypes.EthAddress{}, err - } else if actor.Address != nil { - if ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(*actor.Address); err == nil && !ethAddr.IsMaskedID() { - return ethAddr, nil - } - } - - // BLOCK B: We gave up on getting an actual Ethereum address and are falling back to a Masked ID address. - // Check if we already have an ID addr, and use it if possible. - if err == nil && ethAddr.IsMaskedID() { - return ethAddr, nil - } - - // Otherwise, resolve the ID addr. - idAddr, err := sa.StateLookupID(ctx, addr, types.EmptyTSK) - if err != nil { - return ethtypes.EthAddress{}, err - } - return ethtypes.EthAddressFromFilecoinAddress(idAddr) -} - -func EthTxHashFromMessageCid(ctx context.Context, c cid.Cid, sa StateAPI) (ethtypes.EthHash, error) { - smsg, err := sa.Chain.GetSignedMessage(ctx, c) - if err == nil { - // This is an Eth Tx, Secp message, Or BLS message in the mpool - return EthTxHashFromSignedMessage(ctx, smsg, sa) - } - - _, err = sa.Chain.GetMessage(ctx, c) - if err == nil { - // This is a BLS message - return ethtypes.EthHashFromCid(c) - } - - return ethtypes.EmptyEthHash, nil -} - -func EthTxHashFromSignedMessage(ctx context.Context, smsg *types.SignedMessage, sa StateAPI) (ethtypes.EthHash, error) { - if smsg.Signature.Type == crypto.SigTypeDelegated { - ethTx, err := newEthTxFromSignedMessage(ctx, smsg, sa) - if err != nil { - return ethtypes.EmptyEthHash, err - } - return ethTx.Hash, nil - } else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { - return ethtypes.EthHashFromCid(smsg.Cid()) - } else { // BLS message - return ethtypes.EthHashFromCid(smsg.Message.Cid()) - } -} - -func newEthTxFromSignedMessage(ctx context.Context, smsg *types.SignedMessage, sa StateAPI) (ethtypes.EthTx, error) { - var tx ethtypes.EthTx - var err error - - // This is an eth tx - if smsg.Signature.Type == crypto.SigTypeDelegated { - tx, err = ethtypes.EthTxFromSignedEthMessage(smsg) - if err != nil { - return ethtypes.EthTx{}, xerrors.Errorf("failed to convert from signed message: %w", err) - } - - tx.Hash, err = tx.TxHash() - if err != nil { - return ethtypes.EthTx{}, xerrors.Errorf("failed to calculate hash for ethTx: %w", err) - } - - fromAddr, err := lookupEthAddress(ctx, smsg.Message.From, sa) - if err != nil { - return ethtypes.EthTx{}, xerrors.Errorf("failed to resolve Ethereum address: %w", err) - } - - tx.From = fromAddr - } else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { // Secp Filecoin Message - tx = ethTxFromNativeMessage(ctx, smsg.VMMessage(), sa) - tx.Hash, err = ethtypes.EthHashFromCid(smsg.Cid()) - if err != nil { - return tx, err - } - } else { // BLS Filecoin message - tx = ethTxFromNativeMessage(ctx, smsg.VMMessage(), sa) - tx.Hash, err = ethtypes.EthHashFromCid(smsg.Message.Cid()) - if err != nil { - return tx, err - } - } - - return tx, nil -} - -// ethTxFromNativeMessage does NOT populate: -// - BlockHash -// - BlockNumber -// - TransactionIndex -// - Hash -func ethTxFromNativeMessage(ctx context.Context, msg *types.Message, sa StateAPI) ethtypes.EthTx { - // We don't care if we error here, conversion is best effort for non-eth transactions - from, _ := lookupEthAddress(ctx, msg.From, sa) - to, _ := lookupEthAddress(ctx, msg.To, sa) - return ethtypes.EthTx{ - To: &to, - From: from, - Nonce: ethtypes.EthUint64(msg.Nonce), - ChainID: ethtypes.EthUint64(build.Eip155ChainId), - Value: ethtypes.EthBigInt(msg.Value), - Type: ethtypes.Eip1559TxType, - Gas: ethtypes.EthUint64(msg.GasLimit), - MaxFeePerGas: ethtypes.EthBigInt(msg.GasFeeCap), - MaxPriorityFeePerGas: ethtypes.EthBigInt(msg.GasPremium), - AccessList: []ethtypes.EthHash{}, - } -} - -// newEthTxFromMessageLookup creates an ethereum transaction from filecoin message lookup. If a negative txIdx is passed -// into the function, it looks up the transaction index of the message in the tipset, otherwise it uses the txIdx passed into the -// function -func newEthTxFromMessageLookup(ctx context.Context, msgLookup *api.MsgLookup, txIdx int, cs *store.ChainStore, sa StateAPI) (ethtypes.EthTx, error) { - ts, err := cs.LoadTipSet(ctx, msgLookup.TipSet) - if err != nil { - return ethtypes.EthTx{}, err - } - - // This tx is located in the parent tipset - parentTs, err := cs.LoadTipSet(ctx, ts.Parents()) - if err != nil { - return ethtypes.EthTx{}, err - } - - parentTsCid, err := parentTs.Key().Cid() - if err != nil { - return ethtypes.EthTx{}, err - } - - // lookup the transactionIndex - if txIdx < 0 { - msgs, err := cs.MessagesForTipset(ctx, parentTs) - if err != nil { - return ethtypes.EthTx{}, err - } - for i, msg := range msgs { - if msg.Cid() == msgLookup.Message { - txIdx = i - break - } - } - if txIdx < 0 { - return ethtypes.EthTx{}, fmt.Errorf("cannot find the msg in the tipset") - } - } - - blkHash, err := ethtypes.EthHashFromCid(parentTsCid) - if err != nil { - return ethtypes.EthTx{}, err - } - - smsg, err := getSignedMessage(ctx, cs, msgLookup.Message) - if err != nil { - return ethtypes.EthTx{}, xerrors.Errorf("failed to get signed msg: %w", err) - } - - tx, err := newEthTxFromSignedMessage(ctx, smsg, sa) - if err != nil { - return ethtypes.EthTx{}, err - } - - var ( - bn = ethtypes.EthUint64(parentTs.Height()) - ti = ethtypes.EthUint64(txIdx) - ) - - tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId) - tx.BlockHash = &blkHash - tx.BlockNumber = &bn - tx.TransactionIndex = &ti - return tx, nil -} - -func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLookup, events []types.Event, cs *store.ChainStore, sa StateAPI) (api.EthTxReceipt, error) { - var ( - transactionIndex ethtypes.EthUint64 - blockHash ethtypes.EthHash - blockNumber ethtypes.EthUint64 - ) - - if tx.TransactionIndex != nil { - transactionIndex = *tx.TransactionIndex - } - if tx.BlockHash != nil { - blockHash = *tx.BlockHash - } - if tx.BlockNumber != nil { - blockNumber = *tx.BlockNumber - } - - receipt := api.EthTxReceipt{ - TransactionHash: tx.Hash, - From: tx.From, - To: tx.To, - TransactionIndex: transactionIndex, - BlockHash: blockHash, - BlockNumber: blockNumber, - Type: ethtypes.EthUint64(2), - Logs: []ethtypes.EthLog{}, // empty log array is compulsory when no logs, or libraries like ethers.js break - LogsBloom: ethtypes.EmptyEthBloom[:], - } - - if lookup.Receipt.ExitCode.IsSuccess() { - receipt.Status = 1 - } else { - receipt.Status = 0 - } - - receipt.GasUsed = ethtypes.EthUint64(lookup.Receipt.GasUsed) - - // TODO: handle CumulativeGasUsed - receipt.CumulativeGasUsed = ethtypes.EmptyEthInt - - // TODO: avoid loading the tipset twice (once here, once when we convert the message to a txn) - ts, err := cs.GetTipSetFromKey(ctx, lookup.TipSet) - if err != nil { - return api.EthTxReceipt{}, xerrors.Errorf("failed to lookup tipset %s when constructing the eth txn receipt: %w", lookup.TipSet, err) - } - - baseFee := ts.Blocks()[0].ParentBaseFee - gasOutputs := vm.ComputeGasOutputs(lookup.Receipt.GasUsed, int64(tx.Gas), baseFee, big.Int(tx.MaxFeePerGas), big.Int(tx.MaxPriorityFeePerGas), true) - totalSpent := big.Sum(gasOutputs.BaseFeeBurn, gasOutputs.MinerTip, gasOutputs.OverEstimationBurn) - - effectiveGasPrice := big.Zero() - if lookup.Receipt.GasUsed > 0 { - effectiveGasPrice = big.Div(totalSpent, big.NewInt(lookup.Receipt.GasUsed)) - } - receipt.EffectiveGasPrice = ethtypes.EthBigInt(effectiveGasPrice) - - if receipt.To == nil && lookup.Receipt.ExitCode.IsSuccess() { - // Create and Create2 return the same things. - var ret eam.CreateExternalReturn - if err := ret.UnmarshalCBOR(bytes.NewReader(lookup.Receipt.Return)); err != nil { - return api.EthTxReceipt{}, xerrors.Errorf("failed to parse contract creation result: %w", err) - } - addr := ethtypes.EthAddress(ret.EthAddress) - receipt.ContractAddress = &addr - } - - if len(events) > 0 { - receipt.Logs = make([]ethtypes.EthLog, 0, len(events)) - for i, evt := range events { - l := ethtypes.EthLog{ - Removed: false, - LogIndex: ethtypes.EthUint64(i), - TransactionHash: tx.Hash, - TransactionIndex: transactionIndex, - BlockHash: blockHash, - BlockNumber: blockNumber, - } - - data, topics, ok := ethLogFromEvent(evt.Entries) - if !ok { - // not an eth event. - continue - } - for _, topic := range topics { - ethtypes.EthBloomSet(receipt.LogsBloom, topic[:]) - } - l.Data = data - l.Topics = topics - - addr, err := address.NewIDAddress(uint64(evt.Emitter)) - if err != nil { - return api.EthTxReceipt{}, xerrors.Errorf("failed to create ID address: %w", err) - } - - l.Address, err = lookupEthAddress(ctx, addr, sa) - if err != nil { - return api.EthTxReceipt{}, xerrors.Errorf("failed to resolve Ethereum address: %w", err) - } - - ethtypes.EthBloomSet(receipt.LogsBloom, l.Address[:]) - receipt.Logs = append(receipt.Logs, l) - } - } - - return receipt, nil -} - -func (m *EthTxHashManager) Apply(ctx context.Context, from, to *types.TipSet) error { - for _, blk := range to.Blocks() { - _, smsgs, err := m.StateAPI.Chain.MessagesForBlock(ctx, blk) - if err != nil { - return err - } - - for _, smsg := range smsgs { - if smsg.Signature.Type != crypto.SigTypeDelegated { - continue - } - - hash, err := EthTxHashFromSignedMessage(ctx, smsg, m.StateAPI) - if err != nil { - return err - } - - err = m.TransactionHashLookup.UpsertHash(hash, smsg.Cid()) - if err != nil { - return err - } - } - } - - return nil -} - -type EthTxHashManager struct { - StateAPI StateAPI - TransactionHashLookup *ethhashlookup.EthTxHashLookup -} - -func (m *EthTxHashManager) Revert(ctx context.Context, from, to *types.TipSet) error { - return nil -} - -func (m *EthTxHashManager) PopulateExistingMappings(ctx context.Context, minHeight abi.ChainEpoch) error { - if minHeight < build.UpgradeHyggeHeight { - minHeight = build.UpgradeHyggeHeight - } - - ts := m.StateAPI.Chain.GetHeaviestTipSet() - for ts.Height() > minHeight { - for _, block := range ts.Blocks() { - msgs, err := m.StateAPI.Chain.SecpkMessagesForBlock(ctx, block) - if err != nil { - // If we can't find the messages, we've either imported from snapshot or pruned the store - log.Debug("exiting message mapping population at epoch ", ts.Height()) - return nil - } - - for _, msg := range msgs { - m.ProcessSignedMessage(ctx, msg) - } - } - - var err error - ts, err = m.StateAPI.Chain.GetTipSetFromKey(ctx, ts.Parents()) - if err != nil { - return err - } - } - - return nil -} - -func (m *EthTxHashManager) ProcessSignedMessage(ctx context.Context, msg *types.SignedMessage) { - if msg.Signature.Type != crypto.SigTypeDelegated { - return - } - - ethTx, err := newEthTxFromSignedMessage(ctx, msg, m.StateAPI) - if err != nil { - log.Errorf("error converting filecoin message to eth tx: %s", err) - return - } - - err = m.TransactionHashLookup.UpsertHash(ethTx.Hash, msg.Cid()) - if err != nil { - log.Errorf("error inserting tx mapping to db: %s", err) - return - } -} - -func WaitForMpoolUpdates(ctx context.Context, ch <-chan api.MpoolUpdate, manager *EthTxHashManager) { - for { - select { - case <-ctx.Done(): - return - case u := <-ch: - if u.Type != api.MpoolAdd { - continue - } - - manager.ProcessSignedMessage(ctx, u.Message) - } - } -} - -func EthTxHashGC(ctx context.Context, retentionDays int, manager *EthTxHashManager) { - if retentionDays == 0 { - return - } - - gcPeriod := 1 * time.Hour - for { - entriesDeleted, err := manager.TransactionHashLookup.DeleteEntriesOlderThan(retentionDays) - if err != nil { - log.Errorf("error garbage collecting eth transaction hash database: %s", err) - } - log.Info("garbage collection run on eth transaction hash lookup database. %d entries deleted", entriesDeleted) - time.Sleep(gcPeriod) - } -} - -func parseEthTopics(topics ethtypes.EthTopicSpec) (map[string][][]byte, error) { - keys := map[string][][]byte{} - for idx, vals := range topics { - if len(vals) == 0 { - continue - } - // Ethereum topics are emitted using `LOG{0..4}` opcodes resulting in topics1..4 - key := fmt.Sprintf("t%d", idx+1) - for _, v := range vals { - v := v // copy the ethhash to avoid repeatedly referencing the same one. - keys[key] = append(keys[key], v[:]) - } - } - return keys, nil -} - -const errorFunctionSelector = "\x08\xc3\x79\xa0" // Error(string) -const panicFunctionSelector = "\x4e\x48\x7b\x71" // Panic(uint256) -// Eth ABI (solidity) panic codes. -var panicErrorCodes map[uint64]string = map[uint64]string{ - 0x00: "Panic()", - 0x01: "Assert()", - 0x11: "ArithmeticOverflow()", - 0x12: "DivideByZero()", - 0x21: "InvalidEnumVariant()", - 0x22: "InvalidStorageArray()", - 0x31: "PopEmptyArray()", - 0x32: "ArrayIndexOutOfBounds()", - 0x41: "OutOfMemory()", - 0x51: "CalledUninitializedFunction()", -} - -// Parse an ABI encoded revert reason. This reason should be encoded as if it were the parameters to -// an `Error(string)` function call. -// -// See https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require -func parseEthRevert(ret []byte) string { - if len(ret) == 0 { - return "none" - } - var cbytes abi.CborBytes - if err := cbytes.UnmarshalCBOR(bytes.NewReader(ret)); err != nil { - return "ERROR: revert reason is not cbor encoded bytes" - } - if len(cbytes) == 0 { - return "none" - } - // If it's not long enough to contain an ABI encoded response, return immediately. - if len(cbytes) < 4+32 { - return ethtypes.EthBytes(cbytes).String() - } - switch string(cbytes[:4]) { - case panicFunctionSelector: - cbytes := cbytes[4 : 4+32] - // Read the and check the code. - code, err := ethtypes.EthUint64FromBytes(cbytes) - if err != nil { - // If it's too big, just return the raw value. - codeInt := big.PositiveFromUnsignedBytes(cbytes) - return fmt.Sprintf("Panic(%s)", ethtypes.EthBigInt(codeInt).String()) - } - if s, ok := panicErrorCodes[uint64(code)]; ok { - return s - } - return fmt.Sprintf("Panic(0x%x)", code) - case errorFunctionSelector: - cbytes := cbytes[4:] - cbytesLen := ethtypes.EthUint64(len(cbytes)) - // Read the and check the offset. - offset, err := ethtypes.EthUint64FromBytes(cbytes[:32]) - if err != nil { - break - } - if cbytesLen < offset { - break - } - - // Read and check the length. - if cbytesLen-offset < 32 { - break - } - start := offset + 32 - length, err := ethtypes.EthUint64FromBytes(cbytes[offset : offset+32]) - if err != nil { - break - } - if cbytesLen-start < length { - break - } - // Slice the error message. - return fmt.Sprintf("Error(%s)", cbytes[start:start+length]) - } - return ethtypes.EthBytes(cbytes).String() -} - -func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRewardSorter) ([]ethtypes.EthBigInt, int64) { - var gasUsedTotal int64 - for _, tx := range txGasRewards { - gasUsedTotal += tx.gasUsed - } - - rewards := make([]ethtypes.EthBigInt, len(rewardPercentiles)) - for i := range rewards { - rewards[i] = ethtypes.EthBigInt(types.NewInt(MinGasPremium)) - } - - if len(txGasRewards) == 0 { - return rewards, gasUsedTotal - } - - sort.Stable(txGasRewards) - - var idx int - var sum int64 - for i, percentile := range rewardPercentiles { - threshold := int64(float64(gasUsedTotal) * percentile / 100) - for sum < threshold && idx < len(txGasRewards)-1 { - sum += txGasRewards[idx].gasUsed - idx++ - } - rewards[i] = ethtypes.EthBigInt(txGasRewards[idx].premium) + rewards[i] = ethtypes.EthBigInt(txGasRewards[idx].premium) } return rewards, gasUsedTotal } -func getSignedMessage(ctx context.Context, cs *store.ChainStore, msgCid cid.Cid) (*types.SignedMessage, error) { - smsg, err := cs.GetSignedMessage(ctx, msgCid) - if err != nil { - // We couldn't find the signed message, it might be a BLS message, so search for a regular message. - msg, err := cs.GetMessage(ctx, msgCid) - if err != nil { - return nil, xerrors.Errorf("failed to find msg %s: %w", msgCid, err) - } - smsg = &types.SignedMessage{ - Message: *msg, - Signature: crypto.Signature{ - Type: crypto.SigTypeBLS, - }, - } - } - - return smsg, nil -} - type gasRewardTuple struct { gasUsed int64 premium abi.TokenAmount diff --git a/node/impl/full/eth_event.go b/node/impl/full/eth_event.go new file mode 100644 index 00000000000..69021e08aed --- /dev/null +++ b/node/impl/full/eth_event.go @@ -0,0 +1,382 @@ +package full + +import ( + "context" + "encoding/json" + "sync" + + "github.com/google/uuid" + "github.com/ipfs/go-cid" + "github.com/zyedidia/generic/queue" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-jsonrpc" + + "github.com/filecoin-project/lotus/chain/events/filter" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +type filterEventCollector interface { + TakeCollectedEvents(context.Context) []*filter.CollectedEvent +} + +type filterMessageCollector interface { + TakeCollectedMessages(context.Context) []*types.SignedMessage +} + +type filterTipSetCollector interface { + TakeCollectedTipSets(context.Context) []types.TipSetKey +} + +func ethLogFromEvent(entries []types.EventEntry) (data []byte, topics []ethtypes.EthHash, ok bool) { + var ( + topicsFound [4]bool + topicsFoundCount int + dataFound bool + ) + // Topics must be non-nil, even if empty. So we might as well pre-allocate for 4 (the max). + topics = make([]ethtypes.EthHash, 0, 4) + for _, entry := range entries { + // Drop events with non-raw topics to avoid mistakes. + if entry.Codec != cid.Raw { + log.Warnw("did not expect an event entry with a non-raw codec", "codec", entry.Codec, "key", entry.Key) + return nil, nil, false + } + // Check if the key is t1..t4 + if len(entry.Key) == 2 && "t1" <= entry.Key && entry.Key <= "t4" { + // '1' - '1' == 0, etc. + idx := int(entry.Key[1] - '1') + + // Drop events with mis-sized topics. + if len(entry.Value) != 32 { + log.Warnw("got an EVM event topic with an invalid size", "key", entry.Key, "size", len(entry.Value)) + return nil, nil, false + } + + // Drop events with duplicate topics. + if topicsFound[idx] { + log.Warnw("got a duplicate EVM event topic", "key", entry.Key) + return nil, nil, false + } + topicsFound[idx] = true + topicsFoundCount++ + + // Extend the topics array + for len(topics) <= idx { + topics = append(topics, ethtypes.EthHash{}) + } + copy(topics[idx][:], entry.Value) + } else if entry.Key == "d" { + // Drop events with duplicate data fields. + if dataFound { + log.Warnw("got duplicate EVM event data") + return nil, nil, false + } + + dataFound = true + data = entry.Value + } else { + // Skip entries we don't understand (makes it easier to extend things). + // But we warn for now because we don't expect them. + log.Warnw("unexpected event entry", "key", entry.Key) + } + + } + + // Drop events with skipped topics. + if len(topics) != topicsFoundCount { + log.Warnw("EVM event topic length mismatch", "expected", len(topics), "actual", topicsFoundCount) + return nil, nil, false + } + return data, topics, true +} + +func ethFilterResultFromEvents(evs []*filter.CollectedEvent, sa StateAPI) (*ethtypes.EthFilterResult, error) { + res := ðtypes.EthFilterResult{} + for _, ev := range evs { + log := ethtypes.EthLog{ + Removed: ev.Reverted, + LogIndex: ethtypes.EthUint64(ev.EventIdx), + TransactionIndex: ethtypes.EthUint64(ev.MsgIdx), + BlockNumber: ethtypes.EthUint64(ev.Height), + } + var ( + err error + ok bool + ) + + log.Data, log.Topics, ok = ethLogFromEvent(ev.Entries) + if !ok { + continue + } + + log.Address, err = ethtypes.EthAddressFromFilecoinAddress(ev.EmitterAddr) + if err != nil { + return nil, err + } + + log.TransactionHash, err = ethTxHashFromMessageCid(context.TODO(), ev.MsgCid, sa) + if err != nil { + return nil, err + } + c, err := ev.TipSetKey.Cid() + if err != nil { + return nil, err + } + log.BlockHash, err = ethtypes.EthHashFromCid(c) + if err != nil { + return nil, err + } + + res.Results = append(res.Results, log) + } + + return res, nil +} + +func ethFilterResultFromTipSets(tsks []types.TipSetKey) (*ethtypes.EthFilterResult, error) { + res := ðtypes.EthFilterResult{} + + for _, tsk := range tsks { + c, err := tsk.Cid() + if err != nil { + return nil, err + } + hash, err := ethtypes.EthHashFromCid(c) + if err != nil { + return nil, err + } + + res.Results = append(res.Results, hash) + } + + return res, nil +} + +func ethFilterResultFromMessages(cs []*types.SignedMessage, sa StateAPI) (*ethtypes.EthFilterResult, error) { + res := ðtypes.EthFilterResult{} + + for _, c := range cs { + hash, err := ethTxHashFromSignedMessage(context.TODO(), c, sa) + if err != nil { + return nil, err + } + + res.Results = append(res.Results, hash) + } + + return res, nil +} + +type EthSubscriptionManager struct { + Chain *store.ChainStore + StateAPI StateAPI + ChainAPI ChainAPI + mu sync.Mutex + subs map[ethtypes.EthSubscriptionID]*ethSubscription +} + +func (e *EthSubscriptionManager) StartSubscription(ctx context.Context, out ethSubscriptionCallback, dropFilter func(context.Context, filter.Filter) error) (*ethSubscription, error) { // nolint + rawid, err := uuid.NewRandom() + if err != nil { + return nil, xerrors.Errorf("new uuid: %w", err) + } + id := ethtypes.EthSubscriptionID{} + copy(id[:], rawid[:]) // uuid is 16 bytes + + ctx, quit := context.WithCancel(ctx) + + sub := ðSubscription{ + Chain: e.Chain, + StateAPI: e.StateAPI, + ChainAPI: e.ChainAPI, + uninstallFilter: dropFilter, + id: id, + in: make(chan interface{}, 200), + out: out, + quit: quit, + + toSend: queue.New[[]byte](), + sendCond: make(chan struct{}, 1), + } + + e.mu.Lock() + if e.subs == nil { + e.subs = make(map[ethtypes.EthSubscriptionID]*ethSubscription) + } + e.subs[sub.id] = sub + e.mu.Unlock() + + go sub.start(ctx) + go sub.startOut(ctx) + + return sub, nil +} + +func (e *EthSubscriptionManager) StopSubscription(ctx context.Context, id ethtypes.EthSubscriptionID) error { + e.mu.Lock() + defer e.mu.Unlock() + + sub, ok := e.subs[id] + if !ok { + return xerrors.Errorf("subscription not found") + } + sub.stop() + delete(e.subs, id) + + return nil +} + +type ethSubscriptionCallback func(context.Context, jsonrpc.RawParams) error + +const maxSendQueue = 20000 + +type ethSubscription struct { + Chain *store.ChainStore + StateAPI StateAPI + ChainAPI ChainAPI + uninstallFilter func(context.Context, filter.Filter) error + id ethtypes.EthSubscriptionID + in chan interface{} + out ethSubscriptionCallback + + mu sync.Mutex + filters []filter.Filter + quit func() + + sendLk sync.Mutex + sendQueueLen int + toSend *queue.Queue[[]byte] + sendCond chan struct{} +} + +func (e *ethSubscription) addFilter(ctx context.Context, f filter.Filter) { + e.mu.Lock() + defer e.mu.Unlock() + + f.SetSubChannel(e.in) + e.filters = append(e.filters, f) +} + +// sendOut processes the final subscription queue. It's here in case the subscriber +// is slow, and we need to buffer the messages. +func (e *ethSubscription) startOut(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case <-e.sendCond: + e.sendLk.Lock() + + for !e.toSend.Empty() { + front := e.toSend.Dequeue() + e.sendQueueLen-- + + e.sendLk.Unlock() + + if err := e.out(ctx, front); err != nil { + log.Warnw("error sending subscription response, killing subscription", "sub", e.id, "error", err) + e.stop() + return + } + + e.sendLk.Lock() + } + + e.sendLk.Unlock() + } + } +} + +func (e *ethSubscription) send(ctx context.Context, v interface{}) { + resp := ethtypes.EthSubscriptionResponse{ + SubscriptionID: e.id, + Result: v, + } + + outParam, err := json.Marshal(resp) + if err != nil { + log.Warnw("marshaling subscription response", "sub", e.id, "error", err) + return + } + + e.sendLk.Lock() + defer e.sendLk.Unlock() + + e.toSend.Enqueue(outParam) + + e.sendQueueLen++ + if e.sendQueueLen > maxSendQueue { + log.Warnw("subscription send queue full, killing subscription", "sub", e.id) + e.stop() + return + } + + select { + case e.sendCond <- struct{}{}: + default: // already signalled, and we're holding the lock so we know that the event will be processed + } +} + +func (e *ethSubscription) start(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case v := <-e.in: + switch vt := v.(type) { + case *filter.CollectedEvent: + evs, err := ethFilterResultFromEvents([]*filter.CollectedEvent{vt}, e.StateAPI) + if err != nil { + continue + } + + for _, r := range evs.Results { + e.send(ctx, r) + } + case *types.TipSet: + ev, err := newEthBlockFromFilecoinTipSet(ctx, vt, true, e.Chain, e.StateAPI) + if err != nil { + break + } + + e.send(ctx, ev) + case *types.SignedMessage: // mpool txid + evs, err := ethFilterResultFromMessages([]*types.SignedMessage{vt}, e.StateAPI) + if err != nil { + continue + } + + for _, r := range evs.Results { + e.send(ctx, r) + } + default: + log.Warnf("unexpected subscription value type: %T", vt) + } + } + } +} + +func (e *ethSubscription) stop() { + e.mu.Lock() + if e.quit == nil { + e.mu.Unlock() + return + } + + if e.quit != nil { + e.quit() + e.quit = nil + e.mu.Unlock() + + for _, f := range e.filters { + // note: the context in actually unused in uninstallFilter + if err := e.uninstallFilter(context.TODO(), f); err != nil { + // this will leave the filter a zombie, collecting events up to the maximum allowed + log.Warnf("failed to remove filter when unsubscribing: %v", err) + } + } + } +} diff --git a/node/impl/full/trace.go b/node/impl/full/eth_trace.go similarity index 68% rename from node/impl/full/trace.go rename to node/impl/full/eth_trace.go index 02ffdd46b9a..3e3b48c904f 100644 --- a/node/impl/full/trace.go +++ b/node/impl/full/eth_trace.go @@ -2,49 +2,22 @@ package full import ( "bytes" - "context" "encoding/binary" "encoding/hex" "fmt" "io" "github.com/ipfs/go-cid" - "go.uber.org/fx" - "golang.org/x/xerrors" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/exitcode" - "github.com/filecoin-project/lotus/api" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" - "github.com/filecoin-project/lotus/chain/stmgr" - "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" ) -type EthTraceAPI interface { - TraceBlock(ctx context.Context, blkNum string) (interface{}, error) - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) -} - -var ( - _ EthTraceAPI = *new(api.FullNode) -) - -type EthTrace struct { - fx.In - - Chain *store.ChainStore - StateManager *stmgr.StateManager - - ChainAPI - EthModuleAPI -} - -var _ EthTraceAPI = (*EthTrace)(nil) - type Trace struct { Action Action `json:"action"` Result Result `json:"result"` @@ -93,168 +66,6 @@ type Result struct { Output string `json:"output"` } -func (e *EthTrace) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { - ts, err := e.getTipsetByBlockNr(ctx, blkNum, false) - if err != nil { - return nil, err - } - - _, trace, err := e.StateManager.ExecutionTrace(ctx, ts) - if err != nil { - return nil, xerrors.Errorf("failed to compute base state: %w", err) - } - - tsParent, err := e.ChainAPI.ChainGetTipSetByHeight(ctx, ts.Height()+1, e.Chain.GetHeaviestTipSet().Key()) - if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height()+1) - } - - msgs, err := e.ChainGetParentMessages(ctx, tsParent.Blocks()[0].Cid()) - if err != nil { - return nil, err - } - - cid, err := ts.Key().Cid() - if err != nil { - return nil, err - } - - blkHash, err := ethtypes.EthHashFromCid(cid) - if err != nil { - return nil, err - } - - allTraces := make([]*TraceBlock, 0, len(trace)) - for _, ir := range trace { - // ignore messages from f00 - if ir.Msg.From.String() == "f00" { - continue - } - - idx := -1 - for msgIdx, msg := range msgs { - if ir.Msg.From == msg.Message.From { - idx = msgIdx - break - } - } - if idx == -1 { - log.Warnf("cannot resolve message index for cid: %s", ir.MsgCid) - continue - } - - txHash, err := e.EthGetTransactionHashByCid(ctx, ir.MsgCid) - if err != nil { - return nil, err - } - if txHash == nil { - log.Warnf("cannot find transaction hash for cid %s", ir.MsgCid) - continue - } - - traces := []*Trace{} - err = buildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) - if err != nil { - return nil, xerrors.Errorf("failed when building traces: %w", err) - } - - traceBlocks := make([]*TraceBlock, 0, len(trace)) - for _, trace := range traces { - traceBlocks = append(traceBlocks, &TraceBlock{ - Trace: trace, - BlockHash: blkHash, - BlockNumber: int64(ts.Height()), - TransactionHash: *txHash, - TransactionPosition: idx, - }) - } - - allTraces = append(allTraces, traceBlocks...) - } - - return allTraces, nil -} - -func (e *EthTrace) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { - if len(traceTypes) != 1 || traceTypes[0] != "trace" { - return nil, fmt.Errorf("only 'trace' is supported") - } - - ts, err := e.getTipsetByBlockNr(ctx, blkNum, false) - if err != nil { - return nil, err - } - - _, trace, err := e.StateManager.ExecutionTrace(ctx, ts) - if err != nil { - return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) - } - - allTraces := make([]*TraceReplayBlockTransaction, 0, len(trace)) - for _, ir := range trace { - // ignore messages from f00 - if ir.Msg.From.String() == "f00" { - continue - } - - txHash, err := e.EthGetTransactionHashByCid(ctx, ir.MsgCid) - if err != nil { - return nil, err - } - if txHash == nil { - log.Warnf("cannot find transaction hash for cid %s", ir.MsgCid) - continue - } - - t := TraceReplayBlockTransaction{ - Output: hex.EncodeToString(ir.MsgRct.Return), - TransactionHash: *txHash, - StateDiff: nil, - VmTrace: nil, - } - - err = buildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) - if err != nil { - return nil, xerrors.Errorf("failed when building traces: %w", err) - } - - allTraces = append(allTraces, &t) - } - - return allTraces, nil -} - -func writePadded[T any](w io.Writer, data T, size int) error { - tmp := &bytes.Buffer{} - - // first write data to tmp buffer to get the size - err := binary.Write(tmp, binary.BigEndian, data) - if err != nil { - return fmt.Errorf("writePadded: failed writing tmp data to buffer: %w", err) - } - - if tmp.Len() > size { - return fmt.Errorf("writePadded: data is larger than size") - } - - // write tailing zeros to pad up to size - cnt := size - tmp.Len() - for i := 0; i < cnt; i++ { - err = binary.Write(w, binary.BigEndian, uint8(0)) - if err != nil { - return fmt.Errorf("writePadded: failed writing tailing zeros to buffer: %w", err) - } - } - - // finally write the actual value - err = binary.Write(w, binary.BigEndian, tmp.Bytes()) - if err != nil { - return fmt.Errorf("writePadded: failed writing data to buffer: %w", err) - } - - return nil -} - // buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.ExecutionTrace, height int64) error { trace := &Trace{ @@ -416,6 +227,37 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution return nil } +func writePadded[T any](w io.Writer, data T, size int) error { + tmp := &bytes.Buffer{} + + // first write data to tmp buffer to get the size + err := binary.Write(tmp, binary.BigEndian, data) + if err != nil { + return fmt.Errorf("writePadded: failed writing tmp data to buffer: %w", err) + } + + if tmp.Len() > size { + return fmt.Errorf("writePadded: data is larger than size") + } + + // write tailing zeros to pad up to size + cnt := size - tmp.Len() + for i := 0; i < cnt; i++ { + err = binary.Write(w, binary.BigEndian, uint8(0)) + if err != nil { + return fmt.Errorf("writePadded: failed writing tailing zeros to buffer: %w", err) + } + } + + // finally write the actual value + err = binary.Write(w, binary.BigEndian, tmp.Bytes()) + if err != nil { + return fmt.Errorf("writePadded: failed writing data to buffer: %w", err) + } + + return nil +} + func handleFilecoinMethodInput(method abi.MethodNum, codec uint64, params []byte) ([]byte, error) { NATIVE_METHOD_SELECTOR := []byte{0x86, 0x8e, 0x10, 0xc4} EVM_WORD_SIZE := 32 @@ -477,39 +319,3 @@ func handleFilecoinMethodOutput(exitCode exitcode.ExitCode, codec uint64, data [ return w.Bytes(), nil } - -// TODO: refactor this to be shared code -func (e *EthTrace) getTipsetByBlockNr(ctx context.Context, blkParam string, strict bool) (*types.TipSet, error) { - if blkParam == "earliest" { - return nil, fmt.Errorf("block param \"earliest\" is not supported") - } - - head := e.Chain.GetHeaviestTipSet() - switch blkParam { - case "pending": - return head, nil - case "latest": - parent, err := e.Chain.GetTipSetFromKey(ctx, head.Parents()) - if err != nil { - return nil, fmt.Errorf("cannot get parent tipset") - } - return parent, nil - default: - var num ethtypes.EthUint64 - err := num.UnmarshalJSON([]byte(`"` + blkParam + `"`)) - if err != nil { - return nil, fmt.Errorf("cannot parse block number: %v", err) - } - if abi.ChainEpoch(num) > head.Height()-1 { - return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") - } - ts, err := e.ChainAPI.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(num), head.Key()) - if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", num) - } - if strict && ts.Height() != abi.ChainEpoch(num) { - return nil, ErrNullRound - } - return ts, nil - } -} diff --git a/node/impl/full/eth_utils.go b/node/impl/full/eth_utils.go new file mode 100644 index 00000000000..ab17d13b2c0 --- /dev/null +++ b/node/impl/full/eth_utils.go @@ -0,0 +1,689 @@ +package full + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/ipfs/go-cid" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + builtintypes "github.com/filecoin-project/go-state-types/builtin" + "github.com/filecoin-project/go-state-types/builtin/v10/eam" + "github.com/filecoin-project/go-state-types/crypto" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/actors" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" + "github.com/filecoin-project/lotus/chain/vm" +) + +func getTipsetByBlockNr(ctx context.Context, chain *store.ChainStore, blkParam string, strict bool) (*types.TipSet, error) { + if blkParam == "earliest" { + return nil, fmt.Errorf("block param \"earliest\" is not supported") + } + + head := chain.GetHeaviestTipSet() + switch blkParam { + case "pending": + return head, nil + case "latest": + parent, err := chain.GetTipSetFromKey(ctx, head.Parents()) + if err != nil { + return nil, fmt.Errorf("cannot get parent tipset") + } + return parent, nil + default: + var num ethtypes.EthUint64 + err := num.UnmarshalJSON([]byte(`"` + blkParam + `"`)) + if err != nil { + return nil, fmt.Errorf("cannot parse block number: %v", err) + } + if abi.ChainEpoch(num) > head.Height()-1 { + return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") + } + ts, err := chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), head, true) + if err != nil { + return nil, fmt.Errorf("cannot get tipset at height: %v", num) + } + if strict && ts.Height() != abi.ChainEpoch(num) { + return nil, ErrNullRound + } + return ts, nil + } +} + +func getTipsetByEthBlockNumberOrHash(ctx context.Context, chain *store.ChainStore, blkParam ethtypes.EthBlockNumberOrHash) (*types.TipSet, error) { + head := chain.GetHeaviestTipSet() + + predefined := blkParam.PredefinedBlock + if predefined != nil { + if *predefined == "earliest" { + return nil, fmt.Errorf("block param \"earliest\" is not supported") + } else if *predefined == "pending" { + return head, nil + } else if *predefined == "latest" { + parent, err := chain.GetTipSetFromKey(ctx, head.Parents()) + if err != nil { + return nil, fmt.Errorf("cannot get parent tipset") + } + return parent, nil + } else { + return nil, fmt.Errorf("unknown predefined block %s", *predefined) + } + } + + if blkParam.BlockNumber != nil { + height := abi.ChainEpoch(*blkParam.BlockNumber) + if height > head.Height()-1 { + return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") + } + ts, err := chain.GetTipsetByHeight(ctx, height, head, true) + if err != nil { + return nil, fmt.Errorf("cannot get tipset at height: %v", height) + } + return ts, nil + } + + if blkParam.BlockHash != nil { + ts, err := chain.GetTipSetByCid(ctx, blkParam.BlockHash.ToCid()) + if err != nil { + return nil, fmt.Errorf("cannot get tipset by hash: %v", err) + } + + // verify that the tipset is in the canonical chain + if blkParam.RequireCanonical { + // walk up the current chain (our head) until we reach ts.Height() + walkTs, err := chain.GetTipsetByHeight(ctx, ts.Height(), head, true) + if err != nil { + return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height()) + } + + // verify that it equals the expected tipset + if !walkTs.Equals(ts) { + return nil, fmt.Errorf("tipset is not canonical") + } + } + + return ts, nil + } + + return nil, errors.New("invalid block param") +} + +func ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types.Message, error) { + var from address.Address + if tx.From == nil || *tx.From == (ethtypes.EthAddress{}) { + // Send from the filecoin "system" address. + var err error + from, err = (ethtypes.EthAddress{}).ToFilecoinAddress() + if err != nil { + return nil, fmt.Errorf("failed to construct the ethereum system address: %w", err) + } + } else { + // The from address must be translatable to an f4 address. + var err error + from, err = tx.From.ToFilecoinAddress() + if err != nil { + return nil, fmt.Errorf("failed to translate sender address (%s): %w", tx.From.String(), err) + } + if p := from.Protocol(); p != address.Delegated { + return nil, fmt.Errorf("expected a class 4 address, got: %d: %w", p, err) + } + } + + var params []byte + if len(tx.Data) > 0 { + initcode := abi.CborBytes(tx.Data) + params2, err := actors.SerializeParams(&initcode) + if err != nil { + return nil, fmt.Errorf("failed to serialize params: %w", err) + } + params = params2 + } + + var to address.Address + var method abi.MethodNum + if tx.To == nil { + // this is a contract creation + to = builtintypes.EthereumAddressManagerActorAddr + method = builtintypes.MethodsEAM.CreateExternal + } else { + addr, err := tx.To.ToFilecoinAddress() + if err != nil { + return nil, xerrors.Errorf("cannot get Filecoin address: %w", err) + } + to = addr + method = builtintypes.MethodsEVM.InvokeContract + } + + return &types.Message{ + From: from, + To: to, + Value: big.Int(tx.Value), + Method: method, + Params: params, + GasLimit: build.BlockGasLimit, + GasFeeCap: big.Zero(), + GasPremium: big.Zero(), + }, nil +} + +func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTxInfo bool, cs *store.ChainStore, sa StateAPI) (ethtypes.EthBlock, error) { + parentKeyCid, err := ts.Parents().Cid() + if err != nil { + return ethtypes.EthBlock{}, err + } + parentBlkHash, err := ethtypes.EthHashFromCid(parentKeyCid) + if err != nil { + return ethtypes.EthBlock{}, err + } + + bn := ethtypes.EthUint64(ts.Height()) + + blkCid, err := ts.Key().Cid() + if err != nil { + return ethtypes.EthBlock{}, err + } + blkHash, err := ethtypes.EthHashFromCid(blkCid) + if err != nil { + return ethtypes.EthBlock{}, err + } + + msgs, rcpts, err := messagesAndReceipts(ctx, ts, cs, sa) + if err != nil { + return ethtypes.EthBlock{}, xerrors.Errorf("failed to retrieve messages and receipts: %w", err) + } + + block := ethtypes.NewEthBlock(len(msgs) > 0) + + gasUsed := int64(0) + for i, msg := range msgs { + rcpt := rcpts[i] + ti := ethtypes.EthUint64(i) + gasUsed += rcpt.GasUsed + var smsg *types.SignedMessage + switch msg := msg.(type) { + case *types.SignedMessage: + smsg = msg + case *types.Message: + smsg = &types.SignedMessage{ + Message: *msg, + Signature: crypto.Signature{ + Type: crypto.SigTypeBLS, + }, + } + default: + return ethtypes.EthBlock{}, xerrors.Errorf("failed to get signed msg %s: %w", msg.Cid(), err) + } + tx, err := newEthTxFromSignedMessage(ctx, smsg, sa) + if err != nil { + return ethtypes.EthBlock{}, xerrors.Errorf("failed to convert msg to ethTx: %w", err) + } + + tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId) + tx.BlockHash = &blkHash + tx.BlockNumber = &bn + tx.TransactionIndex = &ti + + if fullTxInfo { + block.Transactions = append(block.Transactions, tx) + } else { + block.Transactions = append(block.Transactions, tx.Hash.String()) + } + } + + block.Hash = blkHash + block.Number = bn + block.ParentHash = parentBlkHash + block.Timestamp = ethtypes.EthUint64(ts.Blocks()[0].Timestamp) + block.BaseFeePerGas = ethtypes.EthBigInt{Int: ts.Blocks()[0].ParentBaseFee.Int} + block.GasUsed = ethtypes.EthUint64(gasUsed) + return block, nil +} + +func messagesAndReceipts(ctx context.Context, ts *types.TipSet, cs *store.ChainStore, sa StateAPI) ([]types.ChainMsg, []types.MessageReceipt, error) { + msgs, err := cs.MessagesForTipset(ctx, ts) + if err != nil { + return nil, nil, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) + } + + _, rcptRoot, err := sa.StateManager.TipSetState(ctx, ts) + if err != nil { + return nil, nil, xerrors.Errorf("failed to compute state: %w", err) + } + + rcpts, err := cs.ReadReceipts(ctx, rcptRoot) + if err != nil { + return nil, nil, xerrors.Errorf("error loading receipts for tipset: %v: %w", ts, err) + } + + if len(msgs) != len(rcpts) { + return nil, nil, xerrors.Errorf("receipts and message array lengths didn't match for tipset: %v: %w", ts, err) + } + + return msgs, rcpts, nil +} + +const errorFunctionSelector = "\x08\xc3\x79\xa0" // Error(string) +const panicFunctionSelector = "\x4e\x48\x7b\x71" // Panic(uint256) +// Eth ABI (solidity) panic codes. +var panicErrorCodes map[uint64]string = map[uint64]string{ + 0x00: "Panic()", + 0x01: "Assert()", + 0x11: "ArithmeticOverflow()", + 0x12: "DivideByZero()", + 0x21: "InvalidEnumVariant()", + 0x22: "InvalidStorageArray()", + 0x31: "PopEmptyArray()", + 0x32: "ArrayIndexOutOfBounds()", + 0x41: "OutOfMemory()", + 0x51: "CalledUninitializedFunction()", +} + +// Parse an ABI encoded revert reason. This reason should be encoded as if it were the parameters to +// an `Error(string)` function call. +// +// See https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require +func parseEthRevert(ret []byte) string { + if len(ret) == 0 { + return "none" + } + var cbytes abi.CborBytes + if err := cbytes.UnmarshalCBOR(bytes.NewReader(ret)); err != nil { + return "ERROR: revert reason is not cbor encoded bytes" + } + if len(cbytes) == 0 { + return "none" + } + // If it's not long enough to contain an ABI encoded response, return immediately. + if len(cbytes) < 4+32 { + return ethtypes.EthBytes(cbytes).String() + } + switch string(cbytes[:4]) { + case panicFunctionSelector: + cbytes := cbytes[4 : 4+32] + // Read the and check the code. + code, err := ethtypes.EthUint64FromBytes(cbytes) + if err != nil { + // If it's too big, just return the raw value. + codeInt := big.PositiveFromUnsignedBytes(cbytes) + return fmt.Sprintf("Panic(%s)", ethtypes.EthBigInt(codeInt).String()) + } + if s, ok := panicErrorCodes[uint64(code)]; ok { + return s + } + return fmt.Sprintf("Panic(0x%x)", code) + case errorFunctionSelector: + cbytes := cbytes[4:] + cbytesLen := ethtypes.EthUint64(len(cbytes)) + // Read the and check the offset. + offset, err := ethtypes.EthUint64FromBytes(cbytes[:32]) + if err != nil { + break + } + if cbytesLen < offset { + break + } + + // Read and check the length. + if cbytesLen-offset < 32 { + break + } + start := offset + 32 + length, err := ethtypes.EthUint64FromBytes(cbytes[offset : offset+32]) + if err != nil { + break + } + if cbytesLen-start < length { + break + } + // Slice the error message. + return fmt.Sprintf("Error(%s)", cbytes[start:start+length]) + } + return ethtypes.EthBytes(cbytes).String() +} + +// lookupEthAddress makes its best effort at finding the Ethereum address for a +// Filecoin address. It does the following: +// +// 1. If the supplied address is an f410 address, we return its payload as the EthAddress. +// 2. Otherwise (f0, f1, f2, f3), we look up the actor on the state tree. If it has a delegated address, we return it if it's f410 address. +// 3. Otherwise, we fall back to returning a masked ID Ethereum address. If the supplied address is an f0 address, we +// use that ID to form the masked ID address. +// 4. Otherwise, we fetch the actor's ID from the state tree and form the masked ID with it. +func lookupEthAddress(ctx context.Context, addr address.Address, sa StateAPI) (ethtypes.EthAddress, error) { + // BLOCK A: We are trying to get an actual Ethereum address from an f410 address. + // Attempt to convert directly, if it's an f4 address. + ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(addr) + if err == nil && !ethAddr.IsMaskedID() { + return ethAddr, nil + } + + // Lookup on the target actor and try to get an f410 address. + if actor, err := sa.StateGetActor(ctx, addr, types.EmptyTSK); err != nil { + return ethtypes.EthAddress{}, err + } else if actor.Address != nil { + if ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(*actor.Address); err == nil && !ethAddr.IsMaskedID() { + return ethAddr, nil + } + } + + // BLOCK B: We gave up on getting an actual Ethereum address and are falling back to a Masked ID address. + // Check if we already have an ID addr, and use it if possible. + if err == nil && ethAddr.IsMaskedID() { + return ethAddr, nil + } + + // Otherwise, resolve the ID addr. + idAddr, err := sa.StateLookupID(ctx, addr, types.EmptyTSK) + if err != nil { + return ethtypes.EthAddress{}, err + } + return ethtypes.EthAddressFromFilecoinAddress(idAddr) +} + +func parseEthTopics(topics ethtypes.EthTopicSpec) (map[string][][]byte, error) { + keys := map[string][][]byte{} + for idx, vals := range topics { + if len(vals) == 0 { + continue + } + // Ethereum topics are emitted using `LOG{0..4}` opcodes resulting in topics1..4 + key := fmt.Sprintf("t%d", idx+1) + for _, v := range vals { + v := v // copy the ethhash to avoid repeatedly referencing the same one. + keys[key] = append(keys[key], v[:]) + } + } + return keys, nil +} + +func ethTxHashFromMessageCid(ctx context.Context, c cid.Cid, sa StateAPI) (ethtypes.EthHash, error) { + smsg, err := sa.Chain.GetSignedMessage(ctx, c) + if err == nil { + // This is an Eth Tx, Secp message, Or BLS message in the mpool + return ethTxHashFromSignedMessage(ctx, smsg, sa) + } + + _, err = sa.Chain.GetMessage(ctx, c) + if err == nil { + // This is a BLS message + return ethtypes.EthHashFromCid(c) + } + + return ethtypes.EmptyEthHash, nil +} + +func ethTxHashFromSignedMessage(ctx context.Context, smsg *types.SignedMessage, sa StateAPI) (ethtypes.EthHash, error) { + if smsg.Signature.Type == crypto.SigTypeDelegated { + ethTx, err := newEthTxFromSignedMessage(ctx, smsg, sa) + if err != nil { + return ethtypes.EmptyEthHash, err + } + return ethTx.Hash, nil + } else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { + return ethtypes.EthHashFromCid(smsg.Cid()) + } else { // BLS message + return ethtypes.EthHashFromCid(smsg.Message.Cid()) + } +} + +func newEthTxFromSignedMessage(ctx context.Context, smsg *types.SignedMessage, sa StateAPI) (ethtypes.EthTx, error) { + var tx ethtypes.EthTx + var err error + + // This is an eth tx + if smsg.Signature.Type == crypto.SigTypeDelegated { + tx, err = ethtypes.EthTxFromSignedEthMessage(smsg) + if err != nil { + return ethtypes.EthTx{}, xerrors.Errorf("failed to convert from signed message: %w", err) + } + + tx.Hash, err = tx.TxHash() + if err != nil { + return ethtypes.EthTx{}, xerrors.Errorf("failed to calculate hash for ethTx: %w", err) + } + + fromAddr, err := lookupEthAddress(ctx, smsg.Message.From, sa) + if err != nil { + return ethtypes.EthTx{}, xerrors.Errorf("failed to resolve Ethereum address: %w", err) + } + + tx.From = fromAddr + } else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { // Secp Filecoin Message + tx = ethTxFromNativeMessage(ctx, smsg.VMMessage(), sa) + tx.Hash, err = ethtypes.EthHashFromCid(smsg.Cid()) + if err != nil { + return tx, err + } + } else { // BLS Filecoin message + tx = ethTxFromNativeMessage(ctx, smsg.VMMessage(), sa) + tx.Hash, err = ethtypes.EthHashFromCid(smsg.Message.Cid()) + if err != nil { + return tx, err + } + } + + return tx, nil +} + +// ethTxFromNativeMessage does NOT populate: +// - BlockHash +// - BlockNumber +// - TransactionIndex +// - Hash +func ethTxFromNativeMessage(ctx context.Context, msg *types.Message, sa StateAPI) ethtypes.EthTx { + // We don't care if we error here, conversion is best effort for non-eth transactions + from, _ := lookupEthAddress(ctx, msg.From, sa) + to, _ := lookupEthAddress(ctx, msg.To, sa) + return ethtypes.EthTx{ + To: &to, + From: from, + Nonce: ethtypes.EthUint64(msg.Nonce), + ChainID: ethtypes.EthUint64(build.Eip155ChainId), + Value: ethtypes.EthBigInt(msg.Value), + Type: ethtypes.Eip1559TxType, + Gas: ethtypes.EthUint64(msg.GasLimit), + MaxFeePerGas: ethtypes.EthBigInt(msg.GasFeeCap), + MaxPriorityFeePerGas: ethtypes.EthBigInt(msg.GasPremium), + AccessList: []ethtypes.EthHash{}, + } +} + +func getSignedMessage(ctx context.Context, cs *store.ChainStore, msgCid cid.Cid) (*types.SignedMessage, error) { + smsg, err := cs.GetSignedMessage(ctx, msgCid) + if err != nil { + // We couldn't find the signed message, it might be a BLS message, so search for a regular message. + msg, err := cs.GetMessage(ctx, msgCid) + if err != nil { + return nil, xerrors.Errorf("failed to find msg %s: %w", msgCid, err) + } + smsg = &types.SignedMessage{ + Message: *msg, + Signature: crypto.Signature{ + Type: crypto.SigTypeBLS, + }, + } + } + + return smsg, nil +} + +// newEthTxFromMessageLookup creates an ethereum transaction from filecoin message lookup. If a negative txIdx is passed +// into the function, it looks up the transaction index of the message in the tipset, otherwise it uses the txIdx passed into the +// function +func newEthTxFromMessageLookup(ctx context.Context, msgLookup *api.MsgLookup, txIdx int, cs *store.ChainStore, sa StateAPI) (ethtypes.EthTx, error) { + ts, err := cs.LoadTipSet(ctx, msgLookup.TipSet) + if err != nil { + return ethtypes.EthTx{}, err + } + + // This tx is located in the parent tipset + parentTs, err := cs.LoadTipSet(ctx, ts.Parents()) + if err != nil { + return ethtypes.EthTx{}, err + } + + parentTsCid, err := parentTs.Key().Cid() + if err != nil { + return ethtypes.EthTx{}, err + } + + // lookup the transactionIndex + if txIdx < 0 { + msgs, err := cs.MessagesForTipset(ctx, parentTs) + if err != nil { + return ethtypes.EthTx{}, err + } + for i, msg := range msgs { + if msg.Cid() == msgLookup.Message { + txIdx = i + break + } + } + if txIdx < 0 { + return ethtypes.EthTx{}, fmt.Errorf("cannot find the msg in the tipset") + } + } + + blkHash, err := ethtypes.EthHashFromCid(parentTsCid) + if err != nil { + return ethtypes.EthTx{}, err + } + + smsg, err := getSignedMessage(ctx, cs, msgLookup.Message) + if err != nil { + return ethtypes.EthTx{}, xerrors.Errorf("failed to get signed msg: %w", err) + } + + tx, err := newEthTxFromSignedMessage(ctx, smsg, sa) + if err != nil { + return ethtypes.EthTx{}, err + } + + var ( + bn = ethtypes.EthUint64(parentTs.Height()) + ti = ethtypes.EthUint64(txIdx) + ) + + tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId) + tx.BlockHash = &blkHash + tx.BlockNumber = &bn + tx.TransactionIndex = &ti + return tx, nil +} + +func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLookup, events []types.Event, cs *store.ChainStore, sa StateAPI) (api.EthTxReceipt, error) { + var ( + transactionIndex ethtypes.EthUint64 + blockHash ethtypes.EthHash + blockNumber ethtypes.EthUint64 + ) + + if tx.TransactionIndex != nil { + transactionIndex = *tx.TransactionIndex + } + if tx.BlockHash != nil { + blockHash = *tx.BlockHash + } + if tx.BlockNumber != nil { + blockNumber = *tx.BlockNumber + } + + receipt := api.EthTxReceipt{ + TransactionHash: tx.Hash, + From: tx.From, + To: tx.To, + TransactionIndex: transactionIndex, + BlockHash: blockHash, + BlockNumber: blockNumber, + Type: ethtypes.EthUint64(2), + Logs: []ethtypes.EthLog{}, // empty log array is compulsory when no logs, or libraries like ethers.js break + LogsBloom: ethtypes.EmptyEthBloom[:], + } + + if lookup.Receipt.ExitCode.IsSuccess() { + receipt.Status = 1 + } else { + receipt.Status = 0 + } + + receipt.GasUsed = ethtypes.EthUint64(lookup.Receipt.GasUsed) + + // TODO: handle CumulativeGasUsed + receipt.CumulativeGasUsed = ethtypes.EmptyEthInt + + // TODO: avoid loading the tipset twice (once here, once when we convert the message to a txn) + ts, err := cs.GetTipSetFromKey(ctx, lookup.TipSet) + if err != nil { + return api.EthTxReceipt{}, xerrors.Errorf("failed to lookup tipset %s when constructing the eth txn receipt: %w", lookup.TipSet, err) + } + + baseFee := ts.Blocks()[0].ParentBaseFee + gasOutputs := vm.ComputeGasOutputs(lookup.Receipt.GasUsed, int64(tx.Gas), baseFee, big.Int(tx.MaxFeePerGas), big.Int(tx.MaxPriorityFeePerGas), true) + totalSpent := big.Sum(gasOutputs.BaseFeeBurn, gasOutputs.MinerTip, gasOutputs.OverEstimationBurn) + + effectiveGasPrice := big.Zero() + if lookup.Receipt.GasUsed > 0 { + effectiveGasPrice = big.Div(totalSpent, big.NewInt(lookup.Receipt.GasUsed)) + } + receipt.EffectiveGasPrice = ethtypes.EthBigInt(effectiveGasPrice) + + if receipt.To == nil && lookup.Receipt.ExitCode.IsSuccess() { + // Create and Create2 return the same things. + var ret eam.CreateExternalReturn + if err := ret.UnmarshalCBOR(bytes.NewReader(lookup.Receipt.Return)); err != nil { + return api.EthTxReceipt{}, xerrors.Errorf("failed to parse contract creation result: %w", err) + } + addr := ethtypes.EthAddress(ret.EthAddress) + receipt.ContractAddress = &addr + } + + if len(events) > 0 { + receipt.Logs = make([]ethtypes.EthLog, 0, len(events)) + for i, evt := range events { + l := ethtypes.EthLog{ + Removed: false, + LogIndex: ethtypes.EthUint64(i), + TransactionHash: tx.Hash, + TransactionIndex: transactionIndex, + BlockHash: blockHash, + BlockNumber: blockNumber, + } + + data, topics, ok := ethLogFromEvent(evt.Entries) + if !ok { + // not an eth event. + continue + } + for _, topic := range topics { + ethtypes.EthBloomSet(receipt.LogsBloom, topic[:]) + } + l.Data = data + l.Topics = topics + + addr, err := address.NewIDAddress(uint64(evt.Emitter)) + if err != nil { + return api.EthTxReceipt{}, xerrors.Errorf("failed to create ID address: %w", err) + } + + l.Address, err = lookupEthAddress(ctx, addr, sa) + if err != nil { + return api.EthTxReceipt{}, xerrors.Errorf("failed to resolve Ethereum address: %w", err) + } + + ethtypes.EthBloomSet(receipt.LogsBloom, l.Address[:]) + receipt.Logs = append(receipt.Logs, l) + } + } + + return receipt, nil +} diff --git a/node/impl/full/txhashmanager.go b/node/impl/full/txhashmanager.go new file mode 100644 index 00000000000..6757cc6dd92 --- /dev/null +++ b/node/impl/full/txhashmanager.go @@ -0,0 +1,129 @@ +package full + +import ( + "context" + "time" + + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/crypto" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/ethhashlookup" + "github.com/filecoin-project/lotus/chain/types" +) + +type EthTxHashManager struct { + StateAPI StateAPI + TransactionHashLookup *ethhashlookup.EthTxHashLookup +} + +func (m *EthTxHashManager) Revert(ctx context.Context, from, to *types.TipSet) error { + return nil +} + +func (m *EthTxHashManager) PopulateExistingMappings(ctx context.Context, minHeight abi.ChainEpoch) error { + if minHeight < build.UpgradeHyggeHeight { + minHeight = build.UpgradeHyggeHeight + } + + ts := m.StateAPI.Chain.GetHeaviestTipSet() + for ts.Height() > minHeight { + for _, block := range ts.Blocks() { + msgs, err := m.StateAPI.Chain.SecpkMessagesForBlock(ctx, block) + if err != nil { + // If we can't find the messages, we've either imported from snapshot or pruned the store + log.Debug("exiting message mapping population at epoch ", ts.Height()) + return nil + } + + for _, msg := range msgs { + m.ProcessSignedMessage(ctx, msg) + } + } + + var err error + ts, err = m.StateAPI.Chain.GetTipSetFromKey(ctx, ts.Parents()) + if err != nil { + return err + } + } + + return nil +} + +func (m *EthTxHashManager) Apply(ctx context.Context, from, to *types.TipSet) error { + for _, blk := range to.Blocks() { + _, smsgs, err := m.StateAPI.Chain.MessagesForBlock(ctx, blk) + if err != nil { + return err + } + + for _, smsg := range smsgs { + if smsg.Signature.Type != crypto.SigTypeDelegated { + continue + } + + hash, err := ethTxHashFromSignedMessage(ctx, smsg, m.StateAPI) + if err != nil { + return err + } + + err = m.TransactionHashLookup.UpsertHash(hash, smsg.Cid()) + if err != nil { + return err + } + } + } + + return nil +} + +func (m *EthTxHashManager) ProcessSignedMessage(ctx context.Context, msg *types.SignedMessage) { + if msg.Signature.Type != crypto.SigTypeDelegated { + return + } + + ethTx, err := newEthTxFromSignedMessage(ctx, msg, m.StateAPI) + if err != nil { + log.Errorf("error converting filecoin message to eth tx: %s", err) + return + } + + err = m.TransactionHashLookup.UpsertHash(ethTx.Hash, msg.Cid()) + if err != nil { + log.Errorf("error inserting tx mapping to db: %s", err) + return + } +} + +func WaitForMpoolUpdates(ctx context.Context, ch <-chan api.MpoolUpdate, manager *EthTxHashManager) { + for { + select { + case <-ctx.Done(): + return + case u := <-ch: + if u.Type != api.MpoolAdd { + continue + } + + manager.ProcessSignedMessage(ctx, u.Message) + } + } +} + +func EthTxHashGC(ctx context.Context, retentionDays int, manager *EthTxHashManager) { + if retentionDays == 0 { + return + } + + gcPeriod := 1 * time.Hour + for { + entriesDeleted, err := manager.TransactionHashLookup.DeleteEntriesOlderThan(retentionDays) + if err != nil { + log.Errorf("error garbage collecting eth transaction hash database: %s", err) + } + log.Info("garbage collection run on eth transaction hash lookup database. %d entries deleted", entriesDeleted) + time.Sleep(gcPeriod) + } +} diff --git a/node/modules/trace.go b/node/modules/trace.go deleted file mode 100644 index aea7fc02f72..00000000000 --- a/node/modules/trace.go +++ /dev/null @@ -1,19 +0,0 @@ -package modules - -import ( - "github.com/filecoin-project/lotus/chain/stmgr" - "github.com/filecoin-project/lotus/chain/store" - "github.com/filecoin-project/lotus/node/impl/full" -) - -func EthTraceAPI() func(*store.ChainStore, *stmgr.StateManager, full.EthModuleAPI, full.ChainAPI) (*full.EthTrace, error) { - return func(cs *store.ChainStore, sm *stmgr.StateManager, evapi full.EthModuleAPI, chainapi full.ChainAPI) (*full.EthTrace, error) { - return &full.EthTrace{ - Chain: cs, - StateManager: sm, - - ChainAPI: chainapi, - EthModuleAPI: evapi, - }, nil - } -} From ebb54bc3814fdf980bc32b85d5c99460c559de26 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Mon, 21 Aug 2023 11:29:09 +0000 Subject: [PATCH 09/25] fix naming lint --- node/impl/full/eth.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index a2f406b7c2d..df27ad4ebf6 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -825,23 +825,23 @@ func (a *EthModule) Web3ClientVersion(ctx context.Context) (string, error) { return build.UserVersion(), nil } -func (e *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { - ts, err := getTipsetByBlockNr(ctx, e.Chain, blkNum, false) +func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { + ts, err := getTipsetByBlockNr(ctx, a.Chain, blkNum, false) if err != nil { return nil, err } - _, trace, err := e.StateManager.ExecutionTrace(ctx, ts) + _, trace, err := a.StateManager.ExecutionTrace(ctx, ts) if err != nil { return nil, xerrors.Errorf("failed to compute base state: %w", err) } - tsParent, err := e.ChainAPI.ChainGetTipSetByHeight(ctx, ts.Height()+1, e.Chain.GetHeaviestTipSet().Key()) + tsParent, err := a.ChainAPI.ChainGetTipSetByHeight(ctx, ts.Height()+1, a.Chain.GetHeaviestTipSet().Key()) if err != nil { return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height()+1) } - msgs, err := e.ChainGetParentMessages(ctx, tsParent.Blocks()[0].Cid()) + msgs, err := a.ChainGetParentMessages(ctx, tsParent.Blocks()[0].Cid()) if err != nil { return nil, err } @@ -875,7 +875,7 @@ func (e *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, continue } - txHash, err := e.EthGetTransactionHashByCid(ctx, ir.MsgCid) + txHash, err := a.EthGetTransactionHashByCid(ctx, ir.MsgCid) if err != nil { return nil, err } @@ -907,17 +907,17 @@ func (e *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, return allTraces, nil } -func (e *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { +func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { if len(traceTypes) != 1 || traceTypes[0] != "trace" { return nil, fmt.Errorf("only 'trace' is supported") } - ts, err := getTipsetByBlockNr(ctx, e.Chain, blkNum, false) + ts, err := getTipsetByBlockNr(ctx, a.Chain, blkNum, false) if err != nil { return nil, err } - _, trace, err := e.StateManager.ExecutionTrace(ctx, ts) + _, trace, err := a.StateManager.ExecutionTrace(ctx, ts) if err != nil { return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) } @@ -929,7 +929,7 @@ func (e *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum str continue } - txHash, err := e.EthGetTransactionHashByCid(ctx, ir.MsgCid) + txHash, err := a.EthGetTransactionHashByCid(ctx, ir.MsgCid) if err != nil { return nil, err } From 4068e0710dcfbd7a8eda51ce6913af53c50b8df7 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 22 Aug 2023 11:05:28 +0000 Subject: [PATCH 10/25] Do not return interface{} from trace api methods --- api/api_full.go | 4 +- api/api_gateway.go | 4 +- api/mocks/mock_full.go | 8 +-- api/proxy_gen.go | 40 ++++++------ .../types/ethtypes}/eth_trace.go | 52 ++++++++-------- documentation/en/api-v1-unstable-methods.md | 62 ++++++++++++++++++- gateway/node.go | 4 +- gateway/proxy_eth.go | 12 ++-- node/impl/full/dummy.go | 4 +- node/impl/full/eth.go | 24 +++---- 10 files changed, 137 insertions(+), 77 deletions(-) rename {node/impl/full => chain/types/ethtypes}/eth_trace.go (88%) diff --git a/api/api_full.go b/api/api_full.go index 95d86ce6f34..ce187feefe0 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -871,9 +871,9 @@ type FullNode interface { // TraceAPI related methods // // Returns traces created at given block - TraceBlock(ctx context.Context, blkNum string) (interface{}, error) //perm:read + TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) //perm:read // Replays all transactions in a block returning the requested traces for each transaction - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) //perm:read + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) //perm:read // CreateBackup creates node backup onder the specified file name. The // method requires that the lotus daemon is running with the diff --git a/api/api_gateway.go b/api/api_gateway.go index 0f2e66709cc..fef7c2c0480 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -127,6 +127,6 @@ type Gateway interface { EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) Web3ClientVersion(ctx context.Context) (string, error) - TraceBlock(ctx context.Context, blkNum string) (interface{}, error) - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) + TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) } diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index 882aacebb06..24a7cc207a8 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -4024,10 +4024,10 @@ func (mr *MockFullNodeMockRecorder) SyncValidateTipset(arg0, arg1 interface{}) * } // TraceBlock mocks base method. -func (m *MockFullNode) TraceBlock(arg0 context.Context, arg1 string) (interface{}, error) { +func (m *MockFullNode) TraceBlock(arg0 context.Context, arg1 string) ([]*ethtypes.TraceBlock, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TraceBlock", arg0, arg1) - ret0, _ := ret[0].(interface{}) + ret0, _ := ret[0].([]*ethtypes.TraceBlock) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -4039,10 +4039,10 @@ func (mr *MockFullNodeMockRecorder) TraceBlock(arg0, arg1 interface{}) *gomock.C } // TraceReplayBlockTransactions mocks base method. -func (m *MockFullNode) TraceReplayBlockTransactions(arg0 context.Context, arg1 string, arg2 []string) (interface{}, error) { +func (m *MockFullNode) TraceReplayBlockTransactions(arg0 context.Context, arg1 string, arg2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TraceReplayBlockTransactions", arg0, arg1, arg2) - ret0, _ := ret[0].(interface{}) + ret0, _ := ret[0].([]*ethtypes.TraceReplayBlockTransaction) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 6f0487b0be8..d3f49daf90d 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -596,9 +596,9 @@ type FullNodeMethods struct { SyncValidateTipset func(p0 context.Context, p1 types.TipSetKey) (bool, error) `perm:"read"` - TraceBlock func(p0 context.Context, p1 string) (interface{}, error) `perm:"read"` + TraceBlock func(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) `perm:"read"` - TraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) (interface{}, error) `perm:"read"` + TraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) `perm:"read"` WalletBalance func(p0 context.Context, p1 address.Address) (types.BigInt, error) `perm:"read"` @@ -818,9 +818,9 @@ type GatewayMethods struct { StateWaitMsg func(p0 context.Context, p1 cid.Cid, p2 uint64, p3 abi.ChainEpoch, p4 bool) (*MsgLookup, error) `` - TraceBlock func(p0 context.Context, p1 string) (interface{}, error) `` + TraceBlock func(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) `` - TraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) (interface{}, error) `` + TraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) `` Version func(p0 context.Context) (APIVersion, error) `` @@ -4005,26 +4005,26 @@ func (s *FullNodeStub) SyncValidateTipset(p0 context.Context, p1 types.TipSetKey return false, ErrNotSupported } -func (s *FullNodeStruct) TraceBlock(p0 context.Context, p1 string) (interface{}, error) { +func (s *FullNodeStruct) TraceBlock(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) { if s.Internal.TraceBlock == nil { - return nil, ErrNotSupported + return *new([]*ethtypes.TraceBlock), ErrNotSupported } return s.Internal.TraceBlock(p0, p1) } -func (s *FullNodeStub) TraceBlock(p0 context.Context, p1 string) (interface{}, error) { - return nil, ErrNotSupported +func (s *FullNodeStub) TraceBlock(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) { + return *new([]*ethtypes.TraceBlock), ErrNotSupported } -func (s *FullNodeStruct) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) (interface{}, error) { +func (s *FullNodeStruct) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { if s.Internal.TraceReplayBlockTransactions == nil { - return nil, ErrNotSupported + return *new([]*ethtypes.TraceReplayBlockTransaction), ErrNotSupported } return s.Internal.TraceReplayBlockTransactions(p0, p1, p2) } -func (s *FullNodeStub) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) (interface{}, error) { - return nil, ErrNotSupported +func (s *FullNodeStub) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { + return *new([]*ethtypes.TraceReplayBlockTransaction), ErrNotSupported } func (s *FullNodeStruct) WalletBalance(p0 context.Context, p1 address.Address) (types.BigInt, error) { @@ -5160,26 +5160,26 @@ func (s *GatewayStub) StateWaitMsg(p0 context.Context, p1 cid.Cid, p2 uint64, p3 return nil, ErrNotSupported } -func (s *GatewayStruct) TraceBlock(p0 context.Context, p1 string) (interface{}, error) { +func (s *GatewayStruct) TraceBlock(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) { if s.Internal.TraceBlock == nil { - return nil, ErrNotSupported + return *new([]*ethtypes.TraceBlock), ErrNotSupported } return s.Internal.TraceBlock(p0, p1) } -func (s *GatewayStub) TraceBlock(p0 context.Context, p1 string) (interface{}, error) { - return nil, ErrNotSupported +func (s *GatewayStub) TraceBlock(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) { + return *new([]*ethtypes.TraceBlock), ErrNotSupported } -func (s *GatewayStruct) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) (interface{}, error) { +func (s *GatewayStruct) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { if s.Internal.TraceReplayBlockTransactions == nil { - return nil, ErrNotSupported + return *new([]*ethtypes.TraceReplayBlockTransaction), ErrNotSupported } return s.Internal.TraceReplayBlockTransactions(p0, p1, p2) } -func (s *GatewayStub) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) (interface{}, error) { - return nil, ErrNotSupported +func (s *GatewayStub) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { + return *new([]*ethtypes.TraceReplayBlockTransaction), ErrNotSupported } func (s *GatewayStruct) Version(p0 context.Context) (APIVersion, error) { diff --git a/node/impl/full/eth_trace.go b/chain/types/ethtypes/eth_trace.go similarity index 88% rename from node/impl/full/eth_trace.go rename to chain/types/ethtypes/eth_trace.go index 3e3b48c904f..66fb860b658 100644 --- a/node/impl/full/eth_trace.go +++ b/chain/types/ethtypes/eth_trace.go @@ -1,4 +1,4 @@ -package full +package ethtypes import ( "bytes" @@ -8,6 +8,7 @@ import ( "io" "github.com/ipfs/go-cid" + logging "github.com/ipfs/go-log/v2" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/builtin" @@ -15,9 +16,10 @@ import ( builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/types" - "github.com/filecoin-project/lotus/chain/types/ethtypes" ) +var log = logging.Logger("traceapi") + type Trace struct { Action Action `json:"action"` Result Result `json:"result"` @@ -35,51 +37,51 @@ func (t *Trace) setCallType(callType string) { type TraceBlock struct { *Trace - BlockHash ethtypes.EthHash `json:"blockHash"` - BlockNumber int64 `json:"blockNumber"` - TransactionHash ethtypes.EthHash `json:"transactionHash"` - TransactionPosition int `json:"transactionPosition"` + BlockHash EthHash `json:"blockHash"` + BlockNumber int64 `json:"blockNumber"` + TransactionHash EthHash `json:"transactionHash"` + TransactionPosition int `json:"transactionPosition"` } type TraceReplayBlockTransaction struct { - Output string `json:"output"` - StateDiff *string `json:"stateDiff"` - Trace []*Trace `json:"trace"` - TransactionHash ethtypes.EthHash `json:"transactionHash"` - VmTrace *string `json:"vmTrace"` + Output string `json:"output"` + StateDiff *string `json:"stateDiff"` + Trace []*Trace `json:"trace"` + TransactionHash EthHash `json:"transactionHash"` + VmTrace *string `json:"vmTrace"` } type Action struct { - CallType string `json:"callType"` - From string `json:"from"` - To string `json:"to"` - Gas ethtypes.EthUint64 `json:"gas"` - Input string `json:"input"` - Value ethtypes.EthBigInt `json:"value"` + CallType string `json:"callType"` + From string `json:"from"` + To string `json:"to"` + Gas EthUint64 `json:"gas"` + Input string `json:"input"` + Value EthBigInt `json:"value"` method abi.MethodNum `json:"-"` codeCid cid.Cid `json:"-"` } type Result struct { - GasUsed ethtypes.EthUint64 `json:"gasUsed"` - Output string `json:"output"` + GasUsed EthUint64 `json:"gasUsed"` + Output string `json:"output"` } -// buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls -func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.ExecutionTrace, height int64) error { +// BuildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls +func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.ExecutionTrace, height int64) error { trace := &Trace{ Action: Action{ From: et.Msg.From.String(), To: et.Msg.To.String(), - Gas: ethtypes.EthUint64(et.Msg.GasLimit), + Gas: EthUint64(et.Msg.GasLimit), Input: hex.EncodeToString(et.Msg.Params), - Value: ethtypes.EthBigInt(et.Msg.Value), + Value: EthBigInt(et.Msg.Value), method: et.Msg.Method, codeCid: et.Msg.CodeCid, }, Result: Result{ - GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), + GasUsed: EthUint64(et.SumGas().TotalGas), Output: hex.EncodeToString(et.MsgRct.Return), }, Subtraces: len(et.Subcalls), @@ -218,7 +220,7 @@ func buildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution *traces = append(*traces, trace) for i, call := range et.Subcalls { - err := buildTraces(traces, trace, append(addr, i), call, height) + err := BuildTraces(traces, trace, append(addr, i), call, height) if err != nil { return err } diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index 273f20dc947..a488ee64bd1 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -8841,7 +8841,34 @@ Inputs: ] ``` -Response: `{}` +Response: +```json +[ + { + "action": { + "callType": "string value", + "from": "string value", + "to": "string value", + "gas": "0x5", + "input": "string value", + "value": "0x0" + }, + "result": { + "gasUsed": "0x5", + "output": "string value" + }, + "subtraces": 123, + "traceAddress": [ + 123 + ], + "Type": "string value", + "blockHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", + "blockNumber": 9, + "transactionHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", + "transactionPosition": 123 + } +] +``` ### TraceReplayBlockTransactions Replays all transactions in a block returning the requested traces for each transaction @@ -8859,7 +8886,38 @@ Inputs: ] ``` -Response: `{}` +Response: +```json +[ + { + "output": "string value", + "stateDiff": "string value", + "trace": [ + { + "action": { + "callType": "string value", + "from": "string value", + "to": "string value", + "gas": "0x5", + "input": "string value", + "value": "0x0" + }, + "result": { + "gasUsed": "0x5", + "output": "string value" + }, + "subtraces": 123, + "traceAddress": [ + 123 + ], + "Type": "string value" + } + ], + "transactionHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", + "vmTrace": "string value" + } +] +``` ## Wallet diff --git a/gateway/node.go b/gateway/node.go index bbda71fcf24..1ed846d1a27 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -144,8 +144,8 @@ type TargetAPI interface { EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) Web3ClientVersion(ctx context.Context) (string, error) - TraceBlock(ctx context.Context, blkNum string) (interface{}, error) - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) + TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) } var _ TargetAPI = *new(api.FullNode) // gateway depends on latest diff --git a/gateway/proxy_eth.go b/gateway/proxy_eth.go index f8b950011d6..42e9d8e4599 100644 --- a/gateway/proxy_eth.go +++ b/gateway/proxy_eth.go @@ -582,25 +582,25 @@ func (gw *Node) Web3ClientVersion(ctx context.Context) (string, error) { return gw.target.Web3ClientVersion(ctx) } -func (gw *Node) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { +func (gw *Node) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) { if err := gw.limit(ctx, stateRateLimitTokens); err != nil { - return 0, err + return nil, err } if err := gw.checkBlkParam(ctx, blkNum, 0); err != nil { - return ethtypes.EthBlock{}, err + return nil, err } return gw.target.TraceBlock(ctx, blkNum) } -func (gw *Node) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { +func (gw *Node) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { if err := gw.limit(ctx, stateRateLimitTokens); err != nil { - return 0, err + return nil, err } if err := gw.checkBlkParam(ctx, blkNum, 0); err != nil { - return ethtypes.EthBlock{}, err + return nil, err } return gw.target.TraceReplayBlockTransactions(ctx, blkNum, traceTypes) diff --git a/node/impl/full/dummy.go b/node/impl/full/dummy.go index c06e5c084e5..81481ea2c54 100644 --- a/node/impl/full/dummy.go +++ b/node/impl/full/dummy.go @@ -178,11 +178,11 @@ func (e *EthModuleDummy) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubs return false, ErrModuleDisabled } -func (e *EthModuleDummy) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { +func (e *EthModuleDummy) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) { return nil, ErrModuleDisabled } -func (e *EthModuleDummy) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { +func (e *EthModuleDummy) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { return nil, ErrModuleDisabled } diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index df27ad4ebf6..bbf98f0aa8c 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -71,8 +71,8 @@ type EthModuleAPI interface { EthMaxPriorityFeePerGas(ctx context.Context) (ethtypes.EthBigInt, error) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) Web3ClientVersion(ctx context.Context) (string, error) - TraceBlock(ctx context.Context, blkNum string) (interface{}, error) - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) + TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) + TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) } type EthEventAPI interface { @@ -825,7 +825,7 @@ func (a *EthModule) Web3ClientVersion(ctx context.Context) (string, error) { return build.UserVersion(), nil } -func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, error) { +func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) { ts, err := getTipsetByBlockNr(ctx, a.Chain, blkNum, false) if err != nil { return nil, err @@ -856,7 +856,7 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, return nil, err } - allTraces := make([]*TraceBlock, 0, len(trace)) + allTraces := make([]*ethtypes.TraceBlock, 0, len(trace)) for _, ir := range trace { // ignore messages from f00 if ir.Msg.From.String() == "f00" { @@ -884,15 +884,15 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, continue } - traces := []*Trace{} - err = buildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) + traces := []*ethtypes.Trace{} + err = ethtypes.BuildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) if err != nil { return nil, xerrors.Errorf("failed when building traces: %w", err) } - traceBlocks := make([]*TraceBlock, 0, len(trace)) + traceBlocks := make([]*ethtypes.TraceBlock, 0, len(trace)) for _, trace := range traces { - traceBlocks = append(traceBlocks, &TraceBlock{ + traceBlocks = append(traceBlocks, ðtypes.TraceBlock{ Trace: trace, BlockHash: blkHash, BlockNumber: int64(ts.Height()), @@ -907,7 +907,7 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) (interface{}, return allTraces, nil } -func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) (interface{}, error) { +func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { if len(traceTypes) != 1 || traceTypes[0] != "trace" { return nil, fmt.Errorf("only 'trace' is supported") } @@ -922,7 +922,7 @@ func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum str return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) } - allTraces := make([]*TraceReplayBlockTransaction, 0, len(trace)) + allTraces := make([]*ethtypes.TraceReplayBlockTransaction, 0, len(trace)) for _, ir := range trace { // ignore messages from f00 if ir.Msg.From.String() == "f00" { @@ -938,14 +938,14 @@ func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum str continue } - t := TraceReplayBlockTransaction{ + t := ethtypes.TraceReplayBlockTransaction{ Output: hex.EncodeToString(ir.MsgRct.Return), TransactionHash: *txHash, StateDiff: nil, VmTrace: nil, } - err = buildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) + err = ethtypes.BuildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) if err != nil { return nil, xerrors.Errorf("failed when building traces: %w", err) } From a1b890c8c7a9ba27db594e17665bef015b485a1b Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 22 Aug 2023 14:30:46 +0000 Subject: [PATCH 11/25] return wrapped errors --- node/impl/full/eth.go | 40 ++++++++++++++++++------------------- node/impl/full/eth_utils.go | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index bbf98f0aa8c..0d983d803ae 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -238,7 +238,7 @@ func (a *EthModule) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthH } func (a *EthModule) EthGetBlockByNumber(ctx context.Context, blkParam string, fullTxInfo bool) (ethtypes.EthBlock, error) { - ts, err := getTipsetByBlockNr(ctx, a.Chain, blkParam, true) + ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkParam, true) if err != nil { return ethtypes.EthBlock{}, err } @@ -693,7 +693,7 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth } } - ts, err := getTipsetByBlockNr(ctx, a.Chain, params.NewestBlkNum, false) + ts, err := getTipsetByBlockNumber(ctx, a.Chain, params.NewestBlkNum, false) if err != nil { return ethtypes.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err) } @@ -826,40 +826,40 @@ func (a *EthModule) Web3ClientVersion(ctx context.Context) (string, error) { } func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) { - ts, err := getTipsetByBlockNr(ctx, a.Chain, blkNum, false) + ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, false) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to get tipset: %w", err) } _, trace, err := a.StateManager.ExecutionTrace(ctx, ts) if err != nil { - return nil, xerrors.Errorf("failed to compute base state: %w", err) + return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) } tsParent, err := a.ChainAPI.ChainGetTipSetByHeight(ctx, ts.Height()+1, a.Chain.GetHeaviestTipSet().Key()) if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height()+1) + return nil, xerrors.Errorf("cannot get tipset at height: %v", ts.Height()+1) } msgs, err := a.ChainGetParentMessages(ctx, tsParent.Blocks()[0].Cid()) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to get parent messages: %w", err) } cid, err := ts.Key().Cid() if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to get tipset key cid: %w", err) } blkHash, err := ethtypes.EthHashFromCid(cid) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to parse eth hash from cid: %w", err) } allTraces := make([]*ethtypes.TraceBlock, 0, len(trace)) for _, ir := range trace { - // ignore messages from f00 - if ir.Msg.From.String() == "f00" { + // ignore messages from system actor + if ir.Msg.From == builtinactors.SystemActorAddr { continue } @@ -877,7 +877,7 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes. txHash, err := a.EthGetTransactionHashByCid(ctx, ir.MsgCid) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to get transaction hash by cid: %w", err) } if txHash == nil { log.Warnf("cannot find transaction hash for cid %s", ir.MsgCid) @@ -887,10 +887,10 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes. traces := []*ethtypes.Trace{} err = ethtypes.BuildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) if err != nil { - return nil, xerrors.Errorf("failed when building traces: %w", err) + return nil, xerrors.Errorf("failed building traces: %w", err) } - traceBlocks := make([]*ethtypes.TraceBlock, 0, len(trace)) + traceBlocks := make([]*ethtypes.TraceBlock, 0, len(traces)) for _, trace := range traces { traceBlocks = append(traceBlocks, ðtypes.TraceBlock{ Trace: trace, @@ -912,9 +912,9 @@ func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum str return nil, fmt.Errorf("only 'trace' is supported") } - ts, err := getTipsetByBlockNr(ctx, a.Chain, blkNum, false) + ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, false) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to get tipset: %w", err) } _, trace, err := a.StateManager.ExecutionTrace(ctx, ts) @@ -924,14 +924,14 @@ func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum str allTraces := make([]*ethtypes.TraceReplayBlockTransaction, 0, len(trace)) for _, ir := range trace { - // ignore messages from f00 - if ir.Msg.From.String() == "f00" { + // ignore messages from system actor + if ir.Msg.From == builtinactors.SystemActorAddr { continue } txHash, err := a.EthGetTransactionHashByCid(ctx, ir.MsgCid) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to get transaction hash by cid: %w", err) } if txHash == nil { log.Warnf("cannot find transaction hash for cid %s", ir.MsgCid) @@ -947,7 +947,7 @@ func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum str err = ethtypes.BuildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) if err != nil { - return nil, xerrors.Errorf("failed when building traces: %w", err) + return nil, xerrors.Errorf("failed building traces: %w", err) } allTraces = append(allTraces, &t) diff --git a/node/impl/full/eth_utils.go b/node/impl/full/eth_utils.go index ab17d13b2c0..5908c941291 100644 --- a/node/impl/full/eth_utils.go +++ b/node/impl/full/eth_utils.go @@ -25,7 +25,7 @@ import ( "github.com/filecoin-project/lotus/chain/vm" ) -func getTipsetByBlockNr(ctx context.Context, chain *store.ChainStore, blkParam string, strict bool) (*types.TipSet, error) { +func getTipsetByBlockNumber(ctx context.Context, chain *store.ChainStore, blkParam string, strict bool) (*types.TipSet, error) { if blkParam == "earliest" { return nil, fmt.Errorf("block param \"earliest\" is not supported") } From ef7bcfec067284e86c11b9879d41ecaff5f67552 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 22 Aug 2023 14:32:59 +0000 Subject: [PATCH 12/25] Do not compute message index as traces should be in message execution order --- chain/types/ethtypes/eth_trace.go | 2 +- node/impl/full/eth.go | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/chain/types/ethtypes/eth_trace.go b/chain/types/ethtypes/eth_trace.go index 66fb860b658..14f7e33328c 100644 --- a/chain/types/ethtypes/eth_trace.go +++ b/chain/types/ethtypes/eth_trace.go @@ -229,7 +229,7 @@ func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution return nil } -func writePadded[T any](w io.Writer, data T, size int) error { +func writePadded(w io.Writer, data any, size int) error { tmp := &bytes.Buffer{} // first write data to tmp buffer to get the size diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 0d983d803ae..25796e8640f 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -857,23 +857,19 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes. } allTraces := make([]*ethtypes.TraceBlock, 0, len(trace)) + msgIdx := 0 for _, ir := range trace { // ignore messages from system actor if ir.Msg.From == builtinactors.SystemActorAddr { continue } - idx := -1 - for msgIdx, msg := range msgs { - if ir.Msg.From == msg.Message.From { - idx = msgIdx - break - } - } - if idx == -1 { - log.Warnf("cannot resolve message index for cid: %s", ir.MsgCid) - continue + // as we include TransactionPosition in the results, lets do sanity checking that the + // traces are indeed in the message execution order + if ir.Msg.Cid() != msgs[msgIdx].Message.Cid() { + return nil, xerrors.Errorf("traces are not in message execution order") } + msgIdx++ txHash, err := a.EthGetTransactionHashByCid(ctx, ir.MsgCid) if err != nil { @@ -897,7 +893,7 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes. BlockHash: blkHash, BlockNumber: int64(ts.Height()), TransactionHash: *txHash, - TransactionPosition: idx, + TransactionPosition: msgIdx, }) } From 8d8891a431ebfe8076da8a102e0ac841baf9f39c Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 22 Aug 2023 16:15:14 +0000 Subject: [PATCH 13/25] Moved tracing types to ethtypes to address circular dependencies --- api/api_full.go | 4 +- api/api_gateway.go | 4 +- api/eth_aliases.go | 4 +- api/mocks/mock_full.go | 60 +++--- api/proxy_gen.go | 104 +++++----- chain/types/ethtypes/eth_types.go | 49 +++++ documentation/en/api-v1-unstable-methods.md | 194 +++++++++--------- gateway/node.go | 4 +- gateway/proxy_eth.go | 8 +- node/impl/full/dummy.go | 4 +- node/impl/full/eth.go | 26 +-- .../ethtypes => node/impl/full}/eth_trace.go | 123 ++++------- 12 files changed, 289 insertions(+), 295 deletions(-) rename {chain/types/ethtypes => node/impl/full}/eth_trace.go (71%) diff --git a/api/api_full.go b/api/api_full.go index ce187feefe0..55dcc23dfb8 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -871,9 +871,9 @@ type FullNode interface { // TraceAPI related methods // // Returns traces created at given block - TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) //perm:read + EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) //perm:read // Replays all transactions in a block returning the requested traces for each transaction - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) //perm:read + EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) //perm:read // CreateBackup creates node backup onder the specified file name. The // method requires that the lotus daemon is running with the diff --git a/api/api_gateway.go b/api/api_gateway.go index fef7c2c0480..08199564d35 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -127,6 +127,6 @@ type Gateway interface { EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) Web3ClientVersion(ctx context.Context) (string, error) - TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) + EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) + EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) } diff --git a/api/eth_aliases.go b/api/eth_aliases.go index fe761c545f4..eb0c510050e 100644 --- a/api/eth_aliases.go +++ b/api/eth_aliases.go @@ -40,8 +40,8 @@ func CreateEthRPCAliases(as apitypes.Aliaser) { as.AliasMethod("eth_subscribe", "Filecoin.EthSubscribe") as.AliasMethod("eth_unsubscribe", "Filecoin.EthUnsubscribe") - as.AliasMethod("trace_block", "Filecoin.TraceBlock") - as.AliasMethod("trace_replayBlockTransactions", "Filecoin.TraceReplayBlockTransactions") + as.AliasMethod("trace_block", "Filecoin.EthTraceBlock") + as.AliasMethod("trace_replayBlockTransactions", "Filecoin.EthTraceReplayBlockTransactions") as.AliasMethod("net_version", "Filecoin.NetVersion") as.AliasMethod("net_listening", "Filecoin.NetListening") diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index 24a7cc207a8..856d83813af 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -1491,6 +1491,36 @@ func (mr *MockFullNodeMockRecorder) EthSyncing(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthSyncing", reflect.TypeOf((*MockFullNode)(nil).EthSyncing), arg0) } +// EthTraceBlock mocks base method. +func (m *MockFullNode) EthTraceBlock(arg0 context.Context, arg1 string) ([]*ethtypes.EthTraceBlock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthTraceBlock", arg0, arg1) + ret0, _ := ret[0].([]*ethtypes.EthTraceBlock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthTraceBlock indicates an expected call of EthTraceBlock. +func (mr *MockFullNodeMockRecorder) EthTraceBlock(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthTraceBlock", reflect.TypeOf((*MockFullNode)(nil).EthTraceBlock), arg0, arg1) +} + +// EthTraceReplayBlockTransactions mocks base method. +func (m *MockFullNode) EthTraceReplayBlockTransactions(arg0 context.Context, arg1 string, arg2 []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthTraceReplayBlockTransactions", arg0, arg1, arg2) + ret0, _ := ret[0].([]*ethtypes.EthTraceReplayBlockTransaction) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthTraceReplayBlockTransactions indicates an expected call of EthTraceReplayBlockTransactions. +func (mr *MockFullNodeMockRecorder) EthTraceReplayBlockTransactions(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthTraceReplayBlockTransactions", reflect.TypeOf((*MockFullNode)(nil).EthTraceReplayBlockTransactions), arg0, arg1, arg2) +} + // EthUninstallFilter mocks base method. func (m *MockFullNode) EthUninstallFilter(arg0 context.Context, arg1 ethtypes.EthFilterID) (bool, error) { m.ctrl.T.Helper() @@ -4023,36 +4053,6 @@ func (mr *MockFullNodeMockRecorder) SyncValidateTipset(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncValidateTipset", reflect.TypeOf((*MockFullNode)(nil).SyncValidateTipset), arg0, arg1) } -// TraceBlock mocks base method. -func (m *MockFullNode) TraceBlock(arg0 context.Context, arg1 string) ([]*ethtypes.TraceBlock, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TraceBlock", arg0, arg1) - ret0, _ := ret[0].([]*ethtypes.TraceBlock) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// TraceBlock indicates an expected call of TraceBlock. -func (mr *MockFullNodeMockRecorder) TraceBlock(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TraceBlock", reflect.TypeOf((*MockFullNode)(nil).TraceBlock), arg0, arg1) -} - -// TraceReplayBlockTransactions mocks base method. -func (m *MockFullNode) TraceReplayBlockTransactions(arg0 context.Context, arg1 string, arg2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TraceReplayBlockTransactions", arg0, arg1, arg2) - ret0, _ := ret[0].([]*ethtypes.TraceReplayBlockTransaction) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// TraceReplayBlockTransactions indicates an expected call of TraceReplayBlockTransactions. -func (mr *MockFullNodeMockRecorder) TraceReplayBlockTransactions(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TraceReplayBlockTransactions", reflect.TypeOf((*MockFullNode)(nil).TraceReplayBlockTransactions), arg0, arg1, arg2) -} - // Version mocks base method. func (m *MockFullNode) Version(arg0 context.Context) (api.APIVersion, error) { m.ctrl.T.Helper() diff --git a/api/proxy_gen.go b/api/proxy_gen.go index d3f49daf90d..1082e8f4ce4 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -316,6 +316,10 @@ type FullNodeMethods struct { EthSyncing func(p0 context.Context) (ethtypes.EthSyncingResult, error) `perm:"read"` + EthTraceBlock func(p0 context.Context, p1 string) ([]*ethtypes.EthTraceBlock, error) `perm:"read"` + + EthTraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) `perm:"read"` + EthUninstallFilter func(p0 context.Context, p1 ethtypes.EthFilterID) (bool, error) `perm:"read"` EthUnsubscribe func(p0 context.Context, p1 ethtypes.EthSubscriptionID) (bool, error) `perm:"read"` @@ -596,10 +600,6 @@ type FullNodeMethods struct { SyncValidateTipset func(p0 context.Context, p1 types.TipSetKey) (bool, error) `perm:"read"` - TraceBlock func(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) `perm:"read"` - - TraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) `perm:"read"` - WalletBalance func(p0 context.Context, p1 address.Address) (types.BigInt, error) `perm:"read"` WalletDefaultAddress func(p0 context.Context) (address.Address, error) `perm:"write"` @@ -736,6 +736,10 @@ type GatewayMethods struct { EthSyncing func(p0 context.Context) (ethtypes.EthSyncingResult, error) `` + EthTraceBlock func(p0 context.Context, p1 string) ([]*ethtypes.EthTraceBlock, error) `` + + EthTraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) `` + EthUninstallFilter func(p0 context.Context, p1 ethtypes.EthFilterID) (bool, error) `` EthUnsubscribe func(p0 context.Context, p1 ethtypes.EthSubscriptionID) (bool, error) `` @@ -818,10 +822,6 @@ type GatewayMethods struct { StateWaitMsg func(p0 context.Context, p1 cid.Cid, p2 uint64, p3 abi.ChainEpoch, p4 bool) (*MsgLookup, error) `` - TraceBlock func(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) `` - - TraceReplayBlockTransactions func(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) `` - Version func(p0 context.Context) (APIVersion, error) `` WalletBalance func(p0 context.Context, p1 address.Address) (types.BigInt, error) `` @@ -2465,6 +2465,28 @@ func (s *FullNodeStub) EthSyncing(p0 context.Context) (ethtypes.EthSyncingResult return *new(ethtypes.EthSyncingResult), ErrNotSupported } +func (s *FullNodeStruct) EthTraceBlock(p0 context.Context, p1 string) ([]*ethtypes.EthTraceBlock, error) { + if s.Internal.EthTraceBlock == nil { + return *new([]*ethtypes.EthTraceBlock), ErrNotSupported + } + return s.Internal.EthTraceBlock(p0, p1) +} + +func (s *FullNodeStub) EthTraceBlock(p0 context.Context, p1 string) ([]*ethtypes.EthTraceBlock, error) { + return *new([]*ethtypes.EthTraceBlock), ErrNotSupported +} + +func (s *FullNodeStruct) EthTraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { + if s.Internal.EthTraceReplayBlockTransactions == nil { + return *new([]*ethtypes.EthTraceReplayBlockTransaction), ErrNotSupported + } + return s.Internal.EthTraceReplayBlockTransactions(p0, p1, p2) +} + +func (s *FullNodeStub) EthTraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { + return *new([]*ethtypes.EthTraceReplayBlockTransaction), ErrNotSupported +} + func (s *FullNodeStruct) EthUninstallFilter(p0 context.Context, p1 ethtypes.EthFilterID) (bool, error) { if s.Internal.EthUninstallFilter == nil { return false, ErrNotSupported @@ -4005,28 +4027,6 @@ func (s *FullNodeStub) SyncValidateTipset(p0 context.Context, p1 types.TipSetKey return false, ErrNotSupported } -func (s *FullNodeStruct) TraceBlock(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) { - if s.Internal.TraceBlock == nil { - return *new([]*ethtypes.TraceBlock), ErrNotSupported - } - return s.Internal.TraceBlock(p0, p1) -} - -func (s *FullNodeStub) TraceBlock(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) { - return *new([]*ethtypes.TraceBlock), ErrNotSupported -} - -func (s *FullNodeStruct) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { - if s.Internal.TraceReplayBlockTransactions == nil { - return *new([]*ethtypes.TraceReplayBlockTransaction), ErrNotSupported - } - return s.Internal.TraceReplayBlockTransactions(p0, p1, p2) -} - -func (s *FullNodeStub) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { - return *new([]*ethtypes.TraceReplayBlockTransaction), ErrNotSupported -} - func (s *FullNodeStruct) WalletBalance(p0 context.Context, p1 address.Address) (types.BigInt, error) { if s.Internal.WalletBalance == nil { return *new(types.BigInt), ErrNotSupported @@ -4709,6 +4709,28 @@ func (s *GatewayStub) EthSyncing(p0 context.Context) (ethtypes.EthSyncingResult, return *new(ethtypes.EthSyncingResult), ErrNotSupported } +func (s *GatewayStruct) EthTraceBlock(p0 context.Context, p1 string) ([]*ethtypes.EthTraceBlock, error) { + if s.Internal.EthTraceBlock == nil { + return *new([]*ethtypes.EthTraceBlock), ErrNotSupported + } + return s.Internal.EthTraceBlock(p0, p1) +} + +func (s *GatewayStub) EthTraceBlock(p0 context.Context, p1 string) ([]*ethtypes.EthTraceBlock, error) { + return *new([]*ethtypes.EthTraceBlock), ErrNotSupported +} + +func (s *GatewayStruct) EthTraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { + if s.Internal.EthTraceReplayBlockTransactions == nil { + return *new([]*ethtypes.EthTraceReplayBlockTransaction), ErrNotSupported + } + return s.Internal.EthTraceReplayBlockTransactions(p0, p1, p2) +} + +func (s *GatewayStub) EthTraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { + return *new([]*ethtypes.EthTraceReplayBlockTransaction), ErrNotSupported +} + func (s *GatewayStruct) EthUninstallFilter(p0 context.Context, p1 ethtypes.EthFilterID) (bool, error) { if s.Internal.EthUninstallFilter == nil { return false, ErrNotSupported @@ -5160,28 +5182,6 @@ func (s *GatewayStub) StateWaitMsg(p0 context.Context, p1 cid.Cid, p2 uint64, p3 return nil, ErrNotSupported } -func (s *GatewayStruct) TraceBlock(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) { - if s.Internal.TraceBlock == nil { - return *new([]*ethtypes.TraceBlock), ErrNotSupported - } - return s.Internal.TraceBlock(p0, p1) -} - -func (s *GatewayStub) TraceBlock(p0 context.Context, p1 string) ([]*ethtypes.TraceBlock, error) { - return *new([]*ethtypes.TraceBlock), ErrNotSupported -} - -func (s *GatewayStruct) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { - if s.Internal.TraceReplayBlockTransactions == nil { - return *new([]*ethtypes.TraceReplayBlockTransaction), ErrNotSupported - } - return s.Internal.TraceReplayBlockTransactions(p0, p1, p2) -} - -func (s *GatewayStub) TraceReplayBlockTransactions(p0 context.Context, p1 string, p2 []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { - return *new([]*ethtypes.TraceReplayBlockTransaction), ErrNotSupported -} - func (s *GatewayStruct) Version(p0 context.Context) (APIVersion, error) { if s.Internal.Version == nil { return *new(APIVersion), ErrNotSupported diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index 3e0dd872467..b04d01b69c2 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -18,6 +18,7 @@ import ( "golang.org/x/xerrors" "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" builtintypes "github.com/filecoin-project/go-state-types/builtin" @@ -929,3 +930,51 @@ func (e *EthBlockNumberOrHash) UnmarshalJSON(b []byte) error { return errors.New("invalid block param") } + +type EthTrace struct { + Action EthTraceAction `json:"action"` + Result EthTraceResult `json:"result"` + Subtraces int `json:"subtraces"` + TraceAddress []int `json:"traceAddress"` + Type string `json:"Type"` + + Parent *EthTrace `json:"-"` +} + +func (t *EthTrace) SetCallType(callType string) { + t.Action.CallType = callType + t.Type = callType +} + +type EthTraceBlock struct { + *EthTrace + BlockHash EthHash `json:"blockHash"` + BlockNumber int64 `json:"blockNumber"` + TransactionHash EthHash `json:"transactionHash"` + TransactionPosition int `json:"transactionPosition"` +} + +type EthTraceReplayBlockTransaction struct { + Output string `json:"output"` + StateDiff *string `json:"stateDiff"` + Trace []*EthTrace `json:"trace"` + TransactionHash EthHash `json:"transactionHash"` + VmTrace *string `json:"vmTrace"` +} + +type EthTraceAction struct { + CallType string `json:"callType"` + From string `json:"from"` + To string `json:"to"` + Gas EthUint64 `json:"gas"` + Input string `json:"input"` + Value EthBigInt `json:"value"` + + Method abi.MethodNum `json:"-"` + CodeCid cid.Cid `json:"-"` +} + +type EthTraceResult struct { + GasUsed EthUint64 `json:"gasUsed"` + Output string `json:"output"` +} diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index a488ee64bd1..b28e5d5efbf 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -104,6 +104,8 @@ * [EthSendRawTransaction](#EthSendRawTransaction) * [EthSubscribe](#EthSubscribe) * [EthSyncing](#EthSyncing) + * [EthTraceBlock](#EthTraceBlock) + * [EthTraceReplayBlockTransactions](#EthTraceReplayBlockTransactions) * [EthUninstallFilter](#EthUninstallFilter) * [EthUnsubscribe](#EthUnsubscribe) * [Filecoin](#Filecoin) @@ -287,9 +289,6 @@ * [SyncUnmarkAllBad](#SyncUnmarkAllBad) * [SyncUnmarkBad](#SyncUnmarkBad) * [SyncValidateTipset](#SyncValidateTipset) -* [Trace](#Trace) - * [TraceBlock](#TraceBlock) - * [TraceReplayBlockTransactions](#TraceReplayBlockTransactions) * [Wallet](#Wallet) * [WalletBalance](#WalletBalance) * [WalletDefaultAddress](#WalletDefaultAddress) @@ -3086,6 +3085,99 @@ Inputs: `null` Response: `false` +### EthTraceBlock +TraceAPI related methods + +Returns traces created at given block + + +Perms: read + +Inputs: +```json +[ + "string value" +] +``` + +Response: +```json +[ + { + "action": { + "callType": "string value", + "from": "string value", + "to": "string value", + "gas": "0x5", + "input": "string value", + "value": "0x0" + }, + "result": { + "gasUsed": "0x5", + "output": "string value" + }, + "subtraces": 123, + "traceAddress": [ + 123 + ], + "Type": "string value", + "blockHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", + "blockNumber": 9, + "transactionHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", + "transactionPosition": 123 + } +] +``` + +### EthTraceReplayBlockTransactions +Replays all transactions in a block returning the requested traces for each transaction + + +Perms: read + +Inputs: +```json +[ + "string value", + [ + "string value" + ] +] +``` + +Response: +```json +[ + { + "output": "string value", + "stateDiff": "string value", + "trace": [ + { + "action": { + "callType": "string value", + "from": "string value", + "to": "string value", + "gas": "0x5", + "input": "string value", + "value": "0x0" + }, + "result": { + "gasUsed": "0x5", + "output": "string value" + }, + "subtraces": 123, + "traceAddress": [ + 123 + ], + "Type": "string value" + } + ], + "transactionHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", + "vmTrace": "string value" + } +] +``` + ### EthUninstallFilter Uninstalls a filter with given id. @@ -8823,102 +8915,6 @@ Inputs: Response: `true` -## Trace - - -### TraceBlock -TraceAPI related methods - -Returns traces created at given block - - -Perms: read - -Inputs: -```json -[ - "string value" -] -``` - -Response: -```json -[ - { - "action": { - "callType": "string value", - "from": "string value", - "to": "string value", - "gas": "0x5", - "input": "string value", - "value": "0x0" - }, - "result": { - "gasUsed": "0x5", - "output": "string value" - }, - "subtraces": 123, - "traceAddress": [ - 123 - ], - "Type": "string value", - "blockHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", - "blockNumber": 9, - "transactionHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", - "transactionPosition": 123 - } -] -``` - -### TraceReplayBlockTransactions -Replays all transactions in a block returning the requested traces for each transaction - - -Perms: read - -Inputs: -```json -[ - "string value", - [ - "string value" - ] -] -``` - -Response: -```json -[ - { - "output": "string value", - "stateDiff": "string value", - "trace": [ - { - "action": { - "callType": "string value", - "from": "string value", - "to": "string value", - "gas": "0x5", - "input": "string value", - "value": "0x0" - }, - "result": { - "gasUsed": "0x5", - "output": "string value" - }, - "subtraces": 123, - "traceAddress": [ - 123 - ], - "Type": "string value" - } - ], - "transactionHash": "0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e", - "vmTrace": "string value" - } -] -``` - ## Wallet diff --git a/gateway/node.go b/gateway/node.go index 1ed846d1a27..367e645c1f6 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -144,8 +144,8 @@ type TargetAPI interface { EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) Web3ClientVersion(ctx context.Context) (string, error) - TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) + EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) + EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) } var _ TargetAPI = *new(api.FullNode) // gateway depends on latest diff --git a/gateway/proxy_eth.go b/gateway/proxy_eth.go index 42e9d8e4599..e6d433a1744 100644 --- a/gateway/proxy_eth.go +++ b/gateway/proxy_eth.go @@ -582,7 +582,7 @@ func (gw *Node) Web3ClientVersion(ctx context.Context) (string, error) { return gw.target.Web3ClientVersion(ctx) } -func (gw *Node) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) { +func (gw *Node) EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) { if err := gw.limit(ctx, stateRateLimitTokens); err != nil { return nil, err } @@ -591,10 +591,10 @@ func (gw *Node) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.Trac return nil, err } - return gw.target.TraceBlock(ctx, blkNum) + return gw.target.EthTraceBlock(ctx, blkNum) } -func (gw *Node) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { +func (gw *Node) EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { if err := gw.limit(ctx, stateRateLimitTokens); err != nil { return nil, err } @@ -603,7 +603,7 @@ func (gw *Node) TraceReplayBlockTransactions(ctx context.Context, blkNum string, return nil, err } - return gw.target.TraceReplayBlockTransactions(ctx, blkNum, traceTypes) + return gw.target.EthTraceReplayBlockTransactions(ctx, blkNum, traceTypes) } var EthMaxFiltersPerConn = 16 // todo make this configurable diff --git a/node/impl/full/dummy.go b/node/impl/full/dummy.go index 81481ea2c54..743eadf34dd 100644 --- a/node/impl/full/dummy.go +++ b/node/impl/full/dummy.go @@ -178,11 +178,11 @@ func (e *EthModuleDummy) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubs return false, ErrModuleDisabled } -func (e *EthModuleDummy) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) { +func (e *EthModuleDummy) EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) { return nil, ErrModuleDisabled } -func (e *EthModuleDummy) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { +func (e *EthModuleDummy) EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { return nil, ErrModuleDisabled } diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 25796e8640f..0a0a79cd4c4 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -71,8 +71,8 @@ type EthModuleAPI interface { EthMaxPriorityFeePerGas(ctx context.Context) (ethtypes.EthBigInt, error) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) Web3ClientVersion(ctx context.Context) (string, error) - TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) - TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) + EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) + EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) } type EthEventAPI interface { @@ -825,7 +825,7 @@ func (a *EthModule) Web3ClientVersion(ctx context.Context) (string, error) { return build.UserVersion(), nil } -func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.TraceBlock, error) { +func (a *EthModule) EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) { ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, false) if err != nil { return nil, xerrors.Errorf("failed to get tipset: %w", err) @@ -856,7 +856,7 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes. return nil, xerrors.Errorf("failed to parse eth hash from cid: %w", err) } - allTraces := make([]*ethtypes.TraceBlock, 0, len(trace)) + allTraces := make([]*ethtypes.EthTraceBlock, 0, len(trace)) msgIdx := 0 for _, ir := range trace { // ignore messages from system actor @@ -880,16 +880,16 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes. continue } - traces := []*ethtypes.Trace{} - err = ethtypes.BuildTraces(&traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) + traces := []*ethtypes.EthTrace{} + err = buildTraces(ctx, &traces, nil, []int{}, ir.ExecutionTrace, int64(ts.Height()), a.StateAPI) if err != nil { return nil, xerrors.Errorf("failed building traces: %w", err) } - traceBlocks := make([]*ethtypes.TraceBlock, 0, len(traces)) + traceBlocks := make([]*ethtypes.EthTraceBlock, 0, len(traces)) for _, trace := range traces { - traceBlocks = append(traceBlocks, ðtypes.TraceBlock{ - Trace: trace, + traceBlocks = append(traceBlocks, ðtypes.EthTraceBlock{ + EthTrace: trace, BlockHash: blkHash, BlockNumber: int64(ts.Height()), TransactionHash: *txHash, @@ -903,7 +903,7 @@ func (a *EthModule) TraceBlock(ctx context.Context, blkNum string) ([]*ethtypes. return allTraces, nil } -func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.TraceReplayBlockTransaction, error) { +func (a *EthModule) EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { if len(traceTypes) != 1 || traceTypes[0] != "trace" { return nil, fmt.Errorf("only 'trace' is supported") } @@ -918,7 +918,7 @@ func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum str return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) } - allTraces := make([]*ethtypes.TraceReplayBlockTransaction, 0, len(trace)) + allTraces := make([]*ethtypes.EthTraceReplayBlockTransaction, 0, len(trace)) for _, ir := range trace { // ignore messages from system actor if ir.Msg.From == builtinactors.SystemActorAddr { @@ -934,14 +934,14 @@ func (a *EthModule) TraceReplayBlockTransactions(ctx context.Context, blkNum str continue } - t := ethtypes.TraceReplayBlockTransaction{ + t := ethtypes.EthTraceReplayBlockTransaction{ Output: hex.EncodeToString(ir.MsgRct.Return), TransactionHash: *txHash, StateDiff: nil, VmTrace: nil, } - err = ethtypes.BuildTraces(&t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height())) + err = buildTraces(ctx, &t.Trace, nil, []int{}, ir.ExecutionTrace, int64(ts.Height()), a.StateAPI) if err != nil { return nil, xerrors.Errorf("failed building traces: %w", err) } diff --git a/chain/types/ethtypes/eth_trace.go b/node/impl/full/eth_trace.go similarity index 71% rename from chain/types/ethtypes/eth_trace.go rename to node/impl/full/eth_trace.go index 14f7e33328c..ce650b9eca6 100644 --- a/chain/types/ethtypes/eth_trace.go +++ b/node/impl/full/eth_trace.go @@ -1,100 +1,49 @@ -package ethtypes +package full import ( "bytes" + "context" "encoding/binary" "encoding/hex" "fmt" "io" - "github.com/ipfs/go-cid" - logging "github.com/ipfs/go-log/v2" - "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/exitcode" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" ) -var log = logging.Logger("traceapi") - -type Trace struct { - Action Action `json:"action"` - Result Result `json:"result"` - Subtraces int `json:"subtraces"` - TraceAddress []int `json:"traceAddress"` - Type string `json:"Type"` - - parent *Trace -} - -func (t *Trace) setCallType(callType string) { - t.Action.CallType = callType - t.Type = callType -} - -type TraceBlock struct { - *Trace - BlockHash EthHash `json:"blockHash"` - BlockNumber int64 `json:"blockNumber"` - TransactionHash EthHash `json:"transactionHash"` - TransactionPosition int `json:"transactionPosition"` -} - -type TraceReplayBlockTransaction struct { - Output string `json:"output"` - StateDiff *string `json:"stateDiff"` - Trace []*Trace `json:"trace"` - TransactionHash EthHash `json:"transactionHash"` - VmTrace *string `json:"vmTrace"` -} - -type Action struct { - CallType string `json:"callType"` - From string `json:"from"` - To string `json:"to"` - Gas EthUint64 `json:"gas"` - Input string `json:"input"` - Value EthBigInt `json:"value"` - - method abi.MethodNum `json:"-"` - codeCid cid.Cid `json:"-"` -} - -type Result struct { - GasUsed EthUint64 `json:"gasUsed"` - Output string `json:"output"` -} - -// BuildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls -func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.ExecutionTrace, height int64) error { - trace := &Trace{ - Action: Action{ +// buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls +func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *ethtypes.EthTrace, addr []int, et types.ExecutionTrace, height int64, sa StateAPI) error { + trace := ðtypes.EthTrace{ + Action: ethtypes.EthTraceAction{ From: et.Msg.From.String(), To: et.Msg.To.String(), - Gas: EthUint64(et.Msg.GasLimit), + Gas: ethtypes.EthUint64(et.Msg.GasLimit), Input: hex.EncodeToString(et.Msg.Params), - Value: EthBigInt(et.Msg.Value), - method: et.Msg.Method, - codeCid: et.Msg.CodeCid, + Value: ethtypes.EthBigInt(et.Msg.Value), + Method: et.Msg.Method, + CodeCid: et.Msg.CodeCid, }, - Result: Result{ - GasUsed: EthUint64(et.SumGas().TotalGas), + Result: ethtypes.EthTraceResult{ + GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), Output: hex.EncodeToString(et.MsgRct.Return), }, Subtraces: len(et.Subcalls), TraceAddress: addr, - parent: parent, + Parent: parent, } - trace.setCallType("call") + trace.SetCallType("call") // TODO: is it OK to check this here or is this only specific to certain edge case (evm to evm)? if et.Msg.ReadOnly { - trace.setCallType("staticcall") + trace.SetCallType("staticcall") } // there are several edge cases thar require special handling when displaying the traces @@ -104,7 +53,7 @@ func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // When an EVM actor is invoked with a method number above 1023 that's not frc42(InvokeEVM) // then we need to format native calls in a way that makes sense to Ethereum tooling (convert // the input & output to solidity ABI format). - if builtinactors.IsEvmActor(parent.Action.codeCid) && + if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 1023 && et.Msg.Method != builtin.MethodsEVM.InvokeContract { log.Debugf("found Native call! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) @@ -124,10 +73,10 @@ func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // // Actor A calls to the init actor on method 2 and The init actor creates the target actor B then calls it on method 1 if parent.Action.To == builtin.InitActorAddr.String() && - parent.Action.method == builtin.MethodsInit.Exec && + parent.Action.Method == builtin.MethodsInit.Exec && et.Msg.Method == builtin.MethodConstructor { log.Debugf("Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - parent.setCallType("create") + parent.SetCallType("create") parent.Action.To = et.Msg.To.String() parent.Action.Input = hex.EncodeToString([]byte{0x0, 0x0, 0x0, 0xFE}) parent.Result.Output = "" @@ -143,25 +92,25 @@ func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // 1) EVM contract A calls the EAM (Ethereum Address Manager) on method 2 (create) or 3 (create2). // 2) The EAM calls the init actor on method 3 (Exec4). // 3) The init actor creates the target actor B then calls it on method 1. - if parent.parent != nil { - calledCreateOnEAM := parent.parent.Action.To == builtin.EthereumAddressManagerActorAddr.String() && - (parent.parent.Action.method == builtin.MethodsEAM.Create || parent.parent.Action.method == builtin.MethodsEAM.Create2) + if parent.Parent != nil { + calledCreateOnEAM := parent.Parent.Action.To == builtin.EthereumAddressManagerActorAddr.String() && + (parent.Parent.Action.Method == builtin.MethodsEAM.Create || parent.Parent.Action.Method == builtin.MethodsEAM.Create2) eamCalledInitOnExec4 := parent.Action.To == builtin.InitActorAddr.String() && - parent.Action.method == builtin.MethodsInit.Exec4 - initCreatedActor := trace.Action.method == builtin.MethodConstructor + parent.Action.Method == builtin.MethodsInit.Exec4 + initCreatedActor := trace.Action.Method == builtin.MethodConstructor if calledCreateOnEAM && eamCalledInitOnExec4 && initCreatedActor { log.Debugf("EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - if parent.parent.Action.method == builtin.MethodsEAM.Create { - parent.parent.setCallType("create") + if parent.Parent.Action.Method == builtin.MethodsEAM.Create { + parent.Parent.SetCallType("create") } else { - parent.parent.setCallType("create2") + parent.Parent.SetCallType("create2") } // update the parent.parent to make this - parent.parent.Action.To = trace.Action.To - parent.parent.Subtraces = 0 + parent.Parent.Action.To = trace.Action.To + parent.Parent.Subtraces = 0 // delete the parent (the EAM) and skip the current trace (init) *traces = (*traces)[:len(*traces)-1] @@ -174,17 +123,17 @@ func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions // and should be dropped from the trace. - if builtinactors.IsEvmActor(parent.Action.codeCid) && + if builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method > 0 && et.Msg.Method <= 1023 { - log.Debugf("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.codeCid.String(), height) + log.Debugf("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.CodeCid.String(), height) // TODO: if I handle this case and drop this call from the trace then I am not able to detect delegate calls } // EVM -> EVM calls // // Check for normal EVM to EVM calls and decode the params and return values - if builtinactors.IsEvmActor(parent.Action.codeCid) && + if builtinactors.IsEvmActor(parent.Action.CodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin.MethodsEVM.InvokeContract { log.Debugf("found evm to evm call, code:%s, height: %d", et.Msg.CodeCid.String(), height) @@ -206,12 +155,12 @@ func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution // 2) Check that the previous trace calls another actor on method 3 (GetByteCode) and they are at the same level (same parent) // 3) Treat this as a delegate call to actor A. if trace.Action.From == trace.Action.To && - trace.Action.method == builtin.MethodsEVM.InvokeContractDelegate && + trace.Action.Method == builtin.MethodsEVM.InvokeContractDelegate && len(*traces) > 0 { log.Debugf("found delegate call, height: %d", height) prev := (*traces)[len(*traces)-1] - if prev.Action.From == trace.Action.From && prev.Action.method == builtin.MethodsEVM.GetBytecode && prev.parent == trace.parent { - trace.setCallType("delegatecall") + if prev.Action.From == trace.Action.From && prev.Action.Method == builtin.MethodsEVM.GetBytecode && prev.Parent == trace.Parent { + trace.SetCallType("delegatecall") trace.Action.To = prev.Action.To } } @@ -220,7 +169,7 @@ func BuildTraces(traces *[]*Trace, parent *Trace, addr []int, et types.Execution *traces = append(*traces, trace) for i, call := range et.Subcalls { - err := BuildTraces(traces, trace, append(addr, i), call, height) + err := buildTraces(ctx, traces, trace, append(addr, i), call, height, sa) if err != nil { return err } From cb5e6e0cd15ba4a50c8296544ecb75604479e20e Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Wed, 23 Aug 2023 14:25:52 +0000 Subject: [PATCH 14/25] The From/To address should be in eth format --- node/impl/full/eth_trace.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/node/impl/full/eth_trace.go b/node/impl/full/eth_trace.go index ce650b9eca6..7bfa5a6c761 100644 --- a/node/impl/full/eth_trace.go +++ b/node/impl/full/eth_trace.go @@ -11,6 +11,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/exitcode" + "golang.org/x/xerrors" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/types" @@ -19,10 +20,19 @@ import ( // buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *ethtypes.EthTrace, addr []int, et types.ExecutionTrace, height int64, sa StateAPI) error { + from, err := lookupEthAddress(ctx, et.Msg.From, sa) + if err != nil { + return xerrors.Errorf("buildTraces: failed to lookup from address %s: %w", et.Msg.From, err) + } + to, err := lookupEthAddress(ctx, et.Msg.To, sa) + if err != nil { + return xerrors.Errorf("buildTraces: failed to lookup to address %s: %w", et.Msg.To, err) + } + trace := ðtypes.EthTrace{ Action: ethtypes.EthTraceAction{ - From: et.Msg.From.String(), - To: et.Msg.To.String(), + From: from.String(), + To: to.String(), Gas: ethtypes.EthUint64(et.Msg.GasLimit), Input: hex.EncodeToString(et.Msg.Params), Value: ethtypes.EthBigInt(et.Msg.Value), From cf255127a4f70b1d66a6cf9b5e8e5a7ffd65ebf1 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Thu, 24 Aug 2023 20:05:11 +0000 Subject: [PATCH 15/25] Decode eth param/return values and change them to ethbytes type --- build/openrpc/full.json.gz | Bin 34113 -> 34826 bytes build/openrpc/gateway.json.gz | Bin 11393 -> 11875 bytes build/openrpc/miner.json.gz | Bin 15938 -> 15941 bytes build/openrpc/worker.json.gz | Bin 5245 -> 5243 bytes chain/types/ethtypes/eth_types.go | 4 +- documentation/en/api-v1-unstable-methods.md | 8 +- node/impl/full/eth_trace.go | 113 ++++++++++++++------ 7 files changed, 88 insertions(+), 37 deletions(-) diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index ee1cb638e43d99984bb6b0c1afac78ad1ca6f6d0..6d76e42148126af70189b51fd4b45965fd2fa867 100644 GIT binary patch delta 33995 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 D#^JGe delta 33283 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 diff --git a/build/openrpc/gateway.json.gz b/build/openrpc/gateway.json.gz index 10549b82806cd0a60706c20c5d754c3af304acbc..052d47e574368866dd6f72e40a98c94182d778f7 100644 GIT binary patch literal 11875 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 PpPxJW{Bp5a5TO4D1lw|L literal 11393 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 MLl|_QD;UWC0-(@e4*&oF diff --git a/build/openrpc/miner.json.gz b/build/openrpc/miner.json.gz index 89aca93cbdad98817716233db176bb2ab2ff8569..b3123682dfa26606f79c5ccad1399624122e92f3 100644 GIT binary patch literal 15941 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�sn$4nW^Yk5_P28GX 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 T{RIF4{r*N?V#5)jg980O_cqlF literal 15938 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

R2BShU$0_6E6Dr`GiaQ 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 GE&%{$oTAMD delta 4470 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 ITdghu06Ai+D*ylh diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index b04d01b69c2..99ace8d39bb 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -967,7 +967,7 @@ type EthTraceAction struct { From string `json:"from"` To string `json:"to"` Gas EthUint64 `json:"gas"` - Input string `json:"input"` + Input EthBytes `json:"input"` Value EthBigInt `json:"value"` Method abi.MethodNum `json:"-"` @@ -976,5 +976,5 @@ type EthTraceAction struct { type EthTraceResult struct { GasUsed EthUint64 `json:"gasUsed"` - Output string `json:"output"` + Output EthBytes `json:"output"` } diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index b28e5d5efbf..beb8f5d9dc3 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -3109,12 +3109,12 @@ Response: "from": "string value", "to": "string value", "gas": "0x5", - "input": "string value", + "input": "0x07", "value": "0x0" }, "result": { "gasUsed": "0x5", - "output": "string value" + "output": "0x07" }, "subtraces": 123, "traceAddress": [ @@ -3158,12 +3158,12 @@ Response: "from": "string value", "to": "string value", "gas": "0x5", - "input": "string value", + "input": "0x07", "value": "0x0" }, "result": { "gasUsed": "0x5", - "output": "string value" + "output": "0x07" }, "subtraces": 123, "traceAddress": [ diff --git a/node/impl/full/eth_trace.go b/node/impl/full/eth_trace.go index 7bfa5a6c761..795fae7d6d3 100644 --- a/node/impl/full/eth_trace.go +++ b/node/impl/full/eth_trace.go @@ -4,29 +4,59 @@ import ( "bytes" "context" "encoding/binary" - "encoding/hex" "fmt" "io" + "github.com/multiformats/go-multicodec" + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/xerrors" + "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/exitcode" - "golang.org/x/xerrors" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" ) +func decodePayload(payload []byte, codec uint64) (ethtypes.EthBytes, error) { + if len(payload) == 0 { + return ethtypes.EthBytes(nil), nil + } + + switch multicodec.Code(codec) { + case multicodec.Identity: + return ethtypes.EthBytes(nil), nil + case multicodec.DagCbor, multicodec.Cbor: + buf, err := cbg.ReadByteArray(bytes.NewReader(payload), uint64(len(payload))) + if err != nil { + return nil, xerrors.Errorf("decodePayload: failed to decode cbor payload: %w", err) + } + return buf, nil + case multicodec.Raw: + return ethtypes.EthBytes(payload), nil + } + + return nil, xerrors.Errorf("decodePayload: unsupported codec: %d", codec) +} + // buildTraces recursively builds the traces for a given ExecutionTrace by walking the subcalls func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *ethtypes.EthTrace, addr []int, et types.ExecutionTrace, height int64, sa StateAPI) error { + // lookup the eth address from the from/to addresses. Note that this may fail but to support + // this we need to include the ActorID in the trace. For now, just log a warning and skip + // this trace. + // + // TODO: Add ActorID in trace, see https://github.com/filecoin-project/lotus/pull/11100#discussion_r1302442288 from, err := lookupEthAddress(ctx, et.Msg.From, sa) if err != nil { - return xerrors.Errorf("buildTraces: failed to lookup from address %s: %w", et.Msg.From, err) + log.Warnf("buildTraces: failed to lookup from address %s: %v", et.Msg.From, err) + return nil } to, err := lookupEthAddress(ctx, et.Msg.To, sa) if err != nil { - return xerrors.Errorf("buildTraces: failed to lookup to address %s: %w", et.Msg.To, err) + log.Warnf("buildTraces: failed to lookup to address %s: %w", et.Msg.To, err) + return nil } trace := ðtypes.EthTrace{ @@ -34,14 +64,14 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht From: from.String(), To: to.String(), Gas: ethtypes.EthUint64(et.Msg.GasLimit), - Input: hex.EncodeToString(et.Msg.Params), + Input: nil, Value: ethtypes.EthBigInt(et.Msg.Value), Method: et.Msg.Method, CodeCid: et.Msg.CodeCid, }, Result: ethtypes.EthTraceResult{ GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), - Output: hex.EncodeToString(et.MsgRct.Return), + Output: nil, }, Subtraces: len(et.Subcalls), TraceAddress: addr, @@ -51,6 +81,48 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht trace.SetCallType("call") + if parent != nil && builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method == builtin.MethodsEVM.InvokeContract { + log.Debugf("found InvokeContract call at height: %d", height) + trace.Action.Input, err = decodePayload(et.Msg.Params, et.Msg.ParamsCodec) + if err != nil { + return xerrors.Errorf("buildTraces: %w", err) + } + trace.Result.Output, err = decodePayload(et.MsgRct.Return, et.MsgRct.ReturnCodec) + if err != nil { + return xerrors.Errorf("buildTraces: %w", err) + } + } else if et.Msg.To == builtin.EthereumAddressManagerActorAddr && + et.Msg.Method == builtin.MethodsEAM.CreateExternal { + log.Debugf("found CreateExternal call at height: %d", height) + trace.Action.Input, err = decodePayload(et.Msg.Params, et.Msg.ParamsCodec) + if err != nil { + return xerrors.Errorf("buildTraces: %w", err) + } + + if et.MsgRct.ExitCode.IsSuccess() { + // ignore return value + trace.Result.Output = nil + } else { + // return value is the error message + trace.Result.Output, err = decodePayload(et.MsgRct.Return, et.MsgRct.ReturnCodec) + if err != nil { + return xerrors.Errorf("buildTraces: %w", err) + } + } + + // treat this as a contract creation + trace.SetCallType("create") + } else { + trace.Action.Input, err = handleFilecoinMethodInput(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) + if err != nil { + return xerrors.Errorf("buildTraces: %w", err) + } + trace.Result.Output, err = handleFilecoinMethodOutput(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) + if err != nil { + return xerrors.Errorf("buildTraces: %w", err) + } + } + // TODO: is it OK to check this here or is this only specific to certain edge case (evm to evm)? if et.Msg.ReadOnly { trace.SetCallType("staticcall") @@ -58,27 +130,6 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht // there are several edge cases thar require special handling when displaying the traces if parent != nil { - // Handle Native calls - // - // When an EVM actor is invoked with a method number above 1023 that's not frc42(InvokeEVM) - // then we need to format native calls in a way that makes sense to Ethereum tooling (convert - // the input & output to solidity ABI format). - if builtinactors.IsEvmActor(parent.Action.CodeCid) && - et.Msg.Method > 1023 && - et.Msg.Method != builtin.MethodsEVM.InvokeContract { - log.Debugf("found Native call! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - input, err := handleFilecoinMethodInput(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) - if err != nil { - return err - } - trace.Action.Input = hex.EncodeToString(input) - output, err := handleFilecoinMethodOutput(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) - if err != nil { - return err - } - trace.Result.Output = hex.EncodeToString(output) - } - // Handle Native actor creation // // Actor A calls to the init actor on method 2 and The init actor creates the target actor B then calls it on method 1 @@ -88,8 +139,8 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht log.Debugf("Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) parent.SetCallType("create") parent.Action.To = et.Msg.To.String() - parent.Action.Input = hex.EncodeToString([]byte{0x0, 0x0, 0x0, 0xFE}) - parent.Result.Output = "" + parent.Action.Input = []byte{0x0, 0x0, 0x0, 0xFE} + parent.Result.Output = nil // there should never be any subcalls when creating a native actor return nil @@ -151,12 +202,12 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht if err != nil { return err } - trace.Action.Input = hex.EncodeToString(input) + trace.Action.Input = input output, err := handleFilecoinMethodOutput(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) if err != nil { return err } - trace.Result.Output = hex.EncodeToString(output) + trace.Result.Output = output } // Handle delegate calls From ee3cdf0e974d4e6c9a06c79c83440f9e4bf262ac Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Fri, 25 Aug 2023 13:15:34 +0000 Subject: [PATCH 16/25] Fix use filecoin addr + other small refactor After changing in prev commit to use to ethereum addresses the comparison does not make sense against builtin actors. This fixes that by storing also the filecoin addresses in each trace Also renamed filecoin related fields to Filecoin prefix. Also remove requirement call to InvokeContract needed to come from a evm actor --- chain/types/ethtypes/eth_types.go | 8 ++++-- node/impl/full/eth.go | 3 +- node/impl/full/eth_trace.go | 48 +++++++++++++++++-------------- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index 99ace8d39bb..d74fb15925d 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -955,7 +955,7 @@ type EthTraceBlock struct { } type EthTraceReplayBlockTransaction struct { - Output string `json:"output"` + Output EthBytes `json:"output"` StateDiff *string `json:"stateDiff"` Trace []*EthTrace `json:"trace"` TransactionHash EthHash `json:"transactionHash"` @@ -970,8 +970,10 @@ type EthTraceAction struct { Input EthBytes `json:"input"` Value EthBigInt `json:"value"` - Method abi.MethodNum `json:"-"` - CodeCid cid.Cid `json:"-"` + FilecoinMethod abi.MethodNum `json:"-"` + FilecoinCodeCid cid.Cid `json:"-"` + FilecoinFrom address.Address `json:"-"` + FilecoinTo address.Address `json:"-"` } type EthTraceResult struct { diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 0a0a79cd4c4..999b200f733 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -3,7 +3,6 @@ package full import ( "bytes" "context" - "encoding/hex" "errors" "fmt" "os" @@ -935,7 +934,7 @@ func (a *EthModule) EthTraceReplayBlockTransactions(ctx context.Context, blkNum } t := ethtypes.EthTraceReplayBlockTransaction{ - Output: hex.EncodeToString(ir.MsgRct.Return), + Output: ir.MsgRct.Return, TransactionHash: *txHash, StateDiff: nil, VmTrace: nil, diff --git a/node/impl/full/eth_trace.go b/node/impl/full/eth_trace.go index 795fae7d6d3..743d1f949ca 100644 --- a/node/impl/full/eth_trace.go +++ b/node/impl/full/eth_trace.go @@ -61,13 +61,16 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht trace := ðtypes.EthTrace{ Action: ethtypes.EthTraceAction{ - From: from.String(), - To: to.String(), - Gas: ethtypes.EthUint64(et.Msg.GasLimit), - Input: nil, - Value: ethtypes.EthBigInt(et.Msg.Value), - Method: et.Msg.Method, - CodeCid: et.Msg.CodeCid, + From: from.String(), + To: to.String(), + Gas: ethtypes.EthUint64(et.Msg.GasLimit), + Input: nil, + Value: ethtypes.EthBigInt(et.Msg.Value), + + FilecoinFrom: et.Msg.From, + FilecoinTo: et.Msg.To, + FilecoinMethod: et.Msg.Method, + FilecoinCodeCid: et.Msg.CodeCid, }, Result: ethtypes.EthTraceResult{ GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), @@ -81,8 +84,11 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht trace.SetCallType("call") - if parent != nil && builtinactors.IsEvmActor(parent.Action.CodeCid) && et.Msg.Method == builtin.MethodsEVM.InvokeContract { + if et.Msg.Method == builtin.MethodsEVM.InvokeContract { log.Debugf("found InvokeContract call at height: %d", height) + + // TODO: ignore return errors since actors can send gibberish and we don't want + // to fail the whole trace in that case trace.Action.Input, err = decodePayload(et.Msg.Params, et.Msg.ParamsCodec) if err != nil { return xerrors.Errorf("buildTraces: %w", err) @@ -133,8 +139,8 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht // Handle Native actor creation // // Actor A calls to the init actor on method 2 and The init actor creates the target actor B then calls it on method 1 - if parent.Action.To == builtin.InitActorAddr.String() && - parent.Action.Method == builtin.MethodsInit.Exec && + if parent.Action.FilecoinTo == builtin.InitActorAddr && + parent.Action.FilecoinMethod == builtin.MethodsInit.Exec && et.Msg.Method == builtin.MethodConstructor { log.Debugf("Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) parent.SetCallType("create") @@ -154,16 +160,16 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht // 2) The EAM calls the init actor on method 3 (Exec4). // 3) The init actor creates the target actor B then calls it on method 1. if parent.Parent != nil { - calledCreateOnEAM := parent.Parent.Action.To == builtin.EthereumAddressManagerActorAddr.String() && - (parent.Parent.Action.Method == builtin.MethodsEAM.Create || parent.Parent.Action.Method == builtin.MethodsEAM.Create2) - eamCalledInitOnExec4 := parent.Action.To == builtin.InitActorAddr.String() && - parent.Action.Method == builtin.MethodsInit.Exec4 - initCreatedActor := trace.Action.Method == builtin.MethodConstructor + calledCreateOnEAM := parent.Parent.Action.FilecoinTo == builtin.EthereumAddressManagerActorAddr && + (parent.Parent.Action.FilecoinMethod == builtin.MethodsEAM.Create || parent.Parent.Action.FilecoinMethod == builtin.MethodsEAM.Create2) + eamCalledInitOnExec4 := parent.Action.FilecoinTo == builtin.InitActorAddr && + parent.Action.FilecoinMethod == builtin.MethodsInit.Exec4 + initCreatedActor := trace.Action.FilecoinMethod == builtin.MethodConstructor if calledCreateOnEAM && eamCalledInitOnExec4 && initCreatedActor { log.Debugf("EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) - if parent.Parent.Action.Method == builtin.MethodsEAM.Create { + if parent.Parent.Action.FilecoinMethod == builtin.MethodsEAM.Create { parent.Parent.SetCallType("create") } else { parent.Parent.SetCallType("create2") @@ -184,17 +190,17 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht // // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions // and should be dropped from the trace. - if builtinactors.IsEvmActor(parent.Action.CodeCid) && + if builtinactors.IsEvmActor(parent.Action.FilecoinCodeCid) && et.Msg.Method > 0 && et.Msg.Method <= 1023 { - log.Debugf("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.CodeCid.String(), height) + log.Debugf("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.FilecoinCodeCid.String(), height) // TODO: if I handle this case and drop this call from the trace then I am not able to detect delegate calls } // EVM -> EVM calls // // Check for normal EVM to EVM calls and decode the params and return values - if builtinactors.IsEvmActor(parent.Action.CodeCid) && + if builtinactors.IsEvmActor(parent.Action.FilecoinCodeCid) && builtinactors.IsEthAccountActor(et.Msg.CodeCid) && et.Msg.Method == builtin.MethodsEVM.InvokeContract { log.Debugf("found evm to evm call, code:%s, height: %d", et.Msg.CodeCid.String(), height) @@ -216,11 +222,11 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht // 2) Check that the previous trace calls another actor on method 3 (GetByteCode) and they are at the same level (same parent) // 3) Treat this as a delegate call to actor A. if trace.Action.From == trace.Action.To && - trace.Action.Method == builtin.MethodsEVM.InvokeContractDelegate && + trace.Action.FilecoinMethod == builtin.MethodsEVM.InvokeContractDelegate && len(*traces) > 0 { log.Debugf("found delegate call, height: %d", height) prev := (*traces)[len(*traces)-1] - if prev.Action.From == trace.Action.From && prev.Action.Method == builtin.MethodsEVM.GetBytecode && prev.Parent == trace.Parent { + if prev.Action.From == trace.Action.From && prev.Action.FilecoinMethod == builtin.MethodsEVM.GetBytecode && prev.Parent == trace.Parent { trace.SetCallType("delegatecall") trace.Action.To = prev.Action.To } From 10a54808957786a3ebdd075a32e5e50066edde8d Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Fri, 25 Aug 2023 15:52:38 +0000 Subject: [PATCH 17/25] Decode output using top level trace --- node/impl/full/eth.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 999b200f733..7510856e721 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -933,8 +933,13 @@ func (a *EthModule) EthTraceReplayBlockTransactions(ctx context.Context, blkNum continue } + output, err := decodePayload(ir.ExecutionTrace.MsgRct.Return, ir.ExecutionTrace.MsgRct.ReturnCodec) + if err != nil { + return nil, xerrors.Errorf("failed to decode payload: %w", err) + } + t := ethtypes.EthTraceReplayBlockTransaction{ - Output: ir.MsgRct.Return, + Output: output, TransactionHash: *txHash, StateDiff: nil, VmTrace: nil, From 029a4a72b887144fb59f96ea6574d3f75298cb74 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Fri, 25 Aug 2023 18:21:48 +0000 Subject: [PATCH 18/25] Address most recent comments --- chain/types/ethtypes/eth_types.go | 15 ++--- node/impl/full/eth_trace.go | 93 +++++++++++++++---------------- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index d74fb15925d..786d7d57093 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -938,7 +938,8 @@ type EthTrace struct { TraceAddress []int `json:"traceAddress"` Type string `json:"Type"` - Parent *EthTrace `json:"-"` + Parent *EthTrace `json:"-"` + LastByteCode *EthTrace `json:"-"` } func (t *EthTrace) SetCallType(callType string) { @@ -963,12 +964,12 @@ type EthTraceReplayBlockTransaction struct { } type EthTraceAction struct { - CallType string `json:"callType"` - From string `json:"from"` - To string `json:"to"` - Gas EthUint64 `json:"gas"` - Input EthBytes `json:"input"` - Value EthBigInt `json:"value"` + CallType string `json:"callType"` + From EthAddress `json:"from"` + To EthAddress `json:"to"` + Gas EthUint64 `json:"gas"` + Input EthBytes `json:"input"` + Value EthBigInt `json:"value"` FilecoinMethod abi.MethodNum `json:"-"` FilecoinCodeCid cid.Cid `json:"-"` diff --git a/node/impl/full/eth_trace.go b/node/impl/full/eth_trace.go index 743d1f949ca..5b72828f452 100644 --- a/node/impl/full/eth_trace.go +++ b/node/impl/full/eth_trace.go @@ -20,14 +20,15 @@ import ( "github.com/filecoin-project/lotus/chain/types/ethtypes" ) +// decodePayload is a utility function which decodes the payload using the given codec func decodePayload(payload []byte, codec uint64) (ethtypes.EthBytes, error) { if len(payload) == 0 { - return ethtypes.EthBytes(nil), nil + return nil, nil } switch multicodec.Code(codec) { case multicodec.Identity: - return ethtypes.EthBytes(nil), nil + return nil, nil case multicodec.DagCbor, multicodec.Cbor: buf, err := cbg.ReadByteArray(bytes.NewReader(payload), uint64(len(payload))) if err != nil { @@ -61,8 +62,8 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht trace := ðtypes.EthTrace{ Action: ethtypes.EthTraceAction{ - From: from.String(), - To: to.String(), + From: from, + To: to, Gas: ethtypes.EthUint64(et.Msg.GasLimit), Input: nil, Value: ethtypes.EthBigInt(et.Msg.Value), @@ -76,16 +77,17 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht GasUsed: ethtypes.EthUint64(et.SumGas().TotalGas), Output: nil, }, - Subtraces: len(et.Subcalls), + Subtraces: 0, // will be updated by the children once they are added to the trace TraceAddress: addr, - Parent: parent, + Parent: parent, + LastByteCode: nil, } trace.SetCallType("call") if et.Msg.Method == builtin.MethodsEVM.InvokeContract { - log.Debugf("found InvokeContract call at height: %d", height) + log.Debugf("COND1 found InvokeContract call at height: %d", height) // TODO: ignore return errors since actors can send gibberish and we don't want // to fail the whole trace in that case @@ -99,7 +101,7 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht } } else if et.Msg.To == builtin.EthereumAddressManagerActorAddr && et.Msg.Method == builtin.MethodsEAM.CreateExternal { - log.Debugf("found CreateExternal call at height: %d", height) + log.Debugf("COND2 found CreateExternal call at height: %d", height) trace.Action.Input, err = decodePayload(et.Msg.Params, et.Msg.ParamsCodec) if err != nil { return xerrors.Errorf("buildTraces: %w", err) @@ -119,6 +121,8 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht // treat this as a contract creation trace.SetCallType("create") } else { + // we are going to assume a native method, but we may change it in one of the edge cases below + // TODO: only do this if we know it's a native method (optimization) trace.Action.Input, err = handleFilecoinMethodInput(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) if err != nil { return xerrors.Errorf("buildTraces: %w", err) @@ -134,7 +138,8 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht trace.SetCallType("staticcall") } - // there are several edge cases thar require special handling when displaying the traces + // there are several edge cases that require special handling when displaying the traces. Note that while iterating over + // the traces we update the trace backwards (through the parent pointer) if parent != nil { // Handle Native actor creation // @@ -142,10 +147,10 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht if parent.Action.FilecoinTo == builtin.InitActorAddr && parent.Action.FilecoinMethod == builtin.MethodsInit.Exec && et.Msg.Method == builtin.MethodConstructor { - log.Debugf("Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + log.Debugf("COND3 Native actor creation! method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) parent.SetCallType("create") - parent.Action.To = et.Msg.To.String() - parent.Action.Input = []byte{0x0, 0x0, 0x0, 0xFE} + parent.Action.To = to + parent.Action.Input = []byte{0xFE} parent.Result.Output = nil // there should never be any subcalls when creating a native actor @@ -166,8 +171,9 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht parent.Action.FilecoinMethod == builtin.MethodsInit.Exec4 initCreatedActor := trace.Action.FilecoinMethod == builtin.MethodConstructor + // TODO: We need to handle failures in contract creations and support resurrections on an existing but dead EVM actor) if calledCreateOnEAM && eamCalledInitOnExec4 && initCreatedActor { - log.Debugf("EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) + log.Debugf("COND4 EVM contract creation method:%d, code:%s, height:%d", et.Msg.Method, et.Msg.CodeCid.String(), height) if parent.Parent.Action.FilecoinMethod == builtin.MethodsEAM.Create { parent.Parent.SetCallType("create") @@ -186,53 +192,44 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht } } - // Handle EVM call special casing - // - // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions - // and should be dropped from the trace. - if builtinactors.IsEvmActor(parent.Action.FilecoinCodeCid) && - et.Msg.Method > 0 && - et.Msg.Method <= 1023 { - log.Debugf("found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.FilecoinCodeCid.String(), height) - // TODO: if I handle this case and drop this call from the trace then I am not able to detect delegate calls - } - - // EVM -> EVM calls - // - // Check for normal EVM to EVM calls and decode the params and return values - if builtinactors.IsEvmActor(parent.Action.FilecoinCodeCid) && - builtinactors.IsEthAccountActor(et.Msg.CodeCid) && - et.Msg.Method == builtin.MethodsEVM.InvokeContract { - log.Debugf("found evm to evm call, code:%s, height: %d", et.Msg.CodeCid.String(), height) - input, err := handleFilecoinMethodInput(et.Msg.Method, et.Msg.ParamsCodec, et.Msg.Params) - if err != nil { - return err - } - trace.Action.Input = input - output, err := handleFilecoinMethodOutput(et.MsgRct.ExitCode, et.MsgRct.ReturnCodec, et.MsgRct.Return) - if err != nil { - return err - } - trace.Result.Output = output - } - // Handle delegate calls // // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate, method 6. // 2) Check that the previous trace calls another actor on method 3 (GetByteCode) and they are at the same level (same parent) // 3) Treat this as a delegate call to actor A. - if trace.Action.From == trace.Action.To && - trace.Action.FilecoinMethod == builtin.MethodsEVM.InvokeContractDelegate && - len(*traces) > 0 { - log.Debugf("found delegate call, height: %d", height) - prev := (*traces)[len(*traces)-1] + if parent.LastByteCode != nil && trace.Action.From == trace.Action.To && + trace.Action.FilecoinMethod == builtin.MethodsEVM.InvokeContractDelegate { + log.Debugf("COND7 found delegate call, height: %d", height) + prev := parent.LastByteCode if prev.Action.From == trace.Action.From && prev.Action.FilecoinMethod == builtin.MethodsEVM.GetBytecode && prev.Parent == trace.Parent { trace.SetCallType("delegatecall") trace.Action.To = prev.Action.To } + } else { + // Handle EVM call special casing + // + // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions + // and should be dropped from the trace. + if builtinactors.IsEvmActor(parent.Action.FilecoinCodeCid) && + et.Msg.Method > 0 && + et.Msg.Method <= 1023 { + log.Debugf("COND5 found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.FilecoinCodeCid.String(), height) + + if et.Msg.Method == builtin.MethodsEVM.GetBytecode { + // save the last bytecode trace to handle delegate calls + parent.LastByteCode = trace + } + + return nil + } } } + // we are adding trace to the traces so update the parent subtraces count as it was originally set to zero + if parent != nil { + parent.Subtraces += 1 + } + *traces = append(*traces, trace) for i, call := range et.Subcalls { From ed407689e6d6bfc91ff49e69f445657116e51e66 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Fri, 25 Aug 2023 21:50:24 +0000 Subject: [PATCH 19/25] Parse input/output for delegate call + other smaller things --- chain/types/ethtypes/eth_types.go | 5 ++- node/impl/full/eth_trace.go | 69 +++++++++++++++++++------------ 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index 786d7d57093..b796e6f56f2 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -938,7 +938,10 @@ type EthTrace struct { TraceAddress []int `json:"traceAddress"` Type string `json:"Type"` - Parent *EthTrace `json:"-"` + Parent *EthTrace `json:"-"` + + // if a subtrace makes a call to GetBytecode, we store a pointer to that subtrace here + // which we then lookup when checking for delegatecall (InvokeContractDelegate) LastByteCode *EthTrace `json:"-"` } diff --git a/node/impl/full/eth_trace.go b/node/impl/full/eth_trace.go index 5b72828f452..12e95b6448f 100644 --- a/node/impl/full/eth_trace.go +++ b/node/impl/full/eth_trace.go @@ -13,6 +13,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/builtin" + "github.com/filecoin-project/go-state-types/builtin/v10/evm" "github.com/filecoin-project/go-state-types/exitcode" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" @@ -192,37 +193,51 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht } } - // Handle delegate calls - // - // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate, method 6. - // 2) Check that the previous trace calls another actor on method 3 (GetByteCode) and they are at the same level (same parent) - // 3) Treat this as a delegate call to actor A. - if parent.LastByteCode != nil && trace.Action.From == trace.Action.To && - trace.Action.FilecoinMethod == builtin.MethodsEVM.InvokeContractDelegate { - log.Debugf("COND7 found delegate call, height: %d", height) - prev := parent.LastByteCode - if prev.Action.From == trace.Action.From && prev.Action.FilecoinMethod == builtin.MethodsEVM.GetBytecode && prev.Parent == trace.Parent { - trace.SetCallType("delegatecall") - trace.Action.To = prev.Action.To - } - } else { - // Handle EVM call special casing + if builtinactors.IsEvmActor(parent.Action.FilecoinCodeCid) { + // Handle delegate calls // - // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions - // and should be dropped from the trace. - if builtinactors.IsEvmActor(parent.Action.FilecoinCodeCid) && - et.Msg.Method > 0 && - et.Msg.Method <= 1023 { - log.Debugf("COND5 found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.FilecoinCodeCid.String(), height) - - if et.Msg.Method == builtin.MethodsEVM.GetBytecode { - // save the last bytecode trace to handle delegate calls - parent.LastByteCode = trace + // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate, method 6. + // 2) Check that the previous trace calls another actor on method 3 (GetByteCode) and they are at the same level (same parent) + // 3) Treat this as a delegate call to actor A. + if parent.LastByteCode != nil && trace.Action.From == trace.Action.To && + trace.Action.FilecoinMethod == builtin.MethodsEVM.InvokeContractDelegate { + log.Debugf("COND7 found delegate call, height: %d", height) + prev := parent.LastByteCode + if prev.Action.From == trace.Action.From && prev.Action.FilecoinMethod == builtin.MethodsEVM.GetBytecode && prev.Parent == trace.Parent { + trace.SetCallType("delegatecall") + trace.Action.To = prev.Action.To + + var dp evm.DelegateCallParams + err := dp.UnmarshalCBOR(bytes.NewReader(et.Msg.Params)) + if err != nil { + return xerrors.Errorf("failed UnmarshalCBOR: %w", err) + } + trace.Action.Input = dp.Input + + trace.Result.Output, err = decodePayload(et.MsgRct.Return, et.MsgRct.ReturnCodec) + if err != nil { + return xerrors.Errorf("failed decodePayload: %w", err) + } + } + } else { + // Handle EVM call special casing + // + // Any outbound call from an EVM actor on methods 1-1023 are side-effects from EVM instructions + // and should be dropped from the trace. + if et.Msg.Method > 0 && + et.Msg.Method <= 1023 { + log.Debugf("Infof found outbound call from an EVM actor on method 1-1023 method:%d, code:%s, height:%d", et.Msg.Method, parent.Action.FilecoinCodeCid.String(), height) + + if et.Msg.Method == builtin.MethodsEVM.GetBytecode { + // save the last bytecode trace to handle delegate calls + parent.LastByteCode = trace + } + + return nil } - - return nil } } + } // we are adding trace to the traces so update the parent subtraces count as it was originally set to zero From aef0ecf2d2f44235f41c8fe6d1a8c92ed2a8c7f9 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Mon, 28 Aug 2023 10:38:53 +0000 Subject: [PATCH 20/25] Run make gen --- build/openrpc/full.json.gz | Bin 34826 -> 34825 bytes build/openrpc/gateway.json.gz | Bin 11875 -> 11869 bytes documentation/en/api-v1-unstable-methods.md | 10 +++++----- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index 6d76e42148126af70189b51fd4b45965fd2fa867..dabda4d1e216ec71a32a9a6870a1defbe3532165 100644 GIT binary patch delta 33996 zcmV)-K!?AIkOGO20)HQi2mk;800030?7e$)+qjYk`c+W6f1D(xcx1_s#J5h}iXTb5 z<0M|&ne1+y-GN9*!k7X$1n9@4^4)LY!S_QTMcJ~AIj3eU5@<9)p!?U2M&oCX`iKO+ z)7$H9ZVWeedi@?I6WZJR+2fddsJGWUp*&>Z?DPPJJ9)u2m2})>@*foFbfq(xS5bUYampni`3{{@~>$`E^PD{PWLVzsJJ)90qs2yw)GB)xvmhXI_c0SyE3g~oTUeaf)>1p2l3fs= z1bPhpDtXrLpEP!%MmGEV{$1~uB04=@}<;pu@t$gSuJ@6d=!Yq!2l%S1!Q-`nFW88(2%_Z zGXy<81CFQxD3OcC5j1{d9P;b}5?zh>1>-weHY6?zygN*$0E>A%M}*5oxUuwt`y6^+ z;S=hI$@+###=5rtTo4~>D#sPL0&Fz>@C$l=u zmEfXgPA3h!@-GGmbrA^A1OM#U< zC^*sw5FAh!IpD?33_CM`iHpSy7j4W#k7Gx?eeFZ%3b}DBV)eWPkhp&Uf={B<46@lq zub)**zlm{!le^*gALt-Bx?%2Y=WYZ?JA6E1S7`44MQ{K1!#g_n{~FIH=x#FFn&44* zw-bhFYk0lY6B;dAs!BZ?E>| zbHsl~cYWXwOP-C=bPs?vc42Y5O~0h2NZ$pU~* z094xZk{rA{d(8;+*^F}F;s80^y91m87qhEqAu~M39t_0eR73qsf^Hq;^FDBqkBBSe z0wsXTugUtd$(XhS3BbhhLMaU$gniEXmt>9@gHy5mi3_AI4=8`-Qg+Dt9wJjd17pN* z5K?-d`N$D+#ucc*y(FS0hu9Mhh79-tbwfw+a@m7{GsD-=dp!-Hi!OUHJ7PQ_j~Kcn z(DNj1N?yx>0WRcl1AvPelC7XX4VV}WUC9Mw=v)C#FUb@U#IWiEpCN`)?TCJl5eS8d z

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 diff --git a/build/openrpc/gateway.json.gz b/build/openrpc/gateway.json.gz index 052d47e574368866dd6f72e40a98c94182d778f7..0a855c9b1f7cb8c4c2d6649a4d62ca62edff30e5 100644 GIT binary patch delta 6366 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

vY=Qc$a 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 delta 6465 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 bl_Bl6_sz%0?~ng400960@GYRJMU4UgDr2bG diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index beb8f5d9dc3..e2c2493956d 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -3106,8 +3106,8 @@ Response: { "action": { "callType": "string value", - "from": "string value", - "to": "string value", + "from": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "to": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", "gas": "0x5", "input": "0x07", "value": "0x0" @@ -3149,14 +3149,14 @@ Response: ```json [ { - "output": "string value", + "output": "0x07", "stateDiff": "string value", "trace": [ { "action": { "callType": "string value", - "from": "string value", - "to": "string value", + "from": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", + "to": "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031", "gas": "0x5", "input": "0x07", "value": "0x0" From 57e825901e4cc049339efc1d55d14a3443704941 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Mon, 28 Aug 2023 17:51:47 +0000 Subject: [PATCH 21/25] Update FFI --- extern/filecoin-ffi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/filecoin-ffi b/extern/filecoin-ffi index 8c2147f706a..bf5edd551d2 160000 --- a/extern/filecoin-ffi +++ b/extern/filecoin-ffi @@ -1 +1 @@ -Subproject commit 8c2147f706a8ccdbd27b7340c87dc5953448c911 +Subproject commit bf5edd551d23901fa565aac4ce94433afe0c278e From 930e9b957c1d77fe5749a4d918446b21b10ee003 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Mon, 28 Aug 2023 18:47:45 +0000 Subject: [PATCH 22/25] fix lint --- node/impl/full/eth_trace.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/impl/full/eth_trace.go b/node/impl/full/eth_trace.go index 12e95b6448f..f92dcf2ebb5 100644 --- a/node/impl/full/eth_trace.go +++ b/node/impl/full/eth_trace.go @@ -242,7 +242,7 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht // we are adding trace to the traces so update the parent subtraces count as it was originally set to zero if parent != nil { - parent.Subtraces += 1 + parent.Subtraces++ } *traces = append(*traces, trace) From 13e1b4b3df433ea8c419b8af3ae5dde3c4168641 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 29 Aug 2023 10:38:21 +0000 Subject: [PATCH 23/25] Added todo to support native actors calling another when created --- node/impl/full/eth_trace.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/node/impl/full/eth_trace.go b/node/impl/full/eth_trace.go index f92dcf2ebb5..3766c544874 100644 --- a/node/impl/full/eth_trace.go +++ b/node/impl/full/eth_trace.go @@ -155,6 +155,8 @@ func buildTraces(ctx context.Context, traces *[]*ethtypes.EthTrace, parent *etht parent.Result.Output = nil // there should never be any subcalls when creating a native actor + // + // TODO: add support for native actors calling another when created return nil } From 144bbdfd3b059b8c8caa5d17f8ac0ab2069df7b5 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 29 Aug 2023 11:26:52 +0000 Subject: [PATCH 24/25] Added trace api as experimental feature to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5045d877327..9076e978f80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ # UNRELEASED +## New features +- feat: Added new tracing API (**HIGHLY EXPERIMENTAL**) supporting two RPC methods: `trace_block` and `trace_replayBlockTransactions` ([filecoin-project/lotus#11100](https://github.com/filecoin-project/lotus/pull/11100)) + # v1.23.3 / 2023-08-01 This feature release of Lotus includes numerous improvements and enhancements for node operators, ETH RPC-providers and storage providers. From 0096d521c33cad503f7e5280792d448d46f839e2 Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Tue, 29 Aug 2023 12:27:08 +0000 Subject: [PATCH 25/25] fix decoding toplevel output in trace_replayBlockTransactions --- node/impl/full/eth.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 7510856e721..a051b49b179 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -20,6 +20,7 @@ import ( "github.com/filecoin-project/go-jsonrpc" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/go-state-types/builtin" builtintypes "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/builtin/v10/evm" "github.com/filecoin-project/go-state-types/exitcode" @@ -933,9 +934,18 @@ func (a *EthModule) EthTraceReplayBlockTransactions(ctx context.Context, blkNum continue } - output, err := decodePayload(ir.ExecutionTrace.MsgRct.Return, ir.ExecutionTrace.MsgRct.ReturnCodec) - if err != nil { - return nil, xerrors.Errorf("failed to decode payload: %w", err) + var output ethtypes.EthBytes + invokeCreateOnEAM := ir.Msg.To == builtin.EthereumAddressManagerActorAddr && (ir.Msg.Method == builtin.MethodsEAM.Create || ir.Msg.Method == builtin.MethodsEAM.Create2) + if ir.Msg.Method == builtin.MethodsEVM.InvokeContract || invokeCreateOnEAM { + output, err = decodePayload(ir.ExecutionTrace.MsgRct.Return, ir.ExecutionTrace.MsgRct.ReturnCodec) + if err != nil { + return nil, xerrors.Errorf("failed to decode payload: %w", err) + } + } else { + output, err = handleFilecoinMethodOutput(ir.ExecutionTrace.MsgRct.ExitCode, ir.ExecutionTrace.MsgRct.ReturnCodec, ir.ExecutionTrace.MsgRct.Return) + if err != nil { + return nil, xerrors.Errorf("could not convert output: %w", err) + } } t := ethtypes.EthTraceReplayBlockTransaction{