diff --git a/api/api_full.go b/api/api_full.go index 0e128b398..95d86ce6f 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 61cb48146..0f2e66709 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 ca0f861ac..fe761c545 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 d2f2e528e..882aacebb 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 95668596d..6f0487b0b 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 90d1a14c5..a9040613f 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 2a25d22e2..4556f7b88 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 13626d702..742f3de8e 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 2049e2175..273f20dc9 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 bcc4af9ac..bbda71fcf 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 e5954c2ff..f8b950011 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 267659f00..0681020ed 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 affcc960e..0f87cfe29 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 c4bda6428..7412ed754 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 000000000..cc31103f9 --- /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 000000000..aea7fc02f --- /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 + } +}