diff --git a/CHANGELOG.md b/CHANGELOG.md index d2daf9aeb6c..9b096c616fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Add F3GetCertificate & F3GetLatestCertificate to the gateway. ([filecoin-project/lotus#12778](https://github.com/filecoin-project/lotus/pull/12778)) - Add Magik's bootstrap node. ([filecoin-project/lotus#12792](https://github.com/filecoin-project/lotus/pull/12792)) - Lotus now reports the network name as a tag in most metrics. Some untagged metrics will be completed in a follow-up at a later date. ([filecoin-project/lotus#12733](https://github.com/filecoin-project/lotus/pull/12733)) +- Refactored Ethereum API implementation into smaller, more manageable modules in a new `github.com/filecoin-project/lotus/node/impl/eth` package. ([filecoin-project/lotus#12796](https://github.com/filecoin-project/lotus/pull/12796)) # UNRELEASED v.1.32.0 diff --git a/api/api_full.go b/api/api_full.go index c02dfdf0e8d..5d751379be2 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -768,6 +768,32 @@ type FullNode interface { // MethodGroup: Eth // These methods are used for Ethereum-compatible JSON-RPC calls // + // ### Execution model reconciliation + // + // Ethereum relies on an immediate block-based execution model. The block that includes + // a transaction is also the block that executes it. Each block specifies the state root + // resulting from executing all transactions within it (output state). + // + // In Filecoin, at every epoch there is an unknown number of round winners all of whom are + // entitled to publish a block. Blocks are collected into a tipset. A tipset is committed + // only when the subsequent tipset is built on it (i.e. it becomes a parent). Block producers + // execute the parent tipset and specify the resulting state root in the block being produced. + // In other words, contrary to Ethereum, each block specifies the input state root. + // + // Ethereum clients expect transactions returned via eth_getBlock* to have a receipt + // (due to immediate execution). For this reason: + // + // - eth_blockNumber returns the latest executed epoch (head - 1) + // - The 'latest' block refers to the latest executed epoch (head - 1) + // - The 'pending' block refers to the current speculative tipset (head) + // - eth_getTransactionByHash returns the inclusion tipset of a message, but + // only after it has executed. + // - eth_getTransactionReceipt ditto. + // + // "Latest executed epoch" refers to the tipset that this node currently + // accepts as the best parent tipset, based on the blocks it is accumulating + // within the HEAD tipset. + // EthAccounts will always return [] since we don't expect Lotus to manage private keys EthAccounts(ctx context.Context) ([]ethtypes.EthAddress, error) //perm:read // EthAddressToFilecoinAddress converts an EthAddress into an f410 Filecoin Address diff --git a/build/openrpc/full.json b/build/openrpc/full.json index 010f5f6592f..1ca1979ff8b 100644 --- a/build/openrpc/full.json +++ b/build/openrpc/full.json @@ -2155,7 +2155,7 @@ { "name": "Filecoin.EthAccounts", "description": "```go\nfunc (s *FullNodeStruct) EthAccounts(p0 context.Context) ([]ethtypes.EthAddress, error) {\n\tif s.Internal.EthAccounts == nil {\n\t\treturn *new([]ethtypes.EthAddress), ErrNotSupported\n\t}\n\treturn s.Internal.EthAccounts(p0)\n}\n```", - "summary": "There are not yet any comments for this method.", + "summary": "EthAccounts will always return [] since we don't expect Lotus to manage private keys\n", "paramStructure": "by-position", "params": [], "result": { diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index a7abe617785..7089dcb0663 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -1326,11 +1326,36 @@ Response: `{}` ## Eth These methods are used for Ethereum-compatible JSON-RPC calls -EthAccounts will always return [] since we don't expect Lotus to manage private keys +### Execution model reconciliation + +Ethereum relies on an immediate block-based execution model. The block that includes +a transaction is also the block that executes it. Each block specifies the state root +resulting from executing all transactions within it (output state). + +In Filecoin, at every epoch there is an unknown number of round winners all of whom are +entitled to publish a block. Blocks are collected into a tipset. A tipset is committed +only when the subsequent tipset is built on it (i.e. it becomes a parent). Block producers +execute the parent tipset and specify the resulting state root in the block being produced. +In other words, contrary to Ethereum, each block specifies the input state root. + +Ethereum clients expect transactions returned via eth_getBlock* to have a receipt +(due to immediate execution). For this reason: + + - eth_blockNumber returns the latest executed epoch (head - 1) + - The 'latest' block refers to the latest executed epoch (head - 1) + - The 'pending' block refers to the current speculative tipset (head) + - eth_getTransactionByHash returns the inclusion tipset of a message, but + only after it has executed. + - eth_getTransactionReceipt ditto. + +"Latest executed epoch" refers to the tipset that this node currently +accepts as the best parent tipset, based on the blocks it is accumulating +within the HEAD tipset. ### EthAccounts -There are not yet any comments for this method. +EthAccounts will always return [] since we don't expect Lotus to manage private keys + Perms: read diff --git a/gateway/node.go b/gateway/node.go index 3eefddc424f..b1446297901 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -185,6 +185,7 @@ var ( _ full.MpoolModuleAPI = (*Node)(nil) _ full.StateModuleAPI = (*Node)(nil) _ full.EthModuleAPI = (*Node)(nil) + _ full.EthEventAPI = (*Node)(nil) ) type options struct { diff --git a/itests/eth_config_test.go b/itests/eth_config_test.go index 654fc86bf3b..502c83f677f 100644 --- a/itests/eth_config_test.go +++ b/itests/eth_config_test.go @@ -8,7 +8,7 @@ import ( "github.com/filecoin-project/lotus/chain/types/ethtypes" "github.com/filecoin-project/lotus/itests/kit" - "github.com/filecoin-project/lotus/node/impl/full" + "github.com/filecoin-project/lotus/node/impl/eth" ) func TestEthFilterAPIDisabledViaConfig(t *testing.T) { @@ -21,41 +21,41 @@ func TestEthFilterAPIDisabledViaConfig(t *testing.T) { _, err := client.EthNewPendingTransactionFilter(ctx) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthGetLogs(ctx, ðtypes.EthFilterSpec{}) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthGetFilterChanges(ctx, ethtypes.EthFilterID{}) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthGetFilterLogs(ctx, ethtypes.EthFilterID{}) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthNewFilter(ctx, ðtypes.EthFilterSpec{}) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthNewBlockFilter(ctx) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthNewPendingTransactionFilter(ctx) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthUninstallFilter(ctx, ethtypes.EthFilterID{}) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthSubscribe(ctx, []byte("{}")) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) _, err = client.EthUnsubscribe(ctx, ethtypes.EthSubscriptionID{}) require.NotNil(t, err) - require.Equal(t, err.Error(), full.ErrModuleDisabled.Error()) + require.Equal(t, err.Error(), eth.ErrModuleDisabled.Error()) } diff --git a/itests/eth_fee_history_test.go b/itests/eth_fee_history_test.go index b611efeb18e..5b410b322d9 100644 --- a/itests/eth_fee_history_test.go +++ b/itests/eth_fee_history_test.go @@ -16,7 +16,7 @@ import ( "github.com/filecoin-project/lotus/chain/types/ethtypes" "github.com/filecoin-project/lotus/itests/kit" "github.com/filecoin-project/lotus/lib/result" - "github.com/filecoin-project/lotus/node/impl/full" + "github.com/filecoin-project/lotus/node/impl/gasutils" ) // calculateExpectations calculates the expected number of items to be included in the response @@ -171,7 +171,7 @@ func TestEthFeeHistory(t *testing.T) { for _, arr := range *history.Reward { require.Equal(3, len(arr)) for _, item := range arr { - require.Equal(ethtypes.EthBigInt(types.NewInt(full.MinGasPremium)), item) + require.Equal(ethtypes.EthBigInt(types.NewInt(gasutils.MinGasPremium)), item) } } diff --git a/node/builder_chain.go b/node/builder_chain.go index 1293dcd0c76..1d9e9a98ec5 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -40,7 +40,9 @@ import ( "github.com/filecoin-project/lotus/node/hello" "github.com/filecoin-project/lotus/node/impl" "github.com/filecoin-project/lotus/node/impl/common" + "github.com/filecoin-project/lotus/node/impl/eth" "github.com/filecoin-project/lotus/node/impl/full" + "github.com/filecoin-project/lotus/node/impl/gasutils" "github.com/filecoin-project/lotus/node/impl/net" "github.com/filecoin-project/lotus/node/modules" "github.com/filecoin-project/lotus/node/modules/dtypes" @@ -127,7 +129,7 @@ var ChainNode = Options( // Markets (storage) Override(new(*market.FundManager), market.NewFundManager), - Override(new(*full.GasPriceCache), full.NewGasPriceCache), + Override(new(*gasutils.GasPriceCache), gasutils.NewGasPriceCache), // Lite node API ApplyIf(isLiteNode, @@ -138,9 +140,17 @@ var ChainNode = Options( Override(new(full.MpoolModuleAPI), From(new(api.Gateway))), Override(new(full.StateModuleAPI), From(new(api.Gateway))), 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.ActorEventAPI), From(new(api.Gateway))), + Override(new(eth.EthFilecoinAPI), From(new(api.Gateway))), + Override(new(eth.EthBasicAPI), From(new(api.Gateway))), + Override(new(eth.EthEventsAPI), From(new(api.Gateway))), + Override(new(eth.EthTransactionAPI), From(new(api.Gateway))), + Override(new(eth.EthLookupAPI), From(new(api.Gateway))), + Override(new(eth.EthTraceAPI), From(new(api.Gateway))), + Override(new(eth.EthGasAPI), From(new(api.Gateway))), + // EthSendAPI is a special case, we block the Untrusted method via GatewayEthSend even though it + // shouldn't be exposed on the Gateway API. + Override(new(eth.EthSendAPI), new(modules.GatewayEthSend)), Override(new(index.Indexer), modules.ChainIndexer(config.ChainIndexerConfig{ EnableIndexer: false, @@ -266,17 +276,37 @@ func ConfigFullNode(c interface{}) Option { If(cfg.Fevm.EnableEthRPC || cfg.Events.EnableActorEventsAPI, // Actor event filtering support, only needed for either Eth RPC and ActorEvents API Override(new(events.EventHelperAPI), From(new(modules.EventHelperAPI))), - Override(new(*filter.EventFilterManager), modules.EventFilterManager(cfg.Events)), + Override(new(*filter.EventFilterManager), modules.MakeEventFilterManager(cfg.Events)), ), + Override(new(eth.ChainStore), From(new(*store.ChainStore))), + Override(new(eth.StateManager), From(new(*stmgr.StateManager))), + Override(new(eth.EthFilecoinAPI), eth.NewEthFilecoinAPI), + If(cfg.Fevm.EnableEthRPC, - Override(new(*full.EthEventHandler), modules.EthEventHandler(cfg.Events, cfg.Fevm.EnableEthRPC)), - Override(new(full.EthModuleAPI), modules.EthModuleAPI(cfg.Fevm)), - Override(new(full.EthEventAPI), From(new(*full.EthEventHandler))), + Override(new(eth.StateAPI), From(new(full.StateAPI))), + Override(new(eth.SyncAPI), From(new(full.SyncAPI))), + Override(new(eth.MpoolAPI), From(new(full.MpoolAPI))), + Override(new(eth.MessagePool), From(new(*messagepool.MessagePool))), + Override(new(eth.GasAPI), From(new(full.GasModule))), + + Override(new(eth.EthBasicAPI), eth.NewEthBasicAPI), + Override(new(eth.EthEventsInternal), modules.MakeEthEventsExtended(cfg.Events, cfg.Fevm.EnableEthRPC)), + Override(new(eth.EthEventsAPI), From(new(eth.EthEventsInternal))), + Override(new(eth.EthTransactionAPI), modules.MakeEthTransaction(cfg.Fevm)), + Override(new(eth.EthLookupAPI), eth.NewEthLookupAPI), + Override(new(eth.EthTraceAPI), modules.MakeEthTrace(cfg.Fevm)), + Override(new(eth.EthGasAPI), eth.NewEthGasAPI), + Override(new(eth.EthSendAPI), eth.NewEthSendAPI), ), If(!cfg.Fevm.EnableEthRPC, - Override(new(full.EthModuleAPI), &full.EthModuleDummy{}), - Override(new(full.EthEventAPI), &full.EthModuleDummy{}), + Override(new(eth.EthBasicAPI), ð.EthBasicDisabled{}), + Override(new(eth.EthTransactionAPI), ð.EthTransactionDisabled{}), + Override(new(eth.EthLookupAPI), ð.EthLookupDisabled{}), + Override(new(eth.EthTraceAPI), ð.EthTraceDisabled{}), + Override(new(eth.EthGasAPI), ð.EthGasDisabled{}), + Override(new(eth.EthEventsAPI), ð.EthEventsDisabled{}), + Override(new(eth.EthSendAPI), ð.EthSendDisabled{}), ), If(cfg.Events.EnableActorEventsAPI, diff --git a/node/impl/eth/api.go b/node/impl/eth/api.go new file mode 100644 index 00000000000..b9a5159a3e9 --- /dev/null +++ b/node/impl/eth/api.go @@ -0,0 +1,97 @@ +package eth + +import ( + "context" + + "github.com/ipfs/go-cid" + logging "github.com/ipfs/go-log/v2" + "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/network" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/actors/adt" + "github.com/filecoin-project/lotus/chain/state" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" +) + +var ( + ErrChainIndexerDisabled = xerrors.New("chain indexer is disabled; please enable the ChainIndexer to use the ETH RPC API") + ErrModuleDisabled = xerrors.New("module disabled, enable with Fevm.EnableEthRPC / LOTUS_FEVM_ENABLEETHRPC") +) + +var log = logging.Logger("node/eth") + +// SyncAPI is a minimal version of full.SyncAPI +type SyncAPI interface { + SyncState(ctx context.Context) (*api.SyncState, error) +} + +// ChainStore is a minimal version of store.ChainStore just for tipsets +type ChainStore interface { + // TipSets + GetHeaviestTipSet() *types.TipSet + GetTipsetByHeight(ctx context.Context, h abi.ChainEpoch, ts *types.TipSet, prev bool) (*types.TipSet, error) + GetTipSetFromKey(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) + GetTipSetByCid(ctx context.Context, c cid.Cid) (*types.TipSet, error) + LoadTipSet(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) + + // Messages + GetSignedMessage(ctx context.Context, c cid.Cid) (*types.SignedMessage, error) + GetMessage(ctx context.Context, c cid.Cid) (*types.Message, error) + BlockMsgsForTipset(ctx context.Context, ts *types.TipSet) ([]store.BlockMessages, error) + MessagesForTipset(ctx context.Context, ts *types.TipSet) ([]types.ChainMsg, error) + ReadReceipts(ctx context.Context, root cid.Cid) ([]types.MessageReceipt, error) + + // Misc + ActorStore(ctx context.Context) adt.Store +} + +// StateAPI is a minimal version of full.StateAPI +type StateAPI interface { + StateSearchMsg(ctx context.Context, from types.TipSetKey, msg cid.Cid, limit abi.ChainEpoch, allowReplaced bool) (*api.MsgLookup, error) +} + +// StateManager is a minimal version of stmgr.StateManager +type StateManager interface { + GetNetworkVersion(ctx context.Context, height abi.ChainEpoch) network.Version + + TipSetState(ctx context.Context, ts *types.TipSet) (cid.Cid, cid.Cid, error) + ParentState(ts *types.TipSet) (*state.StateTree, error) + StateTree(st cid.Cid) (*state.StateTree, error) + + LookupIDAddress(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) + LoadActor(ctx context.Context, addr address.Address, ts *types.TipSet) (*types.Actor, error) + LoadActorRaw(ctx context.Context, addr address.Address, st cid.Cid) (*types.Actor, error) + ResolveToDeterministicAddress(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) + + ExecutionTrace(ctx context.Context, ts *types.TipSet) (cid.Cid, []*api.InvocResult, error) + Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) + CallWithGas(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet, applyTsMessages bool) (*api.InvocResult, error) + ApplyOnStateWithGas(ctx context.Context, stateCid cid.Cid, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error) + + HasExpensiveForkBetween(parent, height abi.ChainEpoch) bool +} + +// MpoolAPI is a minimal version of full.MpoolAPI +type MpoolAPI interface { + MpoolPending(ctx context.Context, tsk types.TipSetKey) ([]*types.SignedMessage, error) + MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) + MpoolPushUntrusted(ctx context.Context, smsg *types.SignedMessage) (cid.Cid, error) + MpoolPush(ctx context.Context, smsg *types.SignedMessage) (cid.Cid, error) +} + +// MessagePool is a minimal version of messagepool.MessagePool +type MessagePool interface { + PendingFor(ctx context.Context, a address.Address) ([]*types.SignedMessage, *types.TipSet) + GetConfig() *types.MpoolConfig +} + +// GasAPI is a minimal version of full.GasAPI +type GasAPI interface { + GasEstimateGasPremium(ctx context.Context, nblocksincl uint64, sender address.Address, gaslimit int64, ts types.TipSetKey) (types.BigInt, error) + GasEstimateMessageGas(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec, ts types.TipSetKey) (*types.Message, error) +} diff --git a/node/impl/eth/basic.go b/node/impl/eth/basic.go new file mode 100644 index 00000000000..e2adb099e48 --- /dev/null +++ b/node/impl/eth/basic.go @@ -0,0 +1,126 @@ +package eth + +import ( + "context" + "strconv" + + "golang.org/x/xerrors" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/build/buildconstants" + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +type EthBasicAPI interface { + Web3ClientVersion(ctx context.Context) (string, error) + EthChainId(ctx context.Context) (ethtypes.EthUint64, error) + NetVersion(ctx context.Context) (string, error) + NetListening(ctx context.Context) (bool, error) + EthProtocolVersion(ctx context.Context) (ethtypes.EthUint64, error) + EthSyncing(ctx context.Context) (ethtypes.EthSyncingResult, error) + EthAccounts(ctx context.Context) ([]ethtypes.EthAddress, error) +} + +var ( + _ EthBasicAPI = (*ethBasic)(nil) + _ EthBasicAPI = (*EthBasicDisabled)(nil) +) + +type ethBasic struct { + chainStore ChainStore + syncApi SyncAPI + stateManager StateManager +} + +func NewEthBasicAPI(chainStore ChainStore, syncApi SyncAPI, stateManager StateManager) EthBasicAPI { + return ðBasic{ + chainStore: chainStore, + syncApi: syncApi, + stateManager: stateManager, + } +} + +func (e *ethBasic) Web3ClientVersion(ctx context.Context) (string, error) { + return string(build.NodeUserVersion()), nil +} + +func (e *ethBasic) EthChainId(ctx context.Context) (ethtypes.EthUint64, error) { + return ethtypes.EthUint64(buildconstants.Eip155ChainId), nil +} + +func (e *ethBasic) NetVersion(_ context.Context) (string, error) { + return strconv.FormatInt(buildconstants.Eip155ChainId, 10), nil +} + +func (e *ethBasic) NetListening(ctx context.Context) (bool, error) { + return true, nil +} + +func (e *ethBasic) EthProtocolVersion(ctx context.Context) (ethtypes.EthUint64, error) { + height := e.chainStore.GetHeaviestTipSet().Height() + return ethtypes.EthUint64(e.stateManager.GetNetworkVersion(ctx, height)), nil +} + +func (e *ethBasic) EthSyncing(ctx context.Context) (ethtypes.EthSyncingResult, error) { + state, err := e.syncApi.SyncState(ctx) + if err != nil { + return ethtypes.EthSyncingResult{}, xerrors.Errorf("failed calling SyncState: %w", err) + } + + if len(state.ActiveSyncs) == 0 { + return ethtypes.EthSyncingResult{}, xerrors.New("no active syncs, try again") + } + + working := -1 + for i, ss := range state.ActiveSyncs { + if ss.Stage == api.StageIdle { + continue + } + working = i + } + if working == -1 { + working = len(state.ActiveSyncs) - 1 + } + + ss := state.ActiveSyncs[working] + if ss.Base == nil || ss.Target == nil { + return ethtypes.EthSyncingResult{}, xerrors.New("missing syncing information, try again") + } + + res := ethtypes.EthSyncingResult{ + DoneSync: ss.Stage == api.StageSyncComplete, + CurrentBlock: ethtypes.EthUint64(ss.Height), + StartingBlock: ethtypes.EthUint64(ss.Base.Height()), + HighestBlock: ethtypes.EthUint64(ss.Target.Height()), + } + + return res, nil +} + +func (e *ethBasic) EthAccounts(context.Context) ([]ethtypes.EthAddress, error) { + // The lotus node is not expected to hold manage accounts, so we'll always return an empty array + return []ethtypes.EthAddress{}, nil +} + +type EthBasicDisabled struct{} + +func (EthBasicDisabled) Web3ClientVersion(ctx context.Context) (string, error) { + return string(build.NodeUserVersion()), nil +} +func (EthBasicDisabled) EthChainId(ctx context.Context) (ethtypes.EthUint64, error) { + return 0, ErrModuleDisabled +} +func (EthBasicDisabled) NetVersion(ctx context.Context) (string, error) { return "", ErrModuleDisabled } +func (EthBasicDisabled) NetListening(ctx context.Context) (bool, error) { + return false, ErrModuleDisabled +} +func (EthBasicDisabled) EthProtocolVersion(ctx context.Context) (ethtypes.EthUint64, error) { + return 0, ErrModuleDisabled +} +func (EthBasicDisabled) EthSyncing(ctx context.Context) (ethtypes.EthSyncingResult, error) { + return ethtypes.EthSyncingResult{}, ErrModuleDisabled +} +func (EthBasicDisabled) EthAccounts(ctx context.Context) ([]ethtypes.EthAddress, error) { + return nil, ErrModuleDisabled +} diff --git a/node/impl/eth/events.go b/node/impl/eth/events.go new file mode 100644 index 00000000000..cd2ea7bc549 --- /dev/null +++ b/node/impl/eth/events.go @@ -0,0 +1,785 @@ +package eth + +import ( + "context" + "errors" + "math" + "strings" + "time" + + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multicodec" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-jsonrpc" + "github.com/filecoin-project/go-state-types/abi" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/events/filter" + "github.com/filecoin-project/lotus/chain/index" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +const ( + EthSubscribeEventTypeHeads = "newHeads" + EthSubscribeEventTypeLogs = "logs" + EthSubscribeEventTypePendingTransactions = "newPendingTransactions" +) + +type EthEventsAPI interface { + EthGetLogs(ctx context.Context, filter *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) + EthNewBlockFilter(ctx context.Context) (ethtypes.EthFilterID, error) + EthNewPendingTransactionFilter(ctx context.Context) (ethtypes.EthFilterID, error) + EthNewFilter(ctx context.Context, filter *ethtypes.EthFilterSpec) (ethtypes.EthFilterID, error) + EthUninstallFilter(ctx context.Context, id ethtypes.EthFilterID) (bool, error) + EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) + EthGetFilterLogs(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) + EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) + EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) +} + +// EthEventsInternal extends the EthEvents interface with additional methods that are not exposed +// on the JSON-RPC API. +type EthEventsInternal interface { + EthEventsAPI + + // GetEthLogsForBlockAndTransaction returns the logs for a block and transaction, it is intended + // for internal use rather than being exposed via the JSON-RPC API. + GetEthLogsForBlockAndTransaction(ctx context.Context, blockHash *ethtypes.EthHash, txHash ethtypes.EthHash) ([]ethtypes.EthLog, error) + // GC runs a garbage collection loop, deleting filters that have not been used within the ttl + // window, it is intended for internal use rather than being exposed via the JSON-RPC API. + GC(ctx context.Context, ttl time.Duration) +} + +var ( + _ EthEventsAPI = (*ethEvents)(nil) + _ EthEventsInternal = (*ethEvents)(nil) + _ EthEventsAPI = (*EthEventsDisabled)(nil) +) + +type filterEventCollector interface { + TakeCollectedEvents(context.Context) []*index.CollectedEvent +} + +type filterMessageCollector interface { + TakeCollectedMessages(context.Context) []*types.SignedMessage +} + +type filterTipSetCollector interface { + TakeCollectedTipSets(context.Context) []types.TipSetKey +} + +type ethEvents struct { + subscriptionCtx context.Context + chainStore ChainStore + stateManager StateManager + chainIndexer index.Indexer + eventFilterManager *filter.EventFilterManager + tipSetFilterManager *filter.TipSetFilterManager + memPoolFilterManager *filter.MemPoolFilterManager + filterStore filter.FilterStore + subscriptionManager *EthSubscriptionManager + maxFilterHeightRange abi.ChainEpoch +} + +func NewEthEventsAPI( + subscriptionCtx context.Context, + chainStore ChainStore, + stateManager StateManager, + chainIndexer index.Indexer, + eventFilterManager *filter.EventFilterManager, + tipSetFilterManager *filter.TipSetFilterManager, + memPoolFilterManager *filter.MemPoolFilterManager, + filterStore filter.FilterStore, + subscriptionManager *EthSubscriptionManager, + maxFilterHeightRange abi.ChainEpoch, +) EthEventsInternal { + return ðEvents{ + subscriptionCtx: subscriptionCtx, + chainStore: chainStore, + stateManager: stateManager, + chainIndexer: chainIndexer, + eventFilterManager: eventFilterManager, + tipSetFilterManager: tipSetFilterManager, + memPoolFilterManager: memPoolFilterManager, + filterStore: filterStore, + subscriptionManager: subscriptionManager, + maxFilterHeightRange: maxFilterHeightRange, + } +} + +func (e *ethEvents) EthGetLogs(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) { + ces, err := e.ethGetEventsForFilter(ctx, filterSpec) + if err != nil { + return nil, xerrors.Errorf("failed to get events for filter: %w", err) + } + return ethFilterResultFromEvents(ctx, ces, e.chainStore, e.stateManager) +} + +func (e *ethEvents) EthNewBlockFilter(ctx context.Context) (ethtypes.EthFilterID, error) { + if e.filterStore == nil || e.tipSetFilterManager == nil { + return ethtypes.EthFilterID{}, api.ErrNotSupported + } + + f, err := e.tipSetFilterManager.Install(ctx) + if err != nil { + return ethtypes.EthFilterID{}, err + } + + if err := e.filterStore.Add(ctx, f); err != nil { + // Could not record in store, attempt to delete filter to clean up + err2 := e.tipSetFilterManager.Remove(ctx, f.ID()) + if err2 != nil { + return ethtypes.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err) + } + + return ethtypes.EthFilterID{}, err + } + + return ethtypes.EthFilterID(f.ID()), nil +} + +func (e *ethEvents) EthNewPendingTransactionFilter(ctx context.Context) (ethtypes.EthFilterID, error) { + if e.filterStore == nil || e.memPoolFilterManager == nil { + return ethtypes.EthFilterID{}, api.ErrNotSupported + } + + f, err := e.memPoolFilterManager.Install(ctx) + if err != nil { + return ethtypes.EthFilterID{}, err + } + + if err := e.filterStore.Add(ctx, f); err != nil { + // Could not record in store, attempt to delete filter to clean up + err2 := e.memPoolFilterManager.Remove(ctx, f.ID()) + if err2 != nil { + return ethtypes.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err) + } + + return ethtypes.EthFilterID{}, err + } + + return ethtypes.EthFilterID(f.ID()), nil +} + +func (e *ethEvents) EthNewFilter(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) (ethtypes.EthFilterID, error) { + if e.filterStore == nil || e.eventFilterManager == nil { + return ethtypes.EthFilterID{}, api.ErrNotSupported + } + + pf, err := e.parseEthFilterSpec(filterSpec) + if err != nil { + return ethtypes.EthFilterID{}, err + } + + f, err := e.eventFilterManager.Install(ctx, pf.minHeight, pf.maxHeight, pf.tipsetCid, pf.addresses, pf.keys) + if err != nil { + return ethtypes.EthFilterID{}, xerrors.Errorf("failed to install event filter: %w", err) + } + + if err := e.filterStore.Add(ctx, f); err != nil { + // Could not record in store, attempt to delete filter to clean up + err2 := e.eventFilterManager.Remove(ctx, f.ID()) + if err2 != nil { + return ethtypes.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err) + } + + return ethtypes.EthFilterID{}, err + } + return ethtypes.EthFilterID(f.ID()), nil +} + +func (e *ethEvents) EthUninstallFilter(ctx context.Context, id ethtypes.EthFilterID) (bool, error) { + if e.filterStore == nil { + return false, api.ErrNotSupported + } + + f, err := e.filterStore.Get(ctx, types.FilterID(id)) + if err != nil { + if errors.Is(err, filter.ErrFilterNotFound) { + return false, nil + } + return false, err + } + + if err := e.uninstallFilter(ctx, f); err != nil { + return false, err + } + + return true, nil +} + +func (e *ethEvents) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { + if e.filterStore == nil { + return nil, api.ErrNotSupported + } + + f, err := e.filterStore.Get(ctx, types.FilterID(id)) + if err != nil { + return nil, err + } + + switch fc := f.(type) { + case filterEventCollector: + return ethFilterResultFromEvents(ctx, fc.TakeCollectedEvents(ctx), e.chainStore, e.stateManager) + case filterTipSetCollector: + return ethFilterResultFromTipSets(fc.TakeCollectedTipSets(ctx)) + case filterMessageCollector: + return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx)) + } + + return nil, xerrors.New("unknown filter type") +} + +func (e *ethEvents) EthGetFilterLogs(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { + if e.filterStore == nil { + return nil, api.ErrNotSupported + } + + f, err := e.filterStore.Get(ctx, types.FilterID(id)) + if err != nil { + return nil, err + } + + switch fc := f.(type) { + case filterEventCollector: + return ethFilterResultFromEvents(ctx, fc.TakeCollectedEvents(ctx), e.chainStore, e.stateManager) + } + + return nil, xerrors.New("wrong filter type") +} + +func (e *ethEvents) EthSubscribe(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) { + params, err := jsonrpc.DecodeParams[ethtypes.EthSubscribeParams](p) + if err != nil { + return ethtypes.EthSubscriptionID{}, xerrors.Errorf("decoding params: %w", err) + } + + if e.subscriptionManager == nil { + return ethtypes.EthSubscriptionID{}, api.ErrNotSupported + } + + ethCb, ok := jsonrpc.ExtractReverseClient[api.EthSubscriberMethods](ctx) + if !ok { + return ethtypes.EthSubscriptionID{}, xerrors.New("connection doesn't support callbacks") + } + + sub, err := e.subscriptionManager.StartSubscription(e.subscriptionCtx, ethCb.EthSubscription, e.uninstallFilter) + if err != nil { + return ethtypes.EthSubscriptionID{}, err + } + + switch params.EventType { + case EthSubscribeEventTypeHeads: + f, err := e.tipSetFilterManager.Install(ctx) + if err != nil { + // clean up any previous filters added and stop the sub + _, _ = e.EthUnsubscribe(ctx, sub.id) + return ethtypes.EthSubscriptionID{}, err + } + sub.addFilter(f) + + case EthSubscribeEventTypeLogs: + keys := map[string][][]byte{} + if params.Params != nil { + var err error + keys, err = parseEthTopics(params.Params.Topics) + if err != nil { + // clean up any previous filters added and stop the sub + _, _ = e.EthUnsubscribe(ctx, sub.id) + return ethtypes.EthSubscriptionID{}, err + } + } + + var addresses []address.Address + if params.Params != nil { + for _, ea := range params.Params.Address { + a, err := ea.ToFilecoinAddress() + if err != nil { + return ethtypes.EthSubscriptionID{}, xerrors.Errorf("invalid address %x", ea) + } + addresses = append(addresses, a) + } + } + + f, err := e.eventFilterManager.Install(ctx, -1, -1, cid.Undef, addresses, keysToKeysWithCodec(keys)) + if err != nil { + // clean up any previous filters added and stop the sub + _, _ = e.EthUnsubscribe(ctx, sub.id) + return ethtypes.EthSubscriptionID{}, err + } + sub.addFilter(f) + case EthSubscribeEventTypePendingTransactions: + f, err := e.memPoolFilterManager.Install(ctx) + if err != nil { + // clean up any previous filters added and stop the sub + _, _ = e.EthUnsubscribe(ctx, sub.id) + return ethtypes.EthSubscriptionID{}, err + } + + sub.addFilter(f) + default: + return ethtypes.EthSubscriptionID{}, xerrors.Errorf("unsupported event type: %s", params.EventType) + } + + return sub.id, nil +} + +func (e *ethEvents) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) { + if e.subscriptionManager == nil { + return false, api.ErrNotSupported + } + + err := e.subscriptionManager.StopSubscription(ctx, id) + if err != nil { + return false, nil + } + + return true, nil +} + +func (e *ethEvents) GetEthLogsForBlockAndTransaction(ctx context.Context, blockHash *ethtypes.EthHash, txHash ethtypes.EthHash) ([]ethtypes.EthLog, error) { + ces, err := e.ethGetEventsForFilter(ctx, ðtypes.EthFilterSpec{BlockHash: blockHash}) + if err != nil { + return nil, err + } + logs, err := ethFilterLogsFromEvents(ctx, ces, e.chainStore, e.stateManager) + if err != nil { + return nil, err + } + var out []ethtypes.EthLog + for _, log := range logs { + if log.TransactionHash == txHash { + out = append(out, log) + } + } + return out, nil +} + +// GC runs a garbage collection loop, deleting filters that have not been used within the ttl window +func (e *ethEvents) GC(ctx context.Context, ttl time.Duration) { + if e.filterStore == nil { + return + } + + tt := time.NewTicker(time.Minute * 30) + defer tt.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-tt.C: + fs := e.filterStore.NotTakenSince(time.Now().Add(-ttl)) + for _, f := range fs { + if err := e.uninstallFilter(ctx, f); err != nil { + log.Warnf("Failed to remove actor event filter during garbage collection: %v", err) + } + } + } + } +} + +func (e *ethEvents) ethGetEventsForFilter(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) ([]*index.CollectedEvent, error) { + if e.eventFilterManager == nil { + return nil, api.ErrNotSupported + } + + if e.chainIndexer == nil { + return nil, ErrChainIndexerDisabled + } + + pf, err := e.parseEthFilterSpec(filterSpec) + if err != nil { + return nil, xerrors.Errorf("failed to parse eth filter spec: %w", err) + } + + head := e.chainStore.GetHeaviestTipSet() + // should not ask for events for a tipset >= head because of deferred execution + if pf.tipsetCid != cid.Undef { + ts, err := e.chainStore.GetTipSetByCid(ctx, pf.tipsetCid) + if err != nil { + return nil, xerrors.Errorf("failed to get tipset by cid: %w", err) + } + if ts.Height() >= head.Height() { + return nil, xerrors.New("cannot ask for events for a tipset at or greater than head") + } + } + + if pf.minHeight >= head.Height() || pf.maxHeight >= head.Height() { + return nil, xerrors.New("cannot ask for events for a tipset at or greater than head") + } + + ef := &index.EventFilter{ + MinHeight: pf.minHeight, + MaxHeight: pf.maxHeight, + TipsetCid: pf.tipsetCid, + Addresses: pf.addresses, + KeysWithCodec: pf.keys, + Codec: multicodec.Raw, + MaxResults: e.eventFilterManager.MaxFilterResults, + } + + ces, err := e.chainIndexer.GetEventsForFilter(ctx, ef) + if err != nil { + return nil, xerrors.Errorf("failed to get events for filter from chain indexer: %w", err) + } + + return ces, nil +} + +func ethFilterResultFromEvents(ctx context.Context, evs []*index.CollectedEvent, cs ChainStore, sa StateManager) (*ethtypes.EthFilterResult, error) { + logs, err := ethFilterLogsFromEvents(ctx, evs, cs, sa) + if err != nil { + return nil, err + } + + res := ðtypes.EthFilterResult{} + for _, log := range logs { + 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) (*ethtypes.EthFilterResult, error) { + res := ðtypes.EthFilterResult{} + + for _, c := range cs { + hash, err := ethTxHashFromSignedMessage(c) + if err != nil { + return nil, err + } + + res.Results = append(res.Results, hash) + } + + return res, nil +} + +func ethFilterLogsFromEvents(ctx context.Context, evs []*index.CollectedEvent, cs ChainStore, sa StateManager) ([]ethtypes.EthLog, error) { + var logs []ethtypes.EthLog + 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(ctx, ev.MsgCid, cs) + if err != nil { + return nil, err + } + if log.TransactionHash == ethtypes.EmptyEthHash { + // We've garbage collected the message, ignore the events and continue. + continue + } + c, err := ev.TipSetKey.Cid() + if err != nil { + return nil, err + } + log.BlockHash, err = ethtypes.EthHashFromCid(c) + if err != nil { + return nil, err + } + + logs = append(logs, log) + } + + return logs, nil +} + +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. Built-in actors emit CBOR, and anything else would be + // invalid anyway. + if entry.Codec != cid.Raw { + 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 ethTxHashFromMessageCid(ctx context.Context, c cid.Cid, cs ChainStore) (ethtypes.EthHash, error) { + smsg, err := cs.GetSignedMessage(ctx, c) + if err == nil { + // This is an Eth Tx, Secp message, Or BLS message in the mpool + return ethTxHashFromSignedMessage(smsg) + } + + _, err = cs.GetMessage(ctx, c) + if err == nil { + // This is a BLS message + return ethtypes.EthHashFromCid(c) + } + + return ethtypes.EmptyEthHash, nil +} + +// parseBlockRange is similar to actor event's parseHeightRange but with slightly different semantics +// +// * "block" instead of "height" +// * strings that can have "latest" and "earliest" and nil +// * hex strings for actual heights +func parseBlockRange(heaviest abi.ChainEpoch, fromBlock, toBlock *string, maxRange abi.ChainEpoch) (minHeight abi.ChainEpoch, maxHeight abi.ChainEpoch, err error) { + if fromBlock == nil || *fromBlock == "latest" || len(*fromBlock) == 0 { + minHeight = heaviest + } else if *fromBlock == "earliest" { + minHeight = 0 + } else { + if !strings.HasPrefix(*fromBlock, "0x") { + return 0, 0, xerrors.New("FromBlock is not a hex") + } + epoch, err := ethtypes.EthUint64FromHex(*fromBlock) + if err != nil || epoch > math.MaxInt64 { + return 0, 0, xerrors.New("invalid epoch") + } + minHeight = abi.ChainEpoch(epoch) + } + + if toBlock == nil || *toBlock == "latest" || len(*toBlock) == 0 { + // here latest means the latest at the time + maxHeight = -1 + } else if *toBlock == "earliest" { + maxHeight = 0 + } else { + if !strings.HasPrefix(*toBlock, "0x") { + return 0, 0, xerrors.New("ToBlock is not a hex") + } + epoch, err := ethtypes.EthUint64FromHex(*toBlock) + if err != nil || epoch > math.MaxInt64 { + return 0, 0, xerrors.New("invalid epoch") + } + maxHeight = abi.ChainEpoch(epoch) + } + + // Validate height ranges are within limits set by node operator + if minHeight == -1 && maxHeight > 0 { + // Here the client is looking for events between the head and some future height + if maxHeight-heaviest > maxRange { + return 0, 0, xerrors.Errorf("invalid epoch range: to block is too far in the future (maximum: %d)", maxRange) + } + } else if minHeight >= 0 && maxHeight == -1 { + // Here the client is looking for events between some time in the past and the current head + if heaviest-minHeight > maxRange { + return 0, 0, xerrors.Errorf("invalid epoch range: from block is too far in the past (maximum: %d)", maxRange) + } + } else if minHeight >= 0 && maxHeight >= 0 { + if minHeight > maxHeight { + return 0, 0, xerrors.Errorf("invalid epoch range: to block (%d) must be after from block (%d)", minHeight, maxHeight) + } else if maxHeight-minHeight > maxRange { + return 0, 0, xerrors.Errorf("invalid epoch range: range between to and from blocks is too large (maximum: %d)", maxRange) + } + } + return minHeight, maxHeight, nil +} + +type parsedFilter struct { + minHeight abi.ChainEpoch + maxHeight abi.ChainEpoch + tipsetCid cid.Cid + addresses []address.Address + keys map[string][]types.ActorEventBlock +} + +func (e *ethEvents) parseEthFilterSpec(filterSpec *ethtypes.EthFilterSpec) (*parsedFilter, error) { + var ( + minHeight abi.ChainEpoch + maxHeight abi.ChainEpoch + tipsetCid cid.Cid + addresses []address.Address + keys = map[string][][]byte{} + ) + + if filterSpec.BlockHash != nil { + if filterSpec.FromBlock != nil || filterSpec.ToBlock != nil { + return nil, xerrors.New("must not specify block hash and from/to block") + } + + tipsetCid = filterSpec.BlockHash.ToCid() + } else { + var err error + // Because of deferred execution, we need to subtract 1 from the heaviest tipset height for the "heaviest" parameter + minHeight, maxHeight, err = parseBlockRange(e.chainStore.GetHeaviestTipSet().Height()-1, filterSpec.FromBlock, filterSpec.ToBlock, e.maxFilterHeightRange) + if err != nil { + return nil, err + } + } + + // Convert all addresses to filecoin f4 addresses + for _, ea := range filterSpec.Address { + a, err := ea.ToFilecoinAddress() + if err != nil { + return nil, xerrors.Errorf("invalid address %x", ea) + } + addresses = append(addresses, a) + } + + keys, err := parseEthTopics(filterSpec.Topics) + if err != nil { + return nil, err + } + + return &parsedFilter{ + minHeight: minHeight, + maxHeight: maxHeight, + tipsetCid: tipsetCid, + addresses: addresses, + keys: keysToKeysWithCodec(keys), + }, nil +} + +func keysToKeysWithCodec(keys map[string][][]byte) map[string][]types.ActorEventBlock { + keysWithCodec := make(map[string][]types.ActorEventBlock) + for k, v := range keys { + for _, vv := range v { + keysWithCodec[k] = append(keysWithCodec[k], types.ActorEventBlock{ + Codec: uint64(multicodec.Raw), // FEVM smart contract events are always encoded with the `raw` Codec. + Value: vv, + }) + } + } + return keysWithCodec +} + +func (e *ethEvents) uninstallFilter(ctx context.Context, f filter.Filter) error { + switch f.(type) { + case filter.EventFilter: + err := e.eventFilterManager.Remove(ctx, f.ID()) + if err != nil && !errors.Is(err, filter.ErrFilterNotFound) { + return err + } + case *filter.TipSetFilter: + err := e.tipSetFilterManager.Remove(ctx, f.ID()) + if err != nil && !errors.Is(err, filter.ErrFilterNotFound) { + return err + } + case *filter.MemPoolFilter: + err := e.memPoolFilterManager.Remove(ctx, f.ID()) + if err != nil && !errors.Is(err, filter.ErrFilterNotFound) { + return err + } + default: + return xerrors.New("unknown filter type") + } + + return e.filterStore.Remove(ctx, f.ID()) +} + +type EthEventsDisabled struct{} + +func (EthEventsDisabled) EthGetLogs(ctx context.Context, filter *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) { + return nil, ErrModuleDisabled +} +func (EthEventsDisabled) EthNewBlockFilter(ctx context.Context) (ethtypes.EthFilterID, error) { + return ethtypes.EthFilterID{}, ErrModuleDisabled +} +func (EthEventsDisabled) EthNewPendingTransactionFilter(ctx context.Context) (ethtypes.EthFilterID, error) { + return ethtypes.EthFilterID{}, ErrModuleDisabled +} +func (EthEventsDisabled) EthNewFilter(ctx context.Context, filter *ethtypes.EthFilterSpec) (ethtypes.EthFilterID, error) { + return ethtypes.EthFilterID{}, ErrModuleDisabled +} +func (EthEventsDisabled) EthUninstallFilter(ctx context.Context, id ethtypes.EthFilterID) (bool, error) { + return false, ErrModuleDisabled +} +func (EthEventsDisabled) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { + return nil, ErrModuleDisabled +} +func (EthEventsDisabled) EthGetFilterLogs(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { + return nil, ErrModuleDisabled +} +func (EthEventsDisabled) EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) { + return ethtypes.EthSubscriptionID{}, ErrModuleDisabled +} +func (EthEventsDisabled) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) { + return false, ErrModuleDisabled +} diff --git a/node/impl/eth/events_test.go b/node/impl/eth/events_test.go new file mode 100644 index 00000000000..8849d870160 --- /dev/null +++ b/node/impl/eth/events_test.go @@ -0,0 +1,180 @@ +package eth + +import ( + "fmt" + "testing" + + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/abi" + + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +func TestParseBlockRange(t *testing.T) { + pstring := func(s string) *string { return &s } + + tcs := map[string]struct { + heaviest abi.ChainEpoch + from *string + to *string + maxRange abi.ChainEpoch + minOut abi.ChainEpoch + maxOut abi.ChainEpoch + errStr string + }{ + "fails when both are specified and range is greater than max allowed range": { + heaviest: 100, + from: pstring("0x100"), + to: pstring("0x200"), + maxRange: 10, + minOut: 0, + maxOut: 0, + errStr: "too large", + }, + "fails when min is specified and range is greater than max allowed range": { + heaviest: 500, + from: pstring("0x10"), + to: pstring("latest"), + maxRange: 10, + minOut: 0, + maxOut: 0, + errStr: "too far in the past", + }, + "fails when max is specified and range is greater than max allowed range": { + heaviest: 500, + from: pstring("earliest"), + to: pstring("0x10000"), + maxRange: 10, + minOut: 0, + maxOut: 0, + errStr: "too large", + }, + "works when range is valid": { + heaviest: 500, + from: pstring("earliest"), + to: pstring("latest"), + maxRange: 1000, + minOut: 0, + maxOut: -1, + }, + "works when range is valid and specified": { + heaviest: 500, + from: pstring("0x10"), + to: pstring("0x30"), + maxRange: 1000, + minOut: 16, + maxOut: 48, + }, + } + + for name, tc := range tcs { + tc2 := tc + t.Run(name, func(t *testing.T) { + min, max, err := parseBlockRange(tc2.heaviest, tc2.from, tc2.to, tc2.maxRange) + require.Equal(t, tc2.minOut, min) + require.Equal(t, tc2.maxOut, max) + if tc2.errStr != "" { + fmt.Println(err) + require.Error(t, err) + require.Contains(t, err.Error(), tc2.errStr) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestEthLogFromEvent(t *testing.T) { + // basic empty + data, topics, ok := ethLogFromEvent(nil) + require.True(t, ok) + require.Nil(t, data) + require.Empty(t, topics) + require.NotNil(t, topics) + + // basic topic + data, topics, ok = ethLogFromEvent([]types.EventEntry{{ + Flags: 0, + Key: "t1", + Codec: cid.Raw, + Value: make([]byte, 32), + }}) + require.True(t, ok) + require.Nil(t, data) + require.Len(t, topics, 1) + require.Equal(t, topics[0], ethtypes.EthHash{}) + + // basic topic with data + data, topics, ok = ethLogFromEvent([]types.EventEntry{{ + Flags: 0, + Key: "t1", + Codec: cid.Raw, + Value: make([]byte, 32), + }, { + Flags: 0, + Key: "d", + Codec: cid.Raw, + Value: []byte{0x0}, + }}) + require.True(t, ok) + require.Equal(t, data, []byte{0x0}) + require.Len(t, topics, 1) + require.Equal(t, topics[0], ethtypes.EthHash{}) + + // skip topic + _, _, ok = ethLogFromEvent([]types.EventEntry{{ + Flags: 0, + Key: "t2", + Codec: cid.Raw, + Value: make([]byte, 32), + }}) + require.False(t, ok) + + // duplicate topic + _, _, ok = ethLogFromEvent([]types.EventEntry{{ + Flags: 0, + Key: "t1", + Codec: cid.Raw, + Value: make([]byte, 32), + }, { + Flags: 0, + Key: "t1", + Codec: cid.Raw, + Value: make([]byte, 32), + }}) + require.False(t, ok) + + // duplicate data + _, _, ok = ethLogFromEvent([]types.EventEntry{{ + Flags: 0, + Key: "d", + Codec: cid.Raw, + Value: make([]byte, 32), + }, { + Flags: 0, + Key: "d", + Codec: cid.Raw, + Value: make([]byte, 32), + }}) + require.False(t, ok) + + // unknown key is fine + data, topics, ok = ethLogFromEvent([]types.EventEntry{{ + Flags: 0, + Key: "t5", + Codec: cid.Raw, + Value: make([]byte, 32), + }, { + Flags: 0, + Key: "t1", + Codec: cid.Raw, + Value: make([]byte, 32), + }}) + require.True(t, ok) + require.Nil(t, data) + require.Len(t, topics, 1) + require.Equal(t, topics[0], ethtypes.EthHash{}) +} diff --git a/node/impl/eth/filecoin.go b/node/impl/eth/filecoin.go new file mode 100644 index 00000000000..bb0708e83a0 --- /dev/null +++ b/node/impl/eth/filecoin.go @@ -0,0 +1,92 @@ +package eth + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-jsonrpc" + + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +type EthFilecoinAPI interface { + EthAddressToFilecoinAddress(ctx context.Context, ethAddress ethtypes.EthAddress) (address.Address, error) + FilecoinAddressToEthAddress(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthAddress, error) +} + +var _ EthFilecoinAPI = (*ethFilecoin)(nil) + +type ethFilecoin struct { + chainStore ChainStore + stateManager StateManager +} + +func NewEthFilecoinAPI(chainStore ChainStore, stateManager StateManager) EthFilecoinAPI { + return ðFilecoin{ + chainStore: chainStore, + stateManager: stateManager, + } +} + +func (e *ethFilecoin) EthAddressToFilecoinAddress(ctx context.Context, ethAddress ethtypes.EthAddress) (address.Address, error) { + return ethAddress.ToFilecoinAddress() +} + +func (e *ethFilecoin) FilecoinAddressToEthAddress(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthAddress, error) { + params, err := jsonrpc.DecodeParams[ethtypes.FilecoinAddressToEthAddressParams](p) + if err != nil { + return ethtypes.EthAddress{}, xerrors.Errorf("decoding params: %w", err) + } + + filecoinAddress := params.FilecoinAddress + + // If the address is an "f0" or "f4" address, `EthAddressFromFilecoinAddress` will return the corresponding Ethereum address right away. + if eaddr, err := ethtypes.EthAddressFromFilecoinAddress(filecoinAddress); err == nil { + return eaddr, nil + } else if err != ethtypes.ErrInvalidAddress { + return ethtypes.EthAddress{}, xerrors.Errorf("error converting filecoin address to eth address: %w", err) + } + + // We should only be dealing with "f1"/"f2"/"f3" addresses from here-on. + switch filecoinAddress.Protocol() { + case address.SECP256K1, address.Actor, address.BLS: + // Valid protocols + default: + // Ideally, this should never happen but is here for sanity checking. + return ethtypes.EthAddress{}, xerrors.Errorf("invalid filecoin address protocol: %s", filecoinAddress.String()) + } + + var blkParam string + if params.BlkParam == nil { + blkParam = "finalized" + } else { + blkParam = *params.BlkParam + } + + ts, err := getTipsetByBlockNumber(ctx, e.chainStore, blkParam, false) + if err != nil { + return ethtypes.EthAddress{}, err + } + + // Lookup the ID address + idAddr, err := e.stateManager.LookupIDAddress(ctx, filecoinAddress, ts) + if err != nil { + return ethtypes.EthAddress{}, xerrors.Errorf( + "failed to lookup ID address for given Filecoin address %s ("+ + "ensure that the address has been instantiated on-chain and sufficient epochs have passed since instantiation to confirm to the given 'blkParam': \"%s\"): %w", + filecoinAddress, + blkParam, + err, + ) + } + + // Convert the ID address an ETH address + ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(idAddr) + if err != nil { + return ethtypes.EthAddress{}, xerrors.Errorf("failed to convert filecoin ID address %s to eth address: %w", idAddr, err) + } + + return ethAddr, nil +} diff --git a/node/impl/eth/gas.go b/node/impl/eth/gas.go new file mode 100644 index 00000000000..db0c848f8b9 --- /dev/null +++ b/node/impl/eth/gas.go @@ -0,0 +1,493 @@ +package eth + +import ( + "bytes" + "context" + "errors" + "os" + "sort" + + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-jsonrpc" + "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/exitcode" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/build/buildconstants" + builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" + "github.com/filecoin-project/lotus/node/impl/gasutils" +) + +const maxEthFeeHistoryRewardPercentiles = 100 + +type EthGasAPI interface { + EthGasPrice(ctx context.Context) (ethtypes.EthBigInt, error) + EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthFeeHistory, error) + EthMaxPriorityFeePerGas(ctx context.Context) (ethtypes.EthBigInt, error) + EthEstimateGas(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthUint64, error) + EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) +} + +var ( + _ EthGasAPI = (*ethGas)(nil) + _ EthGasAPI = (*EthGasDisabled)(nil) +) + +var minGasPremium = ethtypes.EthBigInt(types.NewInt(gasutils.MinGasPremium)) + +type ethGas struct { + chainStore ChainStore + stateManager StateManager + messagePool MessagePool + gasApi GasAPI +} + +func NewEthGasAPI(chainStore ChainStore, stateManager StateManager, messagePool MessagePool, gasApi GasAPI) EthGasAPI { + return ðGas{ + chainStore: chainStore, + stateManager: stateManager, + messagePool: messagePool, + gasApi: gasApi, + } +} + +func (e *ethGas) EthGasPrice(ctx context.Context) (ethtypes.EthBigInt, error) { + // According to Geth's implementation, eth_gasPrice should return base + tip + // Ref: https://github.com/ethereum/pm/issues/328#issuecomment-853234014 + + ts := e.chainStore.GetHeaviestTipSet() + baseFee := ts.Blocks()[0].ParentBaseFee + + premium, err := e.EthMaxPriorityFeePerGas(ctx) + if err != nil { + return ethtypes.EthBigInt(big.Zero()), nil + } + + gasPrice := big.Add(baseFee, big.Int(premium)) + return ethtypes.EthBigInt(gasPrice), nil +} + +func (e *ethGas) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthFeeHistory, error) { + params, err := jsonrpc.DecodeParams[ethtypes.EthFeeHistoryParams](p) + if err != nil { + return ethtypes.EthFeeHistory{}, xerrors.Errorf("decoding params: %w", err) + } + if params.BlkCount > 1024 { + return ethtypes.EthFeeHistory{}, xerrors.New("block count should be smaller than 1024") + } + rewardPercentiles := make([]float64, 0) + if params.RewardPercentiles != nil { + if len(*params.RewardPercentiles) > maxEthFeeHistoryRewardPercentiles { + return ethtypes.EthFeeHistory{}, xerrors.New("length of the reward percentile array cannot be greater than 100") + } + rewardPercentiles = append(rewardPercentiles, *params.RewardPercentiles...) + } + for i, rp := range rewardPercentiles { + if rp < 0 || rp > 100 { + return ethtypes.EthFeeHistory{}, xerrors.Errorf("invalid reward percentile: %f should be between 0 and 100", rp) + } + if i > 0 && rp < rewardPercentiles[i-1] { + return ethtypes.EthFeeHistory{}, xerrors.Errorf("invalid reward percentile: %f should be larger than %f", rp, rewardPercentiles[i-1]) + } + } + + ts, err := getTipsetByBlockNumber(ctx, e.chainStore, params.NewestBlkNum, false) + if err != nil { + return ethtypes.EthFeeHistory{}, err + } + + var ( + basefee = ts.Blocks()[0].ParentBaseFee + oldestBlkHeight = uint64(1) + + // NOTE: baseFeePerGas should include the next block after the newest of the returned range, + // because the next base fee can be inferred from the messages in the newest block. + // However, this is NOT the case in Filecoin due to deferred execution, so the best + // we can do is duplicate the last value. + baseFeeArray = []ethtypes.EthBigInt{ethtypes.EthBigInt(basefee)} + rewardsArray = make([][]ethtypes.EthBigInt, 0) + gasUsedRatioArray = []float64{} + blocksIncluded int + ) + + for blocksIncluded < int(params.BlkCount) && ts.Height() > 0 { + basefee = ts.Blocks()[0].ParentBaseFee + _, msgs, rcpts, err := executeTipset(ctx, ts, e.chainStore, e.stateManager) + if err != nil { + return ethtypes.EthFeeHistory{}, xerrors.Errorf("failed to retrieve messages and receipts for height %d: %w", ts.Height(), err) + } + + txGasRewards := gasRewardSorter{} + for i, msg := range msgs { + effectivePremium := msg.VMMessage().EffectiveGasPremium(basefee) + txGasRewards = append(txGasRewards, gasRewardTuple{ + premium: effectivePremium, + gasUsed: rcpts[i].GasUsed, + }) + } + + rewards, totalGasUsed := calculateRewardsAndGasUsed(rewardPercentiles, txGasRewards) + maxGas := buildconstants.BlockGasLimit * int64(len(ts.Blocks())) + + // arrays should be reversed at the end + baseFeeArray = append(baseFeeArray, ethtypes.EthBigInt(basefee)) + gasUsedRatioArray = append(gasUsedRatioArray, float64(totalGasUsed)/float64(maxGas)) + rewardsArray = append(rewardsArray, rewards) + oldestBlkHeight = uint64(ts.Height()) + blocksIncluded++ + + parentTsKey := ts.Parents() + ts, err = e.chainStore.LoadTipSet(ctx, parentTsKey) + if err != nil { + return ethtypes.EthFeeHistory{}, xerrors.Errorf("cannot load tipset key: %v", parentTsKey) + } + } + + // Reverse the arrays; we collected them newest to oldest; the client expects oldest to newest. + for i, j := 0, len(baseFeeArray)-1; i < j; i, j = i+1, j-1 { + baseFeeArray[i], baseFeeArray[j] = baseFeeArray[j], baseFeeArray[i] + } + for i, j := 0, len(gasUsedRatioArray)-1; i < j; i, j = i+1, j-1 { + gasUsedRatioArray[i], gasUsedRatioArray[j] = gasUsedRatioArray[j], gasUsedRatioArray[i] + } + for i, j := 0, len(rewardsArray)-1; i < j; i, j = i+1, j-1 { + rewardsArray[i], rewardsArray[j] = rewardsArray[j], rewardsArray[i] + } + + ret := ethtypes.EthFeeHistory{ + OldestBlock: ethtypes.EthUint64(oldestBlkHeight), + BaseFeePerGas: baseFeeArray, + GasUsedRatio: gasUsedRatioArray, + } + if params.RewardPercentiles != nil { + ret.Reward = &rewardsArray + } + return ret, nil +} + +func (e *ethGas) EthMaxPriorityFeePerGas(ctx context.Context) (ethtypes.EthBigInt, error) { + gasPremium, err := e.gasApi.GasEstimateGasPremium(ctx, 0, builtinactors.SystemActorAddr, 10000, types.EmptyTSK) + if err != nil { + return ethtypes.EthBigInt(big.Zero()), err + } + return ethtypes.EthBigInt(gasPremium), nil +} + +func (e *ethGas) EthEstimateGas(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthUint64, error) { + params, err := jsonrpc.DecodeParams[ethtypes.EthEstimateGasParams](p) + if err != nil { + return ethtypes.EthUint64(0), xerrors.Errorf("decoding params: %w", err) + } + + msg, err := ethCallToFilecoinMessage(ctx, params.Tx) + if err != nil { + return ethtypes.EthUint64(0), err + } + + // Set the gas limit to the zero sentinel value, which makes + // gas estimation actually run. + msg.GasLimit = 0 + + var ts *types.TipSet + if params.BlkParam == nil { + ts = e.chainStore.GetHeaviestTipSet() + } else { + ts, err = getTipsetByEthBlockNumberOrHash(ctx, e.chainStore, *params.BlkParam) + if err != nil { + return ethtypes.EthUint64(0), xerrors.Errorf("failed to process block param: %v; %w", params.BlkParam, err) + } + } + + gassedMsg, err := e.gasApi.GasEstimateMessageGas(ctx, msg, nil, ts.Key()) + if err != nil { + // On failure, GasEstimateMessageGas doesn't actually return the invocation result, + // it just returns an error. That means we can't get the revert reason. + // + // So we re-execute the message with EthCall (well, applyMessage which contains the + // guts of EthCall). This will give us an ethereum specific error with revert + // information. + msg.GasLimit = buildconstants.BlockGasLimit + if _, err2 := e.applyMessage(ctx, msg, ts.Key()); err2 != nil { + // If err2 is an ExecutionRevertedError, return it + var ed *api.ErrExecutionReverted + if errors.As(err2, &ed) { + return ethtypes.EthUint64(0), err2 + } + + // Otherwise, return the error from applyMessage with failed to estimate gas + err = err2 + } + + return ethtypes.EthUint64(0), xerrors.Errorf("failed to estimate gas: %w", err) + } + + expectedGas, err := ethGasSearch(ctx, e.chainStore, e.stateManager, e.messagePool, gassedMsg, ts) + if err != nil { + return 0, xerrors.Errorf("gas search failed: %w", err) + } + + return ethtypes.EthUint64(expectedGas), nil +} + +func (e *ethGas) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { + msg, err := ethCallToFilecoinMessage(ctx, tx) + if err != nil { + return nil, xerrors.Errorf("failed to convert ethcall to filecoin message: %w", err) + } + + ts, err := getTipsetByEthBlockNumberOrHash(ctx, e.chainStore, blkParam) + if err != nil { + return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) + } + + invokeResult, err := e.applyMessage(ctx, msg, ts.Key()) + if err != nil { + return nil, err + } + + if msg.To == builtintypes.EthereumAddressManagerActorAddr { + return ethtypes.EthBytes{}, nil + } else if len(invokeResult.MsgRct.Return) > 0 { + return cbg.ReadByteArray(bytes.NewReader(invokeResult.MsgRct.Return), uint64(len(invokeResult.MsgRct.Return))) + } + + return ethtypes.EthBytes{}, nil +} + +func (e *ethGas) applyMessage(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (res *api.InvocResult, err error) { + ts, err := e.chainStore.GetTipSetFromKey(ctx, tsk) + if err != nil { + return nil, xerrors.Errorf("cannot get tipset: %w", err) + } + + if ts.Height() > 0 { + pts, err := e.chainStore.GetTipSetFromKey(ctx, ts.Parents()) + if err != nil { + return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err) + } + // Check for expensive forks from the parents to the tipset, including nil tipsets + if e.stateManager.HasExpensiveForkBetween(pts.Height(), ts.Height()+1) { + return nil, stmgr.ErrExpensiveFork + } + } + + st, _, err := e.stateManager.TipSetState(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("cannot get tipset state: %w", err) + } + res, err = e.stateManager.ApplyOnStateWithGas(ctx, st, msg, ts) + if err != nil { + return nil, xerrors.Errorf("ApplyWithGasOnState failed: %w", err) + } + + if res.MsgRct.ExitCode.IsError() { + reason := "none" + var cbytes abi.CborBytes + if err := cbytes.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil { + log.Warnw("failed to unmarshal cbor bytes from message receipt return", "error", err) + reason = "ERROR: revert reason is not cbor encoded bytes" + } // else leave as empty bytes + if len(cbytes) > 0 { + reason = parseEthRevert(cbytes) + } + return nil, api.NewErrExecutionReverted(res.MsgRct.ExitCode, reason, res.Error, cbytes) + } + + return res, nil +} + +// ethGasSearch executes a message for gas estimation using the previously estimated gas. +// If the message fails due to an out of gas error then a gas search is performed. +// See gasSearch. +func ethGasSearch( + ctx context.Context, + chainStore ChainStore, + stateManager StateManager, + messagePool MessagePool, + msgIn *types.Message, + ts *types.TipSet, +) (int64, error) { + msg := *msgIn + currTs := ts + + res, priorMsgs, ts, err := gasutils.GasEstimateCallWithGas(ctx, chainStore, stateManager, messagePool, &msg, currTs) + if err != nil { + return -1, xerrors.Errorf("gas estimation failed: %w", err) + } + + if res.MsgRct.ExitCode.IsSuccess() { + return msg.GasLimit, nil + } + + if traceContainsExitCode(res.ExecutionTrace, exitcode.SysErrOutOfGas) { + ret, err := gasSearch(ctx, stateManager, &msg, priorMsgs, ts) + if err != nil { + return -1, xerrors.Errorf("gas estimation search failed: %w", err) + } + + ret = int64(float64(ret) * messagePool.GetConfig().GasLimitOverestimation) + return ret, nil + } + + return -1, xerrors.Errorf("message execution failed: exit %s, reason: %s", res.MsgRct.ExitCode, res.Error) +} + +func traceContainsExitCode(et types.ExecutionTrace, ex exitcode.ExitCode) bool { + if et.MsgRct.ExitCode == ex { + return true + } + + for _, et := range et.Subcalls { + if traceContainsExitCode(et, ex) { + return true + } + } + + return false +} + +// gasSearch does an exponential search to find a gas value to execute the +// message with. It first finds a high gas limit that allows the message to execute +// by doubling the previous gas limit until it succeeds then does a binary +// search till it gets within a range of 1% +func gasSearch( + ctx context.Context, + stateManager StateManager, + msgIn *types.Message, + priorMsgs []types.ChainMsg, + ts *types.TipSet, +) (int64, error) { + msg := *msgIn + + high := msg.GasLimit + low := msg.GasLimit + + applyTsMessages := true + if os.Getenv("LOTUS_SKIP_APPLY_TS_MESSAGE_CALL_WITH_GAS") == "1" { + applyTsMessages = false + } + + canSucceed := func(limit int64) (bool, error) { + msg.GasLimit = limit + + res, err := stateManager.CallWithGas(ctx, &msg, priorMsgs, ts, applyTsMessages) + if err != nil { + return false, xerrors.Errorf("CallWithGas failed: %w", err) + } + + if res.MsgRct.ExitCode.IsSuccess() { + return true, nil + } + + return false, nil + } + + for { + ok, err := canSucceed(high) + if err != nil { + return -1, xerrors.Errorf("searching for high gas limit failed: %w", err) + } + if ok { + break + } + + low = high + high = high * 2 + + if high > buildconstants.BlockGasLimit { + high = buildconstants.BlockGasLimit + break + } + } + + checkThreshold := high / 100 + for (high - low) > checkThreshold { + median := (low + high) / 2 + ok, err := canSucceed(median) + if err != nil { + return -1, xerrors.Errorf("searching for optimal gas limit failed: %w", err) + } + + if ok { + high = median + } else { + low = median + } + + checkThreshold = median / 100 + } + + return high, nil +} + +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] = 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) + } + + return rewards, gasUsedTotal +} + +type gasRewardTuple struct { + gasUsed int64 + premium abi.TokenAmount +} + +// sorted in ascending order +type gasRewardSorter []gasRewardTuple + +func (g gasRewardSorter) Len() int { return len(g) } +func (g gasRewardSorter) Swap(i, j int) { + g[i], g[j] = g[j], g[i] +} +func (g gasRewardSorter) Less(i, j int) bool { + return g[i].premium.Int.Cmp(g[j].premium.Int) == -1 +} + +type EthGasDisabled struct{} + +func (EthGasDisabled) EthGasPrice(ctx context.Context) (ethtypes.EthBigInt, error) { + return ethtypes.EthBigInt{}, ErrModuleDisabled +} +func (EthGasDisabled) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthFeeHistory, error) { + return ethtypes.EthFeeHistory{}, ErrModuleDisabled +} +func (EthGasDisabled) EthMaxPriorityFeePerGas(ctx context.Context) (ethtypes.EthBigInt, error) { + return ethtypes.EthBigInt{}, ErrModuleDisabled +} +func (EthGasDisabled) EthEstimateGas(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthUint64, error) { + return ethtypes.EthUint64(0), ErrModuleDisabled +} +func (EthGasDisabled) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { + return nil, ErrModuleDisabled +} diff --git a/node/impl/eth/lookup.go b/node/impl/eth/lookup.go new file mode 100644 index 00000000000..94f2775bf0f --- /dev/null +++ b/node/impl/eth/lookup.go @@ -0,0 +1,313 @@ +package eth + +import ( + "bytes" + "context" + "errors" + + "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/evm" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/build/buildconstants" + "github.com/filecoin-project/lotus/chain/actors" + builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" + "github.com/filecoin-project/lotus/node/modules/dtypes" +) + +type EthLookupAPI interface { + EthGetCode(ctx context.Context, address ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) + EthGetStorageAt(ctx context.Context, ethAddr ethtypes.EthAddress, position ethtypes.EthBytes, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) + EthGetBalance(ctx context.Context, address ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBigInt, error) +} + +var ( + _ EthLookupAPI = (*ethLookup)(nil) + _ EthLookupAPI = (*EthLookupDisabled)(nil) +) + +type ethLookup struct { + chainStore ChainStore + stateManager StateManager + syncApi SyncAPI + stateBlockstore dtypes.StateBlockstore +} + +func NewEthLookupAPI( + chainStore ChainStore, + stateManager StateManager, + syncApi SyncAPI, + stateBlockstore dtypes.StateBlockstore, +) EthLookupAPI { + return ðLookup{ + chainStore: chainStore, + stateManager: stateManager, + syncApi: syncApi, + stateBlockstore: stateBlockstore, + } +} + +// EthGetCode returns string value of the compiled bytecode +func (e *ethLookup) EthGetCode(ctx context.Context, ethAddr ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { + to, err := ethAddr.ToFilecoinAddress() + if err != nil { + return nil, xerrors.Errorf("cannot get Filecoin address: %w", err) + } + + ts, err := getTipsetByEthBlockNumberOrHash(ctx, e.chainStore, blkParam) + if err != nil { + return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) + } + + // StateManager.Call will panic if there is no parent + if ts.Height() == 0 { + return nil, xerrors.New("block param must not specify genesis block") + } + + actor, err := e.stateManager.LoadActor(ctx, to, ts) + if err != nil { + if errors.Is(err, types.ErrActorNotFound) { + return nil, nil + } + return nil, xerrors.Errorf("failed to lookup contract %s: %w", ethAddr, err) + } + + // Not a contract. We could try to distinguish between accounts and "native" contracts here, + // but it's not worth it. + if !builtinactors.IsEvmActor(actor.Code) { + return nil, nil + } + + msg := &types.Message{ + From: builtinactors.SystemActorAddr, + To: to, + Value: big.Zero(), + Method: builtintypes.MethodsEVM.GetBytecode, + Params: nil, + GasLimit: buildconstants.BlockGasLimit, + GasFeeCap: big.Zero(), + GasPremium: big.Zero(), + } + + // Try calling until we find a height with no migration. + var res *api.InvocResult + for { + res, err = e.stateManager.Call(ctx, msg, ts) + if err != stmgr.ErrExpensiveFork { + break + } + ts, err = e.chainStore.GetTipSetFromKey(ctx, ts.Parents()) + if err != nil { + return nil, xerrors.Errorf("getting parent tipset: %w", err) + } + } + + if err != nil { + return nil, xerrors.Errorf("failed to call GetBytecode: %w", err) + } + + if res.MsgRct == nil { + return nil, xerrors.New("no message receipt") + } + + if res.MsgRct.ExitCode.IsError() { + return nil, xerrors.Errorf("GetBytecode failed: %s", res.Error) + } + + var getBytecodeReturn evm.GetBytecodeReturn + if err := getBytecodeReturn.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil { + return nil, xerrors.Errorf("failed to decode EVM bytecode CID: %w", err) + } + + // The contract has selfdestructed, so the code is "empty". + if getBytecodeReturn.Cid == nil { + return nil, nil + } + + blk, err := e.stateBlockstore.Get(ctx, *getBytecodeReturn.Cid) + if err != nil { + return nil, xerrors.Errorf("failed to get EVM bytecode: %w", err) + } + + return blk.RawData(), nil +} + +func (e *ethLookup) EthGetStorageAt(ctx context.Context, ethAddr ethtypes.EthAddress, position ethtypes.EthBytes, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { + ts, err := getTipsetByEthBlockNumberOrHash(ctx, e.chainStore, blkParam) + if err != nil { + return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) + } + + pl := len(position) + if pl > 32 { + return nil, xerrors.New("supplied storage key is too long") + } + + // pad with zero bytes if smaller than 32 bytes + position = append(make([]byte, 32-pl, 32), position...) + + to, err := ethAddr.ToFilecoinAddress() + if err != nil { + return nil, xerrors.Errorf("cannot get Filecoin address: %w", err) + } + + // use the system actor as the caller + from, err := address.NewIDAddress(0) + if err != nil { + return nil, xerrors.Errorf("failed to construct system sender address: %w", err) + } + + actor, err := e.stateManager.LoadActor(ctx, to, ts) + if err != nil { + if errors.Is(err, types.ErrActorNotFound) { + return ethtypes.EthBytes(make([]byte, 32)), nil + } + return nil, xerrors.Errorf("failed to lookup contract %s: %w", ethAddr, err) + } + + if !builtinactors.IsEvmActor(actor.Code) { + return ethtypes.EthBytes(make([]byte, 32)), nil + } + + params, err := actors.SerializeParams(&evm.GetStorageAtParams{ + StorageKey: *(*[32]byte)(position), + }) + if err != nil { + return nil, xerrors.Errorf("failed to serialize parameters: %w", err) + } + + msg := &types.Message{ + From: from, + To: to, + Value: big.Zero(), + Method: builtintypes.MethodsEVM.GetStorageAt, + Params: params, + GasLimit: buildconstants.BlockGasLimit, + GasFeeCap: big.Zero(), + GasPremium: big.Zero(), + } + + // Try calling until we find a height with no migration. + var res *api.InvocResult + for { + res, err = e.stateManager.Call(ctx, msg, ts) + if err != stmgr.ErrExpensiveFork { + break + } + ts, err = e.chainStore.GetTipSetFromKey(ctx, ts.Parents()) + if err != nil { + return nil, xerrors.Errorf("getting parent tipset: %w", err) + } + } + + if err != nil { + return nil, xerrors.Errorf("Call failed: %w", err) + } + + if res.MsgRct == nil { + return nil, xerrors.New("no message receipt") + } + + if res.MsgRct.ExitCode.IsError() { + return nil, xerrors.Errorf("failed to lookup storage slot: %s", res.Error) + } + + var ret abi.CborBytes + if err := ret.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil { + return nil, xerrors.Errorf("failed to unmarshal storage slot: %w", err) + } + + // pad with zero bytes if smaller than 32 bytes + ret = append(make([]byte, 32-len(ret), 32), ret...) + + return ethtypes.EthBytes(ret), nil +} + +func (e *ethLookup) EthGetBalance(ctx context.Context, address ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBigInt, error) { + filAddr, err := address.ToFilecoinAddress() + if err != nil { + return ethtypes.EthBigInt{}, err + } + + ts, err := getTipsetByEthBlockNumberOrHash(ctx, e.chainStore, blkParam) + if err != nil { + return ethtypes.EthBigInt{}, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) + } + + st, _, err := e.stateManager.TipSetState(ctx, ts) + if err != nil { + return ethtypes.EthBigInt{}, xerrors.Errorf("failed to compute tipset state: %w", err) + } + + actor, err := e.stateManager.LoadActorRaw(ctx, filAddr, st) + if errors.Is(err, types.ErrActorNotFound) { + return ethtypes.EthBigIntZero, nil + } else if err != nil { + return ethtypes.EthBigInt{}, err + } + + return ethtypes.EthBigInt{Int: actor.Balance.Int}, nil +} + +func (e *ethLookup) EthChainId(ctx context.Context) (ethtypes.EthUint64, error) { + return ethtypes.EthUint64(buildconstants.Eip155ChainId), nil +} + +func (e *ethLookup) EthSyncing(ctx context.Context) (ethtypes.EthSyncingResult, error) { + state, err := e.syncApi.SyncState(ctx) + if err != nil { + return ethtypes.EthSyncingResult{}, xerrors.Errorf("failed calling SyncState: %w", err) + } + + if len(state.ActiveSyncs) == 0 { + return ethtypes.EthSyncingResult{}, xerrors.New("no active syncs, try again") + } + + working := -1 + for i, ss := range state.ActiveSyncs { + if ss.Stage == api.StageIdle { + continue + } + working = i + } + if working == -1 { + working = len(state.ActiveSyncs) - 1 + } + + ss := state.ActiveSyncs[working] + if ss.Base == nil || ss.Target == nil { + return ethtypes.EthSyncingResult{}, xerrors.New("missing syncing information, try again") + } + + res := ethtypes.EthSyncingResult{ + DoneSync: ss.Stage == api.StageSyncComplete, + CurrentBlock: ethtypes.EthUint64(ss.Height), + StartingBlock: ethtypes.EthUint64(ss.Base.Height()), + HighestBlock: ethtypes.EthUint64(ss.Target.Height()), + } + + return res, nil +} + +type EthLookupDisabled struct{} + +func (EthLookupDisabled) EthGetCode(ctx context.Context, address ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { + return nil, ErrModuleDisabled +} +func (EthLookupDisabled) EthGetStorageAt(ctx context.Context, ethAddr ethtypes.EthAddress, position ethtypes.EthBytes, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { + return nil, ErrModuleDisabled +} +func (EthLookupDisabled) EthGetBalance(ctx context.Context, address ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBigInt, error) { + return ethtypes.EthBigInt{}, ErrModuleDisabled +} +func (EthLookupDisabled) EthChainId(ctx context.Context) (ethtypes.EthUint64, error) { + return ethtypes.EthUint64(0), ErrModuleDisabled +} diff --git a/node/impl/eth/reward_test.go b/node/impl/eth/reward_test.go new file mode 100644 index 00000000000..2f5bafa032b --- /dev/null +++ b/node/impl/eth/reward_test.go @@ -0,0 +1,72 @@ +package eth + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/big" + + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" + "github.com/filecoin-project/lotus/node/impl/gasutils" +) + +func TestReward(t *testing.T) { + baseFee := big.NewInt(100) + testcases := []struct { + maxFeePerGas, maxPriorityFeePerGas big.Int + answer big.Int + }{ + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(200), answer: big.NewInt(200)}, + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(300), answer: big.NewInt(300)}, + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(500), answer: big.NewInt(500)}, + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(600), answer: big.NewInt(500)}, + {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(1000), answer: big.NewInt(500)}, + {maxFeePerGas: big.NewInt(50), maxPriorityFeePerGas: big.NewInt(200), answer: big.NewInt(0)}, + } + for _, tc := range testcases { + msg := &types.Message{GasFeeCap: tc.maxFeePerGas, GasPremium: tc.maxPriorityFeePerGas} + reward := msg.EffectiveGasPremium(baseFee) + require.Equal(t, 0, reward.Int.Cmp(tc.answer.Int), reward, tc.answer) + } +} + +func TestRewardPercentiles(t *testing.T) { + testcases := []struct { + percentiles []float64 + txGasRewards gasRewardSorter + answer []int64 + }{ + { + percentiles: []float64{25, 50, 75}, + txGasRewards: []gasRewardTuple{}, + answer: []int64{gasutils.MinGasPremium, gasutils.MinGasPremium, gasutils.MinGasPremium}, + }, + { + percentiles: []float64{25, 50, 75, 100}, + txGasRewards: []gasRewardTuple{ + {gasUsed: int64(0), premium: big.NewInt(300)}, + {gasUsed: int64(100), premium: big.NewInt(200)}, + {gasUsed: int64(350), premium: big.NewInt(100)}, + {gasUsed: int64(500), premium: big.NewInt(600)}, + {gasUsed: int64(300), premium: big.NewInt(700)}, + }, + answer: []int64{200, 700, 700, 700}, + }, + } + for _, tc := range testcases { + rewards, totalGasUsed := calculateRewardsAndGasUsed(tc.percentiles, tc.txGasRewards) + var gasUsed int64 + for _, tx := range tc.txGasRewards { + gasUsed += tx.gasUsed + } + ans := []ethtypes.EthBigInt{} + for _, bi := range tc.answer { + ans = append(ans, ethtypes.EthBigInt(big.NewInt(bi))) + } + require.Equal(t, totalGasUsed, gasUsed) + require.Equal(t, len(ans), len(tc.percentiles)) + require.Equal(t, ans, rewards) + } +} diff --git a/node/impl/eth/send.go b/node/impl/eth/send.go new file mode 100644 index 00000000000..6df1d151280 --- /dev/null +++ b/node/impl/eth/send.go @@ -0,0 +1,84 @@ +package eth + +import ( + "context" + + "github.com/filecoin-project/lotus/chain/index" + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +type EthSendAPI interface { + EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) + EthSendRawTransactionUntrusted(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) +} + +var ( + _ EthSendAPI = (*ethSend)(nil) + _ EthSendAPI = (*EthSendDisabled)(nil) +) + +type ethSend struct { + mpoolApi MpoolAPI + chainIndexer index.Indexer +} + +func NewEthSendAPI(mpoolApi MpoolAPI, chainIndexer index.Indexer) EthSendAPI { + return ðSend{ + mpoolApi: mpoolApi, + chainIndexer: chainIndexer, + } +} + +func (e *ethSend) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) { + return e.ethSendRawTransaction(ctx, rawTx, false) +} + +func (e *ethSend) EthSendRawTransactionUntrusted(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) { + return e.ethSendRawTransaction(ctx, rawTx, true) +} + +func (e *ethSend) ethSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes, untrusted bool) (ethtypes.EthHash, error) { + txArgs, err := ethtypes.ParseEthTransaction(rawTx) + if err != nil { + return ethtypes.EmptyEthHash, err + } + + txHash, err := txArgs.TxHash() + if err != nil { + return ethtypes.EmptyEthHash, err + } + + smsg, err := ethtypes.ToSignedFilecoinMessage(txArgs) + if err != nil { + return ethtypes.EmptyEthHash, err + } + + if untrusted { + if _, err = e.mpoolApi.MpoolPushUntrusted(ctx, smsg); err != nil { + return ethtypes.EmptyEthHash, err + } + } else { + if _, err = e.mpoolApi.MpoolPush(ctx, smsg); err != nil { + return ethtypes.EmptyEthHash, err + } + } + + // make it immediately available in the transaction hash lookup db, even though it will also + // eventually get there via the mpool + if e.chainIndexer != nil { + if err := e.chainIndexer.IndexEthTxHash(ctx, txHash, smsg.Cid()); err != nil { + log.Errorf("error indexing tx: %s", err) + } + } + + return ethtypes.EthHashFromTxBytes(rawTx), nil +} + +type EthSendDisabled struct{} + +func (EthSendDisabled) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) { + return ethtypes.EmptyEthHash, ErrModuleDisabled +} +func (EthSendDisabled) EthSendRawTransactionUntrusted(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) { + return ethtypes.EmptyEthHash, ErrModuleDisabled +} diff --git a/node/impl/full/eth_events.go b/node/impl/eth/subscriptions.go similarity index 51% rename from node/impl/full/eth_events.go rename to node/impl/eth/subscriptions.go index 850826ecf9c..b40d3b1b196 100644 --- a/node/impl/full/eth_events.go +++ b/node/impl/eth/subscriptions.go @@ -1,4 +1,4 @@ -package full +package eth import ( "context" @@ -6,7 +6,6 @@ import ( "sync" "github.com/google/uuid" - "github.com/ipfs/go-cid" "github.com/zyedidia/generic/queue" "golang.org/x/xerrors" @@ -14,187 +13,24 @@ import ( "github.com/filecoin-project/lotus/chain/events/filter" "github.com/filecoin-project/lotus/chain/index" - "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) []*index.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. Built-in actors emit CBOR, and anything else would be - // invalid anyway. - if entry.Codec != cid.Raw { - 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 ethFilterLogsFromEvents(ctx context.Context, evs []*index.CollectedEvent, sa StateAPI) ([]ethtypes.EthLog, error) { - var logs []ethtypes.EthLog - 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(ctx, ev.MsgCid, sa) - if err != nil { - return nil, err - } - if log.TransactionHash == ethtypes.EmptyEthHash { - // We've garbage collected the message, ignore the events and continue. - continue - } - c, err := ev.TipSetKey.Cid() - if err != nil { - return nil, err - } - log.BlockHash, err = ethtypes.EthHashFromCid(c) - if err != nil { - return nil, err - } - - logs = append(logs, log) - } - - return logs, nil -} - -func ethFilterResultFromEvents(ctx context.Context, evs []*index.CollectedEvent, sa StateAPI) (*ethtypes.EthFilterResult, error) { - logs, err := ethFilterLogsFromEvents(ctx, evs, sa) - if err != nil { - return nil, err - } - - res := ðtypes.EthFilterResult{} - for _, log := range logs { - 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) - } +const maxSendQueue = 20000 - return res, nil +type EthSubscriptionManager struct { + chainStore ChainStore + stateManager StateManager + mu sync.Mutex + subs map[ethtypes.EthSubscriptionID]*ethSubscription } -func ethFilterResultFromMessages(cs []*types.SignedMessage) (*ethtypes.EthFilterResult, error) { - res := ðtypes.EthFilterResult{} - - for _, c := range cs { - hash, err := ethTxHashFromSignedMessage(c) - if err != nil { - return nil, err - } - - res.Results = append(res.Results, hash) +func NewEthSubscriptionManager(chainStore ChainStore, stateManager StateManager) *EthSubscriptionManager { + return &EthSubscriptionManager{ + chainStore: chainStore, + stateManager: stateManager, } - - 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 @@ -208,9 +44,8 @@ func (e *EthSubscriptionManager) StartSubscription(ctx context.Context, out ethS ctx, quit := context.WithCancel(ctx) sub := ðSubscription{ - Chain: e.Chain, - StateAPI: e.StateAPI, - ChainAPI: e.ChainAPI, + chainStore: e.chainStore, + stateManager: e.stateManager, uninstallFilter: dropFilter, id: id, in: make(chan interface{}, 200), @@ -240,7 +75,7 @@ func (e *EthSubscriptionManager) StopSubscription(ctx context.Context, id ethtyp sub, ok := e.subs[id] if !ok { - return xerrors.Errorf("subscription not found") + return xerrors.New("subscription not found") } sub.stop() delete(e.subs, id) @@ -248,14 +83,9 @@ func (e *EthSubscriptionManager) StopSubscription(ctx context.Context, id ethtyp return nil } -type ethSubscriptionCallback func(context.Context, jsonrpc.RawParams) error - -const maxSendQueue = 20000 - type ethSubscription struct { - Chain *store.ChainStore - StateAPI StateAPI - ChainAPI ChainAPI + chainStore ChainStore + stateManager StateManager uninstallFilter func(context.Context, filter.Filter) error id ethtypes.EthSubscriptionID in chan interface{} @@ -273,7 +103,9 @@ type ethSubscription struct { lastSentTipset *types.TipSetKey } -func (e *ethSubscription) addFilter(ctx context.Context, f filter.Filter) { +type ethSubscriptionCallback func(context.Context, jsonrpc.RawParams) error + +func (e *ethSubscription) addFilter(f filter.Filter) { e.mu.Lock() defer e.mu.Unlock() @@ -349,7 +181,7 @@ func (e *ethSubscription) start(ctx context.Context) { case v := <-e.in: switch vt := v.(type) { case *index.CollectedEvent: - evs, err := ethFilterResultFromEvents(ctx, []*index.CollectedEvent{vt}, e.StateAPI) + evs, err := ethFilterResultFromEvents(ctx, []*index.CollectedEvent{vt}, e.chainStore, e.stateManager) if err != nil { continue } @@ -367,12 +199,12 @@ func (e *ethSubscription) start(ctx context.Context) { if e.lastSentTipset != nil && (*e.lastSentTipset) == parentTipSetKey { continue } - parentTipSet, loadErr := e.Chain.LoadTipSet(ctx, parentTipSetKey) + parentTipSet, loadErr := e.chainStore.LoadTipSet(ctx, parentTipSetKey) if loadErr != nil { log.Warnw("failed to load parent tipset", "tipset", parentTipSetKey, "error", loadErr) continue } - ethBlock, ethBlockErr := newEthBlockFromFilecoinTipSet(ctx, parentTipSet, true, e.Chain, e.StateAPI) + ethBlock, ethBlockErr := newEthBlockFromFilecoinTipSet(ctx, parentTipSet, true, e.chainStore, e.stateManager) if ethBlockErr != nil { continue } diff --git a/node/impl/full/eth_trace.go b/node/impl/eth/trace.go similarity index 63% rename from node/impl/full/eth_trace.go rename to node/impl/eth/trace.go index 15716aff3c6..2c43fe41b89 100644 --- a/node/impl/full/eth_trace.go +++ b/node/impl/eth/trace.go @@ -1,8 +1,11 @@ -package full +package eth import ( "bytes" + "context" + "errors" "fmt" + "strconv" "github.com/multiformats/go-multicodec" cbg "github.com/whyrusleeping/cbor-gen" @@ -16,6 +19,7 @@ import ( init12 "github.com/filecoin-project/go-state-types/builtin/v12/init" "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/actors/builtin/evm" "github.com/filecoin-project/lotus/chain/state" @@ -23,6 +27,381 @@ import ( "github.com/filecoin-project/lotus/chain/types/ethtypes" ) +type EthTraceAPI interface { + EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) + EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) + EthTraceTransaction(ctx context.Context, txHash string) ([]*ethtypes.EthTraceTransaction, error) + EthTraceFilter(ctx context.Context, filter ethtypes.EthTraceFilterCriteria) ([]*ethtypes.EthTraceFilterResult, error) +} + +var ( + _ EthTraceAPI = (*ethTrace)(nil) + _ EthTraceAPI = (*EthTraceDisabled)(nil) +) + +type ethTrace struct { + chainStore ChainStore + stateManager StateManager + ethTransactionApi EthTransactionAPI + traceFilterMaxResults uint64 +} + +func NewEthTraceAPI( + chainStore ChainStore, + stateManager StateManager, + ethTransactionApi EthTransactionAPI, + ethTraceFilterMaxResults uint64, +) EthTraceAPI { + return ðTrace{ + chainStore: chainStore, + stateManager: stateManager, + ethTransactionApi: ethTransactionApi, + traceFilterMaxResults: ethTraceFilterMaxResults, + } +} + +func (e *ethTrace) EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) { + ts, err := getTipsetByBlockNumber(ctx, e.chainStore, blkNum, true) + if err != nil { + return nil, err + } + + stRoot, trace, err := e.stateManager.ExecutionTrace(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) + } + + st, err := e.stateManager.StateTree(stRoot) + if err != nil { + return nil, xerrors.Errorf("failed load computed state-tree: %w", err) + } + + cid, err := ts.Key().Cid() + if err != nil { + return nil, xerrors.Errorf("failed to get tipset key cid: %w", err) + } + + blkHash, err := ethtypes.EthHashFromCid(cid) + if err != nil { + return nil, xerrors.Errorf("failed to parse eth hash from cid: %w", err) + } + + allTraces := make([]*ethtypes.EthTraceBlock, 0, len(trace)) + msgIdx := 0 + for _, ir := range trace { + // ignore messages from system actor + if ir.Msg.From == builtinactors.SystemActorAddr { + continue + } + + msgIdx++ + + txHash, err := getTransactionHashByCid(ctx, e.chainStore, ir.MsgCid) + if err != nil { + return nil, xerrors.Errorf("failed to get transaction hash by cid: %w", err) + } + if txHash == ethtypes.EmptyEthHash { + return nil, xerrors.Errorf("cannot find transaction hash for cid %s", ir.MsgCid) + } + + env, err := baseEnvironment(st, ir.Msg.From) + if err != nil { + return nil, xerrors.Errorf("when processing message %s: %w", ir.MsgCid, err) + } + + err = buildTraces(env, []int{}, &ir.ExecutionTrace) + if err != nil { + return nil, xerrors.Errorf("failed building traces for msg %s: %w", ir.MsgCid, err) + } + + for _, trace := range env.traces { + allTraces = append(allTraces, ðtypes.EthTraceBlock{ + EthTrace: trace, + BlockHash: blkHash, + BlockNumber: int64(ts.Height()), + TransactionHash: txHash, + TransactionPosition: msgIdx, + }) + } + } + + return allTraces, nil +} + +func (e *ethTrace) EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { + if len(traceTypes) != 1 || traceTypes[0] != "trace" { + return nil, xerrors.New("only 'trace' is supported") + } + ts, err := getTipsetByBlockNumber(ctx, e.chainStore, blkNum, true) + if err != nil { + return nil, err + } + + stRoot, trace, err := e.stateManager.ExecutionTrace(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) + } + + st, err := e.stateManager.StateTree(stRoot) + if err != nil { + return nil, xerrors.Errorf("failed load computed state-tree: %w", err) + } + + allTraces := make([]*ethtypes.EthTraceReplayBlockTransaction, 0, len(trace)) + for _, ir := range trace { + // ignore messages from system actor + if ir.Msg.From == builtinactors.SystemActorAddr { + continue + } + + txHash, err := getTransactionHashByCid(ctx, e.chainStore, ir.MsgCid) + if err != nil { + return nil, xerrors.Errorf("failed to get transaction hash by cid: %w", err) + } + if txHash == ethtypes.EmptyEthHash { + return nil, xerrors.Errorf("cannot find transaction hash for cid %s", ir.MsgCid) + } + + env, err := baseEnvironment(st, ir.Msg.From) + if err != nil { + return nil, xerrors.Errorf("when processing message %s: %w", ir.MsgCid, err) + } + + err = buildTraces(env, []int{}, &ir.ExecutionTrace) + if err != nil { + return nil, xerrors.Errorf("failed building traces for msg %s: %w", ir.MsgCid, err) + } + + var output []byte + if len(env.traces) > 0 { + switch r := env.traces[0].Result.(type) { + case *ethtypes.EthCallTraceResult: + output = r.Output + case *ethtypes.EthCreateTraceResult: + output = r.Code + } + } + + allTraces = append(allTraces, ðtypes.EthTraceReplayBlockTransaction{ + Output: output, + TransactionHash: txHash, + Trace: env.traces, + StateDiff: nil, + VmTrace: nil, + }) + } + + return allTraces, nil +} + +func (e *ethTrace) EthTraceTransaction(ctx context.Context, txHash string) ([]*ethtypes.EthTraceTransaction, error) { + // convert from string to internal type + ethTxHash, err := ethtypes.ParseEthHash(txHash) + if err != nil { + return nil, xerrors.Errorf("cannot parse eth hash: %w", err) + } + + tx, err := e.ethTransactionApi.EthGetTransactionByHash(ctx, ðTxHash) + if err != nil { + return nil, xerrors.Errorf("cannot get transaction by hash: %w", err) + } + + if tx == nil { + return nil, xerrors.New("transaction not found") + } + + // tx.BlockNumber is nil when the transaction is still in the mpool/pending + if tx.BlockNumber == nil { + return nil, xerrors.New("no trace for pending transactions") + } + + blockTraces, err := e.EthTraceBlock(ctx, strconv.FormatUint(uint64(*tx.BlockNumber), 10)) + if err != nil { + return nil, xerrors.Errorf("cannot get trace for block: %w", err) + } + + txTraces := make([]*ethtypes.EthTraceTransaction, 0, len(blockTraces)) + for _, blockTrace := range blockTraces { + if blockTrace.TransactionHash == ethTxHash { + // Create a new EthTraceTransaction from the block trace + txTrace := ethtypes.EthTraceTransaction{ + EthTrace: blockTrace.EthTrace, + BlockHash: blockTrace.BlockHash, + BlockNumber: blockTrace.BlockNumber, + TransactionHash: blockTrace.TransactionHash, + TransactionPosition: blockTrace.TransactionPosition, + } + txTraces = append(txTraces, &txTrace) + } + } + + return txTraces, nil +} + +func (e *ethTrace) EthTraceFilter(ctx context.Context, filter ethtypes.EthTraceFilterCriteria) ([]*ethtypes.EthTraceFilterResult, error) { + // Define EthBlockNumberFromString as a private function within EthTraceFilter + // TODO(rv): this all moves to TipSetProvider I think, then we get rid of TipSetProvider for this module + getEthBlockNumberFromString := func(ctx context.Context, block *string) (ethtypes.EthUint64, error) { + head := e.chainStore.GetHeaviestTipSet() + + blockValue := "latest" + if block != nil { + blockValue = *block + } + + switch blockValue { + case "earliest": + return 0, xerrors.New("block param \"earliest\" is not supported") + case "pending": + return ethtypes.EthUint64(head.Height()), nil + case "latest": + parent, err := e.chainStore.GetTipSetFromKey(ctx, head.Parents()) + if err != nil { + return 0, xerrors.New("cannot get parent tipset") + } + return ethtypes.EthUint64(parent.Height()), nil + case "safe": + latestHeight := head.Height() - 1 + safeHeight := latestHeight - ethtypes.SafeEpochDelay + return ethtypes.EthUint64(safeHeight), nil + default: + blockNum, err := ethtypes.EthUint64FromHex(blockValue) + if err != nil { + return 0, xerrors.Errorf("cannot parse fromBlock: %w", err) + } + return blockNum, err + } + } + + fromBlock, err := getEthBlockNumberFromString(ctx, filter.FromBlock) + if err != nil { + return nil, xerrors.Errorf("cannot parse fromBlock: %w", err) + } + + toBlock, err := getEthBlockNumberFromString(ctx, filter.ToBlock) + if err != nil { + return nil, xerrors.Errorf("cannot parse toBlock: %w", err) + } + + var results []*ethtypes.EthTraceFilterResult + + if filter.Count != nil { + // If filter.Count is specified and it is 0, return an empty result set immediately. + if *filter.Count == 0 { + return []*ethtypes.EthTraceFilterResult{}, nil + } + + // If filter.Count is specified and is greater than the EthTraceFilterMaxResults config return error + if uint64(*filter.Count) > e.traceFilterMaxResults { + return nil, xerrors.Errorf("invalid response count, requested %d, maximum supported is %d", *filter.Count, e.traceFilterMaxResults) + } + } + + traceCounter := ethtypes.EthUint64(0) + for blkNum := fromBlock; blkNum <= toBlock; blkNum++ { + blockTraces, err := e.EthTraceBlock(ctx, strconv.FormatUint(uint64(blkNum), 10)) + if err != nil { + if errors.Is(err, &api.ErrNullRound{}) { + continue + } + return nil, xerrors.Errorf("cannot get trace for block %d: %w", blkNum, err) + } + + for _, _blockTrace := range blockTraces { + // Create a copy of blockTrace to avoid pointer quirks + blockTrace := *_blockTrace + match, err := matchFilterCriteria(&blockTrace, filter.FromAddress, filter.ToAddress) + if err != nil { + return nil, xerrors.Errorf("cannot match filter for block %d: %w", blkNum, err) + } + if !match { + continue + } + traceCounter++ + if filter.After != nil && traceCounter <= *filter.After { + continue + } + + txTrace := ethtypes.EthTraceFilterResult(blockTrace) + results = append(results, &txTrace) + + // If Count is specified, limit the results + if filter.Count != nil && ethtypes.EthUint64(len(results)) >= *filter.Count { + return results, nil + } else if filter.Count == nil && uint64(len(results)) > e.traceFilterMaxResults { + return nil, xerrors.Errorf("too many results, maximum supported is %d, try paginating requests with After and Count", e.traceFilterMaxResults) + } + } + } + + return results, nil +} + +// matchFilterCriteria checks if a trace matches the filter criteria. +func matchFilterCriteria(trace *ethtypes.EthTraceBlock, fromDecodedAddresses []ethtypes.EthAddress, toDecodedAddresses []ethtypes.EthAddress) (bool, error) { + var traceTo ethtypes.EthAddress + var traceFrom ethtypes.EthAddress + + switch trace.Type { + case "call": + action, ok := trace.Action.(*ethtypes.EthCallTraceAction) + if !ok { + return false, xerrors.New("invalid call trace action") + } + traceTo = action.To + traceFrom = action.From + case "create": + result, okResult := trace.Result.(*ethtypes.EthCreateTraceResult) + if !okResult { + return false, xerrors.New("invalid create trace result") + } + + action, okAction := trace.Action.(*ethtypes.EthCreateTraceAction) + if !okAction { + return false, xerrors.New("invalid create trace action") + } + + if result.Address == nil { + return false, xerrors.New("address is nil in create trace result") + } + + traceTo = *result.Address + traceFrom = action.From + default: + return false, xerrors.Errorf("invalid trace type: %s", trace.Type) + } + + // Match FromAddress + if len(fromDecodedAddresses) > 0 { + fromMatch := false + for _, ethAddr := range fromDecodedAddresses { + if traceFrom == ethAddr { + fromMatch = true + break + } + } + if !fromMatch { + return false, nil + } + } + + // Match ToAddress + if len(toDecodedAddresses) > 0 { + toMatch := false + for _, ethAddr := range toDecodedAddresses { + if traceTo == ethAddr { + toMatch = true + break + } + } + if !toMatch { + return false, nil + } + } + + return true, nil +} + // decodePayload is a utility function which decodes the payload using the given codec func decodePayload(payload []byte, codec uint64) (ethtypes.EthBytes, error) { switch multicodec.Code(codec) { @@ -348,7 +727,7 @@ func traceNativeCreate(env *environment, addr []int, et *types.ExecutionTrace) ( // something, we have a bug in our tracing logic or a mismatch between our // tracing logic and the actors. if et.MsgRct.ExitCode.IsSuccess() { - return nil, nil, xerrors.Errorf("successful Exec/Exec4 call failed to call a constructor") + return nil, nil, xerrors.New("successful Exec/Exec4 call failed to call a constructor") } // Otherwise, this can happen if creation fails early (bad params, // out of gas, contract already exists, etc.). The EVM wouldn't @@ -367,7 +746,7 @@ func traceNativeCreate(env *environment, addr []int, et *types.ExecutionTrace) ( // actor. I'm catching this here because it likely means that there's a bug // in our trace-conversion logic. if et.Msg.Method == builtin.MethodsInit.Exec4 { - return nil, nil, xerrors.Errorf("direct call to Exec4 successfully called a constructor!") + return nil, nil, xerrors.New("direct call to Exec4 successfully called a constructor!") } var output ethtypes.EthBytes @@ -481,7 +860,7 @@ func traceEthCreate(env *environment, addr []int, et *types.ExecutionTrace) (*et // Same as the Init actor case above, see the comment there. if subTrace == nil { if et.MsgRct.ExitCode.IsSuccess() { - return nil, nil, xerrors.Errorf("successful Create/Create2 call failed to call a constructor") + return nil, nil, xerrors.New("successful Create/Create2 call failed to call a constructor") } return nil, nil, nil } @@ -567,7 +946,7 @@ func traceEVMPrivate(env *environment, addr []int, et *types.ExecutionTrace) (*e // (GetByteCode) and they are at the same level (same parent) // 3) Treat this as a delegate call to actor A. if env.lastByteCode == nil { - return nil, nil, xerrors.Errorf("unknown bytecode for delegate call") + return nil, nil, xerrors.New("unknown bytecode for delegate call") } if to := traceToAddress(et.InvokedActor); env.caller != to { @@ -606,3 +985,18 @@ func traceEVMPrivate(env *environment, addr []int, et *types.ExecutionTrace) (*e // 1024 (exclusive), so any calls in this range must be implementation details. return nil, nil, nil } + +type EthTraceDisabled struct{} + +func (EthTraceDisabled) EthTraceBlock(ctx context.Context, block string) ([]*ethtypes.EthTraceBlock, error) { + return nil, ErrModuleDisabled +} +func (EthTraceDisabled) EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { + return nil, ErrModuleDisabled +} +func (EthTraceDisabled) EthTraceTransaction(ctx context.Context, ethTxHash string) ([]*ethtypes.EthTraceTransaction, error) { + return nil, ErrModuleDisabled +} +func (EthTraceDisabled) EthTraceFilter(ctx context.Context, filter ethtypes.EthTraceFilterCriteria) ([]*ethtypes.EthTraceFilterResult, error) { + return nil, ErrModuleDisabled +} diff --git a/node/impl/eth/transaction.go b/node/impl/eth/transaction.go new file mode 100644 index 00000000000..f35d3788919 --- /dev/null +++ b/node/impl/eth/transaction.go @@ -0,0 +1,595 @@ +package eth + +import ( + "context" + "errors" + + "github.com/hashicorp/golang-lru/arc/v2" + "github.com/ipfs/go-cid" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-state-types/abi" + + "github.com/filecoin-project/lotus/api" + builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" + builtinevm "github.com/filecoin-project/lotus/chain/actors/builtin/evm" + "github.com/filecoin-project/lotus/chain/index" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +type EthTransactionAPI interface { + EthBlockNumber(ctx context.Context) (ethtypes.EthUint64, error) + + EthGetBlockTransactionCountByNumber(ctx context.Context, blkNum ethtypes.EthUint64) (ethtypes.EthUint64, error) + EthGetBlockTransactionCountByHash(ctx context.Context, blkHash ethtypes.EthHash) (ethtypes.EthUint64, error) + EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error) + EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (ethtypes.EthBlock, error) + + EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error) + EthGetTransactionByHashLimited(ctx context.Context, txHash *ethtypes.EthHash, limit abi.ChainEpoch) (*ethtypes.EthTx, error) + EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, txIndex ethtypes.EthUint64) (*ethtypes.EthTx, error) + EthGetTransactionByBlockNumberAndIndex(ctx context.Context, blkNum string, txIndex ethtypes.EthUint64) (*ethtypes.EthTx, error) + + EthGetMessageCidByTransactionHash(ctx context.Context, txHash *ethtypes.EthHash) (*cid.Cid, error) + EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) + EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthUint64, error) + + EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error) + EthGetTransactionReceiptLimited(ctx context.Context, txHash ethtypes.EthHash, limit abi.ChainEpoch) (*api.EthTxReceipt, error) + EthGetBlockReceipts(ctx context.Context, blkParam ethtypes.EthBlockNumberOrHash) ([]*api.EthTxReceipt, error) + EthGetBlockReceiptsLimited(ctx context.Context, blkParam ethtypes.EthBlockNumberOrHash, limit abi.ChainEpoch) ([]*api.EthTxReceipt, error) +} + +var ( + _ EthTransactionAPI = (*ethTransaction)(nil) + _ EthTransactionAPI = (*EthTransactionDisabled)(nil) +) + +type ethTransaction struct { + chainStore ChainStore + stateManager StateManager + stateApi StateAPI + mpoolApi MpoolAPI + chainIndexer index.Indexer + + ethEvents EthEventsInternal + + blockCache *arc.ARCCache[cid.Cid, *ethtypes.EthBlock] // caches blocks by their CID but blocks only have the transaction hashes + blockTransactionCache *arc.ARCCache[cid.Cid, *ethtypes.EthBlock] // caches blocks along with full transaction payload by their CID +} + +func NewEthTransactionAPI( + chainStore ChainStore, + stateManager StateManager, + stateApi StateAPI, + mpoolApi MpoolAPI, + chainIndexer index.Indexer, + ethEvents EthEventsInternal, + blockCacheSize int, +) (EthTransactionAPI, error) { + t := ðTransaction{ + chainStore: chainStore, + stateManager: stateManager, + stateApi: stateApi, + mpoolApi: mpoolApi, + chainIndexer: chainIndexer, + ethEvents: ethEvents, + blockCache: nil, + blockTransactionCache: nil, + } + + if blockCacheSize > 0 { + var err error + if t.blockCache, err = arc.NewARC[cid.Cid, *ethtypes.EthBlock](blockCacheSize); err != nil { + return nil, xerrors.Errorf("failed to create block cache: %w", err) + } + if t.blockTransactionCache, err = arc.NewARC[cid.Cid, *ethtypes.EthBlock](blockCacheSize); err != nil { + return nil, xerrors.Errorf("failed to create block transaction cache: %w", err) + } + } + + return t, nil +} + +func (e *ethTransaction) EthBlockNumber(ctx context.Context) (ethtypes.EthUint64, error) { + // eth_blockNumber needs to return the height of the latest committed tipset. + // Ethereum clients expect all transactions included in this block to have execution outputs. + // This is the parent of the head tipset. The head tipset is speculative, has not been + // recognized by the network, and its messages are only included, not executed. + // See https://github.com/filecoin-project/ref-fvm/issues/1135. + heaviest := e.chainStore.GetHeaviestTipSet() + if height := heaviest.Height(); height == 0 { + // we're at genesis. + return ethtypes.EthUint64(height), nil + } + // First non-null parent. + effectiveParent := heaviest.Parents() + parent, err := e.chainStore.GetTipSetFromKey(ctx, effectiveParent) + if err != nil { + return 0, err + } + return ethtypes.EthUint64(parent.Height()), nil +} + +func (e *ethTransaction) EthGetBlockTransactionCountByNumber(ctx context.Context, blkNum ethtypes.EthUint64) (ethtypes.EthUint64, error) { + ts, err := e.chainStore.GetTipsetByHeight(ctx, abi.ChainEpoch(blkNum), nil, false) + if err != nil { + return ethtypes.EthUint64(0), xerrors.Errorf("error loading tipset %s: %w", ts, err) + } + + count, err := e.countTipsetMsgs(ctx, ts) + return ethtypes.EthUint64(count), err +} + +func (e *ethTransaction) EthGetBlockTransactionCountByHash(ctx context.Context, blkHash ethtypes.EthHash) (ethtypes.EthUint64, error) { + ts, err := e.chainStore.GetTipSetByCid(ctx, blkHash.ToCid()) + if err != nil { + return ethtypes.EthUint64(0), xerrors.Errorf("error loading tipset %s: %w", ts, err) + } + count, err := e.countTipsetMsgs(ctx, ts) + return ethtypes.EthUint64(count), err +} + +func (e *ethTransaction) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error) { + cache := e.blockCache + if fullTxInfo { + cache = e.blockTransactionCache + } + + // Attempt to retrieve the Ethereum block from cache + cid := blkHash.ToCid() + if cache != nil { + if ethBlock, found := cache.Get(cid); found { + if ethBlock != nil { + return *ethBlock, nil + } + // Log and remove the nil entry from cache + log.Errorw("nil value in eth block cache", "hash", blkHash.String()) + cache.Remove(cid) + } + } + + // Fetch the tipset using the block hash + ts, err := e.chainStore.GetTipSetByCid(ctx, cid) + if err != nil { + return ethtypes.EthBlock{}, xerrors.Errorf("failed to load tipset by CID %s: %w", cid, err) + } + + // Generate an Ethereum block from the Filecoin tipset + blk, err := newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, e.chainStore, e.stateManager) + if err != nil { + return ethtypes.EthBlock{}, xerrors.Errorf("failed to create Ethereum block from Filecoin tipset: %w", err) + } + + // Add the newly created block to the cache and return + if cache != nil { + cache.Add(cid, &blk) + } + return blk, nil +} + +func (e *ethTransaction) EthGetBlockByNumber(ctx context.Context, blkParam string, fullTxInfo bool) (ethtypes.EthBlock, error) { + ts, err := getTipsetByBlockNumber(ctx, e.chainStore, blkParam, true) + if err != nil { + return ethtypes.EthBlock{}, err + } + return newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, e.chainStore, e.stateManager) +} + +func (e *ethTransaction) EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error) { + return e.EthGetTransactionByHashLimited(ctx, txHash, api.LookbackNoLimit) +} + +func (e *ethTransaction) EthGetTransactionByHashLimited(ctx context.Context, txHash *ethtypes.EthHash, limit abi.ChainEpoch) (*ethtypes.EthTx, error) { + // Ethereum's behavior is to return null when the txHash is invalid, so we use nil to check if txHash is valid + if txHash == nil { + return nil, nil + } + if e.chainIndexer == nil { + return nil, ErrChainIndexerDisabled + } + + var c cid.Cid + var err error + c, err = e.chainIndexer.GetCidFromHash(ctx, *txHash) + if err != nil && errors.Is(err, index.ErrNotFound) { + log.Debug("could not find transaction hash %s in chain indexer", txHash.String()) + } else if err != nil { + log.Errorf("failed to lookup transaction hash %s in chain indexer: %s", txHash.String(), err) + return nil, xerrors.Errorf("failed to lookup transaction hash %s in chain indexer: %w", txHash.String(), err) + } + + // This isn't an eth transaction we have the mapping for, so let's look it up as a filecoin message + if c == cid.Undef { + c = txHash.ToCid() + } + + // first, try to get the cid from mined transactions + msgLookup, err := e.stateApi.StateSearchMsg(ctx, types.EmptyTSK, c, limit, true) + if err == nil && msgLookup != nil { + tx, err := newEthTxFromMessageLookup(ctx, msgLookup, -1, e.chainStore, e.stateManager) + if err == nil { + return &tx, nil + } + } + + // if not found, try to get it from the mempool + pending, err := e.mpoolApi.MpoolPending(ctx, types.EmptyTSK) + if err != nil { + // inability to fetch mpool pending transactions is an internal node error + // that needs to be reported as-is + return nil, xerrors.Errorf("cannot get pending txs from mpool: %s", err) + } + + for _, p := range pending { + if p.Cid() == c { + // We only return pending eth-account messages because we can't guarantee + // that the from/to addresses of other messages are conversable to 0x-style + // addresses. So we just ignore them. + // + // This should be "fine" as anyone using an "Ethereum-centric" block + // explorer shouldn't care about seeing pending messages from native + // accounts. + ethtx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(p) + if err != nil { + return nil, xerrors.Errorf("could not convert Filecoin message into tx: %w", err) + } + + tx, err := ethtx.ToEthTx(p) + if err != nil { + return nil, xerrors.Errorf("could not convert Eth transaction to EthTx: %w", err) + } + + return &tx, nil + } + } + // Ethereum clients expect an empty response when the message was not found + return nil, nil +} + +func (e *ethTransaction) EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, index ethtypes.EthUint64) (*ethtypes.EthTx, error) { + ts, err := e.chainStore.GetTipSetByCid(ctx, blkHash.ToCid()) + if err != nil { + return nil, xerrors.Errorf("failed to get tipset by cid: %w", err) + } + + return e.getTransactionByTipsetAndIndex(ctx, ts, index) +} + +func (e *ethTransaction) EthGetTransactionByBlockNumberAndIndex(ctx context.Context, blkParam string, index ethtypes.EthUint64) (*ethtypes.EthTx, error) { + ts, err := getTipsetByBlockNumber(ctx, e.chainStore, blkParam, true) + if err != nil { + return nil, err + } + + if ts == nil { + return nil, xerrors.Errorf("tipset not found for block %s", blkParam) + } + + tx, err := e.getTransactionByTipsetAndIndex(ctx, ts, index) + if err != nil { + return nil, xerrors.Errorf("failed to get transaction at index %d: %w", index, err) + } + + return tx, nil +} + +func (e *ethTransaction) EthGetMessageCidByTransactionHash(ctx context.Context, txHash *ethtypes.EthHash) (*cid.Cid, error) { + // Ethereum's behavior is to return null when the txHash is invalid, so we use nil to check if txHash is valid + if txHash == nil { + return nil, nil + } + if e.chainIndexer == nil { + return nil, ErrChainIndexerDisabled + } + + var c cid.Cid + var err error + c, err = e.chainIndexer.GetCidFromHash(ctx, *txHash) + if err != nil && errors.Is(err, index.ErrNotFound) { + log.Debug("could not find transaction hash %s in chain indexer", txHash.String()) + } else if err != nil { + log.Errorf("failed to lookup transaction hash %s in chain indexer: %s", txHash.String(), err) + return nil, xerrors.Errorf("failed to lookup transaction hash %s in chain indexer: %w", txHash.String(), err) + } + + if errors.Is(err, index.ErrNotFound) { + log.Debug("could not find transaction hash %s in lookup table", txHash.String()) + } else if e.chainIndexer != nil { + return &c, nil + } + + // This isn't an eth transaction we have the mapping for, so let's try looking it up as a filecoin message + if c == cid.Undef { + c = txHash.ToCid() + } + + _, err = e.chainStore.GetSignedMessage(ctx, c) + if err == nil { + // This is an Eth Tx, Secp message, Or BLS message in the mpool + return &c, nil + } + + _, err = e.chainStore.GetMessage(ctx, c) + if err == nil { + // This is a BLS message + return &c, nil + } + + // Ethereum clients expect an empty response when the message was not found + return nil, nil +} + +func (e *ethTransaction) EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) { + if txHash, err := getTransactionHashByCid(ctx, e.chainStore, cid); err != nil { + return nil, err + } else if txHash == ethtypes.EmptyEthHash { + // not found + return nil, nil + } else { + return &txHash, nil + } +} + +func (e *ethTransaction) EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthUint64, error) { + addr, err := sender.ToFilecoinAddress() + if err != nil { + return ethtypes.EthUint64(0), xerrors.Errorf("invalid address: %w", err) + } + + // Handle "pending" block parameter separately + if blkParam.PredefinedBlock != nil && *blkParam.PredefinedBlock == "pending" { + nonce, err := e.mpoolApi.MpoolGetNonce(ctx, addr) + if err != nil { + return ethtypes.EthUint64(0), xerrors.Errorf("failed to get nonce from mpool: %w", err) + } + return ethtypes.EthUint64(nonce), nil + } + + // For all other cases, get the tipset based on the block parameter + ts, err := getTipsetByEthBlockNumberOrHash(ctx, e.chainStore, blkParam) + if err != nil { + return ethtypes.EthUint64(0), xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) + } + + // Get the actor state at the specified tipset + actor, err := e.stateManager.LoadActor(ctx, addr, ts) + if err != nil { + if errors.Is(err, types.ErrActorNotFound) { + return 0, nil + } + return 0, xerrors.Errorf("failed to lookup actor %s: %w", sender, err) + } + + // Handle EVM actor case + if builtinactors.IsEvmActor(actor.Code) { + evmState, err := builtinevm.Load(e.chainStore.ActorStore(ctx), actor) + if err != nil { + return 0, xerrors.Errorf("failed to load evm state: %w", err) + } + if alive, err := evmState.IsAlive(); err != nil { + return 0, err + } else if !alive { + return 0, nil + } + nonce, err := evmState.Nonce() + return ethtypes.EthUint64(nonce), err + } + + // For non-EVM actors, get the nonce from the actor state + return ethtypes.EthUint64(actor.Nonce), nil +} + +func (e *ethTransaction) EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error) { + return e.EthGetTransactionReceiptLimited(ctx, txHash, api.LookbackNoLimit) +} + +func (e *ethTransaction) EthGetTransactionReceiptLimited(ctx context.Context, txHash ethtypes.EthHash, limit abi.ChainEpoch) (*api.EthTxReceipt, error) { + var c cid.Cid + var err error + if e.chainIndexer == nil { + return nil, ErrChainIndexerDisabled + } + + c, err = e.chainIndexer.GetCidFromHash(ctx, txHash) + if err != nil && errors.Is(err, index.ErrNotFound) { + log.Debug("could not find transaction hash %s in chain indexer", txHash.String()) + } else if err != nil { + log.Errorf("failed to lookup transaction hash %s in chain indexer: %s", txHash.String(), err) + return nil, xerrors.Errorf("failed to lookup transaction hash %s in chain indexer: %w", txHash.String(), err) + } + + // This isn't an eth transaction we have the mapping for, so let's look it up as a filecoin message + if c == cid.Undef { + c = txHash.ToCid() + } + + msgLookup, err := e.stateApi.StateSearchMsg(ctx, types.EmptyTSK, c, limit, true) + if err != nil { + return nil, xerrors.Errorf("failed to lookup Eth Txn %s as %s: %w", txHash, c, err) + } + if msgLookup == nil { + // This is the best we can do. In theory, we could have just not indexed this + // transaction, but there's no way to check that here. + return nil, nil + } + + tx, err := newEthTxFromMessageLookup(ctx, msgLookup, -1, e.chainStore, e.stateManager) + if err != nil { + return nil, xerrors.Errorf("failed to convert %s into an Eth Txn: %w", txHash, err) + } + + ts, err := e.chainStore.GetTipSetFromKey(ctx, msgLookup.TipSet) + if err != nil { + return nil, xerrors.Errorf("failed to lookup tipset %s when constructing the eth txn receipt: %w", msgLookup.TipSet, err) + } + + // The tx is located in the parent tipset + parentTs, err := e.chainStore.LoadTipSet(ctx, ts.Parents()) + if err != nil { + return nil, xerrors.Errorf("failed to lookup tipset %s when constructing the eth txn receipt: %w", ts.Parents(), err) + } + + baseFee := parentTs.Blocks()[0].ParentBaseFee + + receipt, err := newEthTxReceipt(ctx, tx, baseFee, msgLookup.Receipt, e.ethEvents) + if err != nil { + return nil, xerrors.Errorf("failed to create Eth receipt: %w", err) + } + + return &receipt, nil +} + +func (e *ethTransaction) EthGetBlockReceipts(ctx context.Context, blockParam ethtypes.EthBlockNumberOrHash) ([]*api.EthTxReceipt, error) { + return e.EthGetBlockReceiptsLimited(ctx, blockParam, api.LookbackNoLimit) +} + +func (e *ethTransaction) EthGetBlockReceiptsLimited(ctx context.Context, blockParam ethtypes.EthBlockNumberOrHash, limit abi.ChainEpoch) ([]*api.EthTxReceipt, error) { + ts, err := getTipsetByEthBlockNumberOrHash(ctx, e.chainStore, blockParam) + if err != nil { + return nil, xerrors.Errorf("failed to get tipset: %w", err) + } + + tsCid, err := ts.Key().Cid() + if err != nil { + return nil, xerrors.Errorf("failed to get tipset key cid: %w", err) + } + + blkHash, err := ethtypes.EthHashFromCid(tsCid) + if err != nil { + return nil, xerrors.Errorf("failed to parse eth hash from cid: %w", err) + } + + // Execute the tipset to get the receipts, messages, and events + st, msgs, receipts, err := executeTipset(ctx, ts, e.chainStore, e.stateManager) + if err != nil { + return nil, xerrors.Errorf("failed to execute tipset: %w", err) + } + + // Load the state tree + stateTree, err := e.stateManager.StateTree(st) + if err != nil { + return nil, xerrors.Errorf("failed to load state tree: %w", err) + } + + baseFee := ts.Blocks()[0].ParentBaseFee + + ethReceipts := make([]*api.EthTxReceipt, 0, len(msgs)) + for i, msg := range msgs { + msg := msg + + tx, err := newEthTx(ctx, e.chainStore, stateTree, ts.Height(), tsCid, msg.Cid(), i) + if err != nil { + return nil, xerrors.Errorf("failed to create EthTx: %w", err) + } + + receipt, err := newEthTxReceipt(ctx, tx, baseFee, receipts[i], e.ethEvents) + if err != nil { + return nil, xerrors.Errorf("failed to create Eth receipt: %w", err) + } + + // Set the correct Ethereum block hash + receipt.BlockHash = blkHash + + ethReceipts = append(ethReceipts, &receipt) + } + + return ethReceipts, nil +} + +func (e *ethTransaction) getTransactionByTipsetAndIndex(ctx context.Context, ts *types.TipSet, index ethtypes.EthUint64) (*ethtypes.EthTx, error) { + msgs, err := e.chainStore.MessagesForTipset(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("failed to get messages for tipset: %w", err) + } + + if uint64(index) >= uint64(len(msgs)) { + return nil, xerrors.Errorf("index %d out of range: tipset contains %d messages", index, len(msgs)) + } + + msg := msgs[index] + + cid, err := ts.Key().Cid() + if err != nil { + return nil, xerrors.Errorf("failed to get tipset key cid: %w", err) + } + + // First, get the state tree + st, err := e.stateManager.StateTree(ts.ParentState()) + if err != nil { + return nil, xerrors.Errorf("failed to load state tree: %w", err) + } + + tx, err := newEthTx(ctx, e.chainStore, st, ts.Height(), cid, msg.Cid(), int(index)) + if err != nil { + return nil, xerrors.Errorf("failed to create Ethereum transaction: %w", err) + } + + return &tx, nil +} + +func (e *ethTransaction) countTipsetMsgs(ctx context.Context, ts *types.TipSet) (int, error) { + blkMsgs, err := e.chainStore.BlockMsgsForTipset(ctx, ts) + if err != nil { + return 0, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) + } + + count := 0 + for _, blkMsg := range blkMsgs { + // TODO: may need to run canonical ordering and deduplication here + count += len(blkMsg.BlsMessages) + len(blkMsg.SecpkMessages) + } + return count, nil +} + +type EthTransactionDisabled struct{} + +func (EthTransactionDisabled) EthBlockNumber(ctx context.Context) (ethtypes.EthUint64, error) { + return 0, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetBlockTransactionCountByNumber(ctx context.Context, blkNum ethtypes.EthUint64) (ethtypes.EthUint64, error) { + return 0, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetBlockTransactionCountByHash(ctx context.Context, blkHash ethtypes.EthHash) (ethtypes.EthUint64, error) { + return 0, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error) { + return ethtypes.EthBlock{}, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (ethtypes.EthBlock, error) { + return ethtypes.EthBlock{}, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetTransactionByHashLimited(ctx context.Context, txHash *ethtypes.EthHash, limit abi.ChainEpoch) (*ethtypes.EthTx, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, txIndex ethtypes.EthUint64) (*ethtypes.EthTx, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetTransactionByBlockNumberAndIndex(ctx context.Context, blkNum string, txIndex ethtypes.EthUint64) (*ethtypes.EthTx, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetMessageCidByTransactionHash(ctx context.Context, txHash *ethtypes.EthHash) (*cid.Cid, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthUint64, error) { + return 0, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetTransactionReceiptLimited(ctx context.Context, txHash ethtypes.EthHash, limit abi.ChainEpoch) (*api.EthTxReceipt, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetBlockReceipts(ctx context.Context, blockParam ethtypes.EthBlockNumberOrHash) ([]*api.EthTxReceipt, error) { + return nil, ErrModuleDisabled +} +func (EthTransactionDisabled) EthGetBlockReceiptsLimited(ctx context.Context, blockParam ethtypes.EthBlockNumberOrHash, limit abi.ChainEpoch) ([]*api.EthTxReceipt, error) { + return nil, ErrModuleDisabled +} diff --git a/node/impl/full/eth_utils.go b/node/impl/eth/utils.go similarity index 84% rename from node/impl/full/eth_utils.go rename to node/impl/eth/utils.go index 00f3de90163..165e0aef8b9 100644 --- a/node/impl/full/eth_utils.go +++ b/node/impl/eth/utils.go @@ -1,4 +1,4 @@ -package full +package eth import ( "bytes" @@ -25,7 +25,6 @@ import ( "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/actors/policy" "github.com/filecoin-project/lotus/chain/state" - "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" @@ -43,112 +42,61 @@ func init() { } } -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") - } - - 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 - case "safe": - latestHeight := head.Height() - 1 - safeHeight := latestHeight - ethtypes.SafeEpochDelay - ts, err := chain.GetTipsetByHeight(ctx, safeHeight, head, true) - if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", safeHeight) - } - return ts, nil - case "finalized": - latestHeight := head.Height() - 1 - safeHeight := latestHeight - policy.ChainFinality - ts, err := chain.GetTipsetByHeight(ctx, safeHeight, head, true) - if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", safeHeight) - } - return ts, 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, api.NewErrNullRound(abi.ChainEpoch(num)) - } - return ts, nil - } -} - -func getTipsetByEthBlockNumberOrHash(ctx context.Context, chain *store.ChainStore, blkParam ethtypes.EthBlockNumberOrHash) (*types.TipSet, error) { - head := chain.GetHeaviestTipSet() +func getTipsetByEthBlockNumberOrHash(ctx context.Context, cp ChainStore, blkParam ethtypes.EthBlockNumberOrHash) (*types.TipSet, error) { + head := cp.GetHeaviestTipSet() predefined := blkParam.PredefinedBlock if predefined != nil { if *predefined == "earliest" { - return nil, fmt.Errorf("block param \"earliest\" is not supported") + return nil, xerrors.New("block param \"earliest\" is not supported") } else if *predefined == "pending" { return head, nil } else if *predefined == "latest" { - parent, err := chain.GetTipSetFromKey(ctx, head.Parents()) + parent, err := cp.GetTipSetFromKey(ctx, head.Parents()) if err != nil { - return nil, fmt.Errorf("cannot get parent tipset") + return nil, xerrors.New("cannot get parent tipset") } return parent, nil } - return nil, fmt.Errorf("unknown predefined block %s", *predefined) + return nil, xerrors.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')") + return nil, xerrors.New("requested a future epoch (beyond 'latest')") } - ts, err := chain.GetTipsetByHeight(ctx, height, head, true) + ts, err := cp.GetTipsetByHeight(ctx, height, head, true) if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", height) + return nil, xerrors.Errorf("cannot get tipset at height: %v", height) } return ts, nil } if blkParam.BlockHash != nil { - ts, err := chain.GetTipSetByCid(ctx, blkParam.BlockHash.ToCid()) + ts, err := cp.GetTipSetByCid(ctx, blkParam.BlockHash.ToCid()) if err != nil { - return nil, fmt.Errorf("cannot get tipset by hash: %v", err) + return nil, xerrors.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) + walkTs, err := cp.GetTipsetByHeight(ctx, ts.Height(), head, true) if err != nil { - return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height()) + return nil, xerrors.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 nil, xerrors.New("tipset is not canonical") } } return ts, nil } - return nil, errors.New("invalid block param") + return nil, xerrors.New("invalid block param") } func ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types.Message, error) { @@ -158,17 +106,17 @@ func ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types. var err error from, err = (ethtypes.EthAddress{}).ToFilecoinAddress() if err != nil { - return nil, fmt.Errorf("failed to construct the ethereum system address: %w", err) + return nil, xerrors.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) + return nil, xerrors.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) + return nil, xerrors.Errorf("expected a class 4 address, got: %d: %w", p, err) } } @@ -177,7 +125,7 @@ func ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types. initcode := abi.CborBytes(tx.Data) params2, err := actors.SerializeParams(&initcode) if err != nil { - return nil, fmt.Errorf("failed to serialize params: %w", err) + return nil, xerrors.Errorf("failed to serialize params: %w", err) } params = params2 } @@ -209,7 +157,7 @@ func ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types. }, nil } -func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTxInfo bool, cs *store.ChainStore, sa StateAPI) (ethtypes.EthBlock, error) { +func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTxInfo bool, cp ChainStore, sp StateManager) (ethtypes.EthBlock, error) { parentKeyCid, err := ts.Parents().Cid() if err != nil { return ethtypes.EthBlock{}, err @@ -231,12 +179,12 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return ethtypes.EthBlock{}, err } - stRoot, msgs, rcpts, err := executeTipset(ctx, ts, cs, sa) + stRoot, msgs, rcpts, err := executeTipset(ctx, ts, cp, sp) if err != nil { return ethtypes.EthBlock{}, xerrors.Errorf("failed to retrieve messages and receipts: %w", err) } - st, err := sa.StateManager.StateTree(stRoot) + st, err := sp.StateTree(stRoot) if err != nil { return ethtypes.EthBlock{}, xerrors.Errorf("failed to load state-tree root %q: %w", stRoot, err) } @@ -287,18 +235,18 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return block, nil } -func executeTipset(ctx context.Context, ts *types.TipSet, cs *store.ChainStore, sa StateAPI) (cid.Cid, []types.ChainMsg, []types.MessageReceipt, error) { - msgs, err := cs.MessagesForTipset(ctx, ts) +func executeTipset(ctx context.Context, ts *types.TipSet, cp ChainStore, sp StateManager) (cid.Cid, []types.ChainMsg, []types.MessageReceipt, error) { + msgs, err := cp.MessagesForTipset(ctx, ts) if err != nil { return cid.Undef, nil, nil, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) } - stRoot, rcptRoot, err := sa.StateManager.TipSetState(ctx, ts) + stRoot, rcptRoot, err := sp.TipSetState(ctx, ts) if err != nil { return cid.Undef, nil, nil, xerrors.Errorf("failed to compute tipset state: %w", err) } - rcpts, err := cs.ReadReceipts(ctx, rcptRoot) + rcpts, err := cp.ReadReceipts(ctx, rcptRoot) if err != nil { return cid.Undef, nil, nil, xerrors.Errorf("error loading receipts for tipset: %v: %w", ts, err) } @@ -437,14 +385,14 @@ func parseEthTopics(topics ethtypes.EthTopicSpec) (map[string][][]byte, error) { return keys, nil } -func ethTxHashFromMessageCid(ctx context.Context, c cid.Cid, sa StateAPI) (ethtypes.EthHash, error) { - smsg, err := sa.Chain.GetSignedMessage(ctx, c) +func getTransactionHashByCid(ctx context.Context, cp ChainStore, c cid.Cid) (ethtypes.EthHash, error) { + smsg, err := cp.GetSignedMessage(ctx, c) if err == nil { // This is an Eth Tx, Secp message, Or BLS message in the mpool return ethTxHashFromSignedMessage(smsg) } - _, err = sa.Chain.GetMessage(ctx, c) + _, err = cp.GetMessage(ctx, c) if err == nil { // This is a BLS message return ethtypes.EthHashFromCid(c) @@ -577,11 +525,11 @@ func ethTxFromNativeMessage(msg *types.Message, st *state.StateTree) (ethtypes.E return ethTx, nil } -func getSignedMessage(ctx context.Context, cs *store.ChainStore, msgCid cid.Cid) (*types.SignedMessage, error) { - smsg, err := cs.GetSignedMessage(ctx, msgCid) +func getSignedMessage(ctx context.Context, cp ChainStore, msgCid cid.Cid) (*types.SignedMessage, error) { + smsg, err := cp.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) + msg, err := cp.GetMessage(ctx, msgCid) if err != nil { return nil, xerrors.Errorf("failed to find msg %s: %w", msgCid, err) } @@ -599,14 +547,20 @@ func getSignedMessage(ctx context.Context, cs *store.ChainStore, msgCid cid.Cid) // 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) +func newEthTxFromMessageLookup( + ctx context.Context, + msgLookup *api.MsgLookup, + txIdx int, + cp ChainStore, + sp StateManager, +) (ethtypes.EthTx, error) { + ts, err := cp.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()) + parentTs, err := cp.LoadTipSet(ctx, ts.Parents()) if err != nil { return ethtypes.EthTx{}, err } @@ -618,7 +572,7 @@ func newEthTxFromMessageLookup(ctx context.Context, msgLookup *api.MsgLookup, tx // lookup the transactionIndex if txIdx < 0 { - msgs, err := cs.MessagesForTipset(ctx, parentTs) + msgs, err := cp.MessagesForTipset(ctx, parentTs) if err != nil { return ethtypes.EthTx{}, err } @@ -629,20 +583,28 @@ func newEthTxFromMessageLookup(ctx context.Context, msgLookup *api.MsgLookup, tx } } if txIdx < 0 { - return ethtypes.EthTx{}, fmt.Errorf("cannot find the msg in the tipset") + return ethtypes.EthTx{}, xerrors.New("cannot find the msg in the tipset") } } - st, err := sa.StateManager.StateTree(ts.ParentState()) + st, err := sp.StateTree(ts.ParentState()) if err != nil { return ethtypes.EthTx{}, xerrors.Errorf("failed to load message state tree: %w", err) } - return newEthTx(ctx, cs, st, parentTs.Height(), parentTsCid, msgLookup.Message, txIdx) + return newEthTx(ctx, cp, st, parentTs.Height(), parentTsCid, msgLookup.Message, txIdx) } -func newEthTx(ctx context.Context, cs *store.ChainStore, st *state.StateTree, blockHeight abi.ChainEpoch, msgTsCid cid.Cid, msgCid cid.Cid, txIdx int) (ethtypes.EthTx, error) { - smsg, err := getSignedMessage(ctx, cs, msgCid) +func newEthTx( + ctx context.Context, + cp ChainStore, + st *state.StateTree, + blockHeight abi.ChainEpoch, + msgTsCid cid.Cid, + msgCid cid.Cid, + txIdx int, +) (ethtypes.EthTx, error) { + smsg, err := getSignedMessage(ctx, cp, msgCid) if err != nil { return ethtypes.EthTx{}, xerrors.Errorf("failed to get signed msg: %w", err) } @@ -669,7 +631,7 @@ func newEthTx(ctx context.Context, cs *store.ChainStore, st *state.StateTree, bl return tx, nil } -func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, baseFee big.Int, msgReceipt types.MessageReceipt, ev *EthEventHandler) (api.EthTxReceipt, error) { +func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, baseFee big.Int, msgReceipt types.MessageReceipt, ev EthEventsInternal) (api.EthTxReceipt, error) { var ( transactionIndex ethtypes.EthUint64 blockHash ethtypes.EthHash @@ -739,7 +701,7 @@ func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, baseFee big.Int, ms } if rct := msgReceipt; rct.EventsRoot != nil { - logs, err := ev.getEthLogsForBlockAndTransaction(ctx, &blockHash, tx.Hash) + logs, err := ev.GetEthLogsForBlockAndTransaction(ctx, &blockHash, tx.Hash) if err != nil { return api.EthTxReceipt{}, xerrors.Errorf("failed to get eth logs for block and transaction: %w", err) } @@ -799,3 +761,54 @@ func encodeAsABIHelper(param1 uint64, param2 uint64, data []byte) []byte { return buf } + +func getTipsetByBlockNumber(ctx context.Context, cp ChainStore, blkParam string, strict bool) (*types.TipSet, error) { + if blkParam == "earliest" { + return nil, xerrors.New("block param \"earliest\" is not supported") + } + + head := cp.GetHeaviestTipSet() + switch blkParam { + case "pending": + return head, nil + case "latest": + parent, err := cp.GetTipSetFromKey(ctx, head.Parents()) + if err != nil { + return nil, xerrors.New("cannot get parent tipset") + } + return parent, nil + case "safe": + latestHeight := head.Height() - 1 + safeHeight := latestHeight - ethtypes.SafeEpochDelay + ts, err := cp.GetTipsetByHeight(ctx, safeHeight, head, true) + if err != nil { + return nil, xerrors.Errorf("cannot get tipset at height: %v", safeHeight) + } + return ts, nil + case "finalized": + latestHeight := head.Height() - 1 + safeHeight := latestHeight - policy.ChainFinality + ts, err := cp.GetTipsetByHeight(ctx, safeHeight, head, true) + if err != nil { + return nil, xerrors.Errorf("cannot get tipset at height: %v", safeHeight) + } + return ts, nil + default: + var num ethtypes.EthUint64 + err := num.UnmarshalJSON([]byte(`"` + blkParam + `"`)) + if err != nil { + return nil, xerrors.Errorf("cannot parse block number: %v", err) + } + if abi.ChainEpoch(num) > head.Height()-1 { + return nil, xerrors.New("requested a future epoch (beyond 'latest')") + } + ts, err := cp.GetTipsetByHeight(ctx, abi.ChainEpoch(num), head, true) + if err != nil { + return nil, xerrors.Errorf("cannot get tipset at height: %v", num) + } + if strict && ts.Height() != abi.ChainEpoch(num) { + return nil, api.NewErrNullRound(abi.ChainEpoch(num)) + } + return ts, nil + } +} diff --git a/node/impl/eth/utils_test.go b/node/impl/eth/utils_test.go new file mode 100644 index 00000000000..3ac6f97e066 --- /dev/null +++ b/node/impl/eth/utils_test.go @@ -0,0 +1,62 @@ +package eth + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/multiformats/go-multicodec" + "github.com/stretchr/testify/require" + cbg "github.com/whyrusleeping/cbor-gen" +) + +func TestABIEncoding(t *testing.T) { + // Generated from https://abi.hashex.org/ + const expected = "000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001b1111111111111111111020200301000000044444444444444444010000000000" + const data = "111111111111111111102020030100000004444444444444444401" + + expectedBytes, err := hex.DecodeString(expected) + require.NoError(t, err) + + dataBytes, err := hex.DecodeString(data) + require.NoError(t, err) + + require.Equal(t, expectedBytes, encodeAsABIHelper(22, 81, dataBytes)) +} + +func TestDecodePayload(t *testing.T) { + // "empty" + b, err := decodePayload(nil, 0) + require.NoError(t, err) + require.Empty(t, b) + + // raw empty + _, err = decodePayload(nil, uint64(multicodec.Raw)) + require.NoError(t, err) + require.Empty(t, b) + + // raw non-empty + b, err = decodePayload([]byte{1}, uint64(multicodec.Raw)) + require.NoError(t, err) + require.EqualValues(t, b, []byte{1}) + + // Invalid cbor bytes + _, err = decodePayload(nil, uint64(multicodec.DagCbor)) + require.Error(t, err) + + // valid cbor bytes + var w bytes.Buffer + require.NoError(t, cbg.WriteByteArray(&w, []byte{1})) + b, err = decodePayload(w.Bytes(), uint64(multicodec.DagCbor)) + require.NoError(t, err) + require.EqualValues(t, b, []byte{1}) + + // regular cbor also works. + b, err = decodePayload(w.Bytes(), uint64(multicodec.Cbor)) + require.NoError(t, err) + require.EqualValues(t, b, []byte{1}) + + // random codec should fail + _, err = decodePayload(w.Bytes(), 42) + require.Error(t, err) +} diff --git a/node/impl/full.go b/node/impl/full.go index 24240e3df2c..156bc24773d 100644 --- a/node/impl/full.go +++ b/node/impl/full.go @@ -33,7 +33,7 @@ type FullNodeAPI struct { full.MsigAPI full.WalletAPI full.SyncAPI - full.EthAPI + full.FullEthAPI full.ActorEventsAPI full.F3API full.ChainIndexAPI diff --git a/node/impl/full/dummy.go b/node/impl/full/dummy.go index 1429b899099..1b58dc91a5d 100644 --- a/node/impl/full/dummy.go +++ b/node/impl/full/dummy.go @@ -4,208 +4,9 @@ import ( "context" "errors" - "github.com/ipfs/go-cid" - - "github.com/filecoin-project/go-address" - "github.com/filecoin-project/go-jsonrpc" - "github.com/filecoin-project/go-state-types/abi" - - "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/types" - "github.com/filecoin-project/lotus/chain/types/ethtypes" ) -var ErrModuleDisabled = errors.New("module disabled, enable with Fevm.EnableEthRPC / LOTUS_FEVM_ENABLEETHRPC") - -type EthModuleDummy struct{} - -func (e *EthModuleDummy) EthAddressToFilecoinAddress(ctx context.Context, ethAddress ethtypes.EthAddress) (address.Address, error) { - return address.Undef, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetMessageCidByTransactionHash(ctx context.Context, txHash *ethtypes.EthHash) (*cid.Cid, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthBlockNumber(ctx context.Context) (ethtypes.EthUint64, error) { - return 0, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthAccounts(ctx context.Context) ([]ethtypes.EthAddress, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetBlockTransactionCountByNumber(ctx context.Context, blkNum ethtypes.EthUint64) (ethtypes.EthUint64, error) { - return 0, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetBlockTransactionCountByHash(ctx context.Context, blkHash ethtypes.EthHash) (ethtypes.EthUint64, error) { - return 0, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error) { - return ethtypes.EthBlock{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (ethtypes.EthBlock, error) { - return ethtypes.EthBlock{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetTransactionByHashLimited(ctx context.Context, txHash *ethtypes.EthHash, limit abi.ChainEpoch) (*ethtypes.EthTx, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthUint64, error) { - return 0, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetBlockReceiptsLimited(ctx context.Context, blkParam ethtypes.EthBlockNumberOrHash, limit abi.ChainEpoch) ([]*api.EthTxReceipt, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetBlockReceipts(ctx context.Context, blkParam ethtypes.EthBlockNumberOrHash) ([]*api.EthTxReceipt, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetTransactionReceiptLimited(ctx context.Context, txHash ethtypes.EthHash, limit abi.ChainEpoch) (*api.EthTxReceipt, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, txIndex ethtypes.EthUint64) (*ethtypes.EthTx, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetTransactionByBlockNumberAndIndex(ctx context.Context, blkNum string, txIndex ethtypes.EthUint64) (*ethtypes.EthTx, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetCode(ctx context.Context, address ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetStorageAt(ctx context.Context, address ethtypes.EthAddress, position ethtypes.EthBytes, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetBalance(ctx context.Context, address ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBigInt, error) { - return ethtypes.EthBigIntZero, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthFeeHistory, error) { - return ethtypes.EthFeeHistory{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthChainId(ctx context.Context) (ethtypes.EthUint64, error) { - return 0, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthSyncing(ctx context.Context) (ethtypes.EthSyncingResult, error) { - return ethtypes.EthSyncingResult{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) NetVersion(ctx context.Context) (string, error) { - return "", ErrModuleDisabled -} - -func (e *EthModuleDummy) NetListening(ctx context.Context) (bool, error) { - return false, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthProtocolVersion(ctx context.Context) (ethtypes.EthUint64, error) { - return 0, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGasPrice(ctx context.Context) (ethtypes.EthBigInt, error) { - return ethtypes.EthBigIntZero, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthEstimateGas(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthUint64, error) { - return 0, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthMaxPriorityFeePerGas(ctx context.Context) (ethtypes.EthBigInt, error) { - return ethtypes.EthBigIntZero, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) { - return ethtypes.EthHash{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) Web3ClientVersion(ctx context.Context) (string, error) { - return "", ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetLogs(ctx context.Context, filter *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) { - return ðtypes.EthFilterResult{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { - return ðtypes.EthFilterResult{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthGetFilterLogs(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { - return ðtypes.EthFilterResult{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthNewFilter(ctx context.Context, filter *ethtypes.EthFilterSpec) (ethtypes.EthFilterID, error) { - return ethtypes.EthFilterID{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthNewBlockFilter(ctx context.Context) (ethtypes.EthFilterID, error) { - return ethtypes.EthFilterID{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthNewPendingTransactionFilter(ctx context.Context) (ethtypes.EthFilterID, error) { - return ethtypes.EthFilterID{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthUninstallFilter(ctx context.Context, id ethtypes.EthFilterID) (bool, error) { - return false, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthSubscribe(ctx context.Context, params jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) { - return ethtypes.EthSubscriptionID{}, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) { - return false, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthTraceReplayBlockTransactions(ctx context.Context, blkNum string, traceTypes []string) ([]*ethtypes.EthTraceReplayBlockTransaction, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthTraceTransaction(ctx context.Context, txHash string) ([]*ethtypes.EthTraceTransaction, error) { - return nil, ErrModuleDisabled -} - -func (e *EthModuleDummy) EthTraceFilter(ctx context.Context, filter ethtypes.EthTraceFilterCriteria) ([]*ethtypes.EthTraceFilterResult, error) { - return nil, ErrModuleDisabled -} - -var _ EthModuleAPI = &EthModuleDummy{} -var _ EthEventAPI = &EthModuleDummy{} - var ErrActorEventModuleDisabled = errors.New("module disabled, enable with Events.EnableActorEventsAPI") type ActorEventDummy struct{} diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index cb1b66aa48c..7e4f9b10444 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -1,54 +1,19 @@ package full import ( - "bytes" "context" - "errors" - "fmt" - "os" - "sort" - "strconv" - "strings" - "time" - "github.com/hashicorp/golang-lru/arc/v2" "github.com/ipfs/go-cid" - "github.com/multiformats/go-multicodec" - cbg "github.com/whyrusleeping/cbor-gen" "go.uber.org/fx" - "golang.org/x/xerrors" - "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-jsonrpc" "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/evm" - "github.com/filecoin-project/go-state-types/exitcode" "github.com/filecoin-project/lotus/api" - "github.com/filecoin-project/lotus/build" - "github.com/filecoin-project/lotus/build/buildconstants" - "github.com/filecoin-project/lotus/chain/actors" - builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" - builtinevm "github.com/filecoin-project/lotus/chain/actors/builtin/evm" - "github.com/filecoin-project/lotus/chain/events/filter" - "github.com/filecoin-project/lotus/chain/index" - "github.com/filecoin-project/lotus/chain/messagepool" - "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" - "github.com/filecoin-project/lotus/node/modules/dtypes" + "github.com/filecoin-project/lotus/node/impl/eth" ) -var ( - ErrUnsupported = errors.New("unsupported method") - ErrChainIndexerDisabled = errors.New("chain indexer is disabled; please enable the ChainIndexer to use the ETH RPC API") -) - -const maxEthFeeHistoryRewardPercentiles = 100 - type EthModuleAPI interface { EthBlockNumber(ctx context.Context) (ethtypes.EthUint64, error) EthAccounts(ctx context.Context) ([]ethtypes.EthAddress, error) @@ -101,2146 +66,21 @@ type EthEventAPI interface { var ( _ EthModuleAPI = *new(api.FullNode) _ EthEventAPI = *new(api.FullNode) - + _ EthModuleAPI = *new(FullEthAPI) + _ EthEventAPI = *new(FullEthAPI) _ EthModuleAPI = *new(api.Gateway) + _ EthEventAPI = *new(api.Gateway) ) -// EthModule provides the default implementation of the standard Ethereum JSON-RPC API. -// -// # Execution model reconciliation -// -// Ethereum relies on an immediate block-based execution model. The block that includes -// a transaction is also the block that executes it. Each block specifies the state root -// resulting from executing all transactions within it (output state). -// -// In Filecoin, at every epoch there is an unknown number of round winners all of whom are -// entitled to publish a block. Blocks are collected into a tipset. A tipset is committed -// only when the subsequent tipset is built on it (i.e. it becomes a parent). Block producers -// execute the parent tipset and specify the resulting state root in the block being produced. -// In other words, contrary to Ethereum, each block specifies the input state root. -// -// Ethereum clients expect transactions returned via eth_getBlock* to have a receipt -// (due to immediate execution). For this reason: -// -// - eth_blockNumber returns the latest executed epoch (head - 1) -// - The 'latest' block refers to the latest executed epoch (head - 1) -// - The 'pending' block refers to the current speculative tipset (head) -// - eth_getTransactionByHash returns the inclusion tipset of a message, but -// only after it has executed. -// - eth_getTransactionReceipt ditto. -// -// "Latest executed epoch" refers to the tipset that this node currently -// accepts as the best parent tipset, based on the blocks it is accumulating -// within the HEAD tipset. -type EthModule struct { - Chain *store.ChainStore - Mpool *messagepool.MessagePool - StateManager *stmgr.StateManager - EthTraceFilterMaxResults uint64 - EthEventHandler *EthEventHandler - - EthBlkCache *arc.ARCCache[cid.Cid, *ethtypes.EthBlock] // caches blocks by their CID but blocks only have the transaction hashes - EthBlkTxCache *arc.ARCCache[cid.Cid, *ethtypes.EthBlock] // caches blocks along with full transaction payload by their CID - - ChainIndexer index.Indexer - - ChainAPI - MpoolAPI - StateAPI - SyncAPI -} - -var _ EthModuleAPI = (*EthModule)(nil) - -type EthEventHandler struct { - Chain *store.ChainStore - EventFilterManager *filter.EventFilterManager - TipSetFilterManager *filter.TipSetFilterManager - MemPoolFilterManager *filter.MemPoolFilterManager - FilterStore filter.FilterStore - SubManager *EthSubscriptionManager - MaxFilterHeightRange abi.ChainEpoch - SubscribtionCtx context.Context -} - -var _ EthEventAPI = (*EthEventHandler)(nil) - -type EthAPI struct { +type FullEthAPI struct { fx.In - Chain *store.ChainStore - StateManager *stmgr.StateManager - ChainIndexer index.Indexer - MpoolAPI MpoolAPI - - EthModuleAPI - EthEventAPI -} - -func (a *EthModule) StateNetworkName(ctx context.Context) (dtypes.NetworkName, error) { - return stmgr.GetNetworkName(ctx, a.StateManager, a.Chain.GetHeaviestTipSet().ParentState()) -} - -func (a *EthModule) EthBlockNumber(ctx context.Context) (ethtypes.EthUint64, error) { - // eth_blockNumber needs to return the height of the latest committed tipset. - // Ethereum clients expect all transactions included in this block to have execution outputs. - // This is the parent of the head tipset. The head tipset is speculative, has not been - // recognized by the network, and its messages are only included, not executed. - // See https://github.com/filecoin-project/ref-fvm/issues/1135. - heaviest := a.Chain.GetHeaviestTipSet() - if height := heaviest.Height(); height == 0 { - // we're at genesis. - return ethtypes.EthUint64(height), nil - } - // First non-null parent. - effectiveParent := heaviest.Parents() - parent, err := a.Chain.GetTipSetFromKey(ctx, effectiveParent) - if err != nil { - return 0, err - } - return ethtypes.EthUint64(parent.Height()), nil -} - -func (a *EthModule) EthAccounts(context.Context) ([]ethtypes.EthAddress, error) { - // The lotus node is not expected to hold manage accounts, so we'll always return an empty array - return []ethtypes.EthAddress{}, nil -} - -func (a *EthAPI) EthAddressToFilecoinAddress(ctx context.Context, ethAddress ethtypes.EthAddress) (address.Address, error) { - return ethAddress.ToFilecoinAddress() -} - -func (a *EthAPI) FilecoinAddressToEthAddress(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthAddress, error) { - params, err := jsonrpc.DecodeParams[ethtypes.FilecoinAddressToEthAddressParams](p) - if err != nil { - return ethtypes.EthAddress{}, xerrors.Errorf("decoding params: %w", err) - } - - filecoinAddress := params.FilecoinAddress - - // If the address is an "f0" or "f4" address, `EthAddressFromFilecoinAddress` will return the corresponding Ethereum address right away. - if eaddr, err := ethtypes.EthAddressFromFilecoinAddress(filecoinAddress); err == nil { - return eaddr, nil - } else if err != ethtypes.ErrInvalidAddress { - return ethtypes.EthAddress{}, xerrors.Errorf("error converting filecoin address to eth address: %w", err) - } - - // We should only be dealing with "f1"/"f2"/"f3" addresses from here-on. - switch filecoinAddress.Protocol() { - case address.SECP256K1, address.Actor, address.BLS: - // Valid protocols - default: - // Ideally, this should never happen but is here for sanity checking. - return ethtypes.EthAddress{}, xerrors.Errorf("invalid filecoin address protocol: %s", filecoinAddress.String()) - } - - var blkParam string - if params.BlkParam == nil { - blkParam = "finalized" - } else { - blkParam = *params.BlkParam - } - - ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkParam, false) - if err != nil { - return ethtypes.EthAddress{}, err - } - - // Lookup the ID address - idAddr, err := a.StateManager.LookupIDAddress(ctx, filecoinAddress, ts) - if err != nil { - return ethtypes.EthAddress{}, xerrors.Errorf( - "failed to lookup ID address for given Filecoin address %s ("+ - "ensure that the address has been instantiated on-chain and sufficient epochs have passed since instantiation to confirm to the given 'blkParam': \"%s\"): %w", - filecoinAddress, - blkParam, - err, - ) - } - - // Convert the ID address an ETH address - ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(idAddr) - if err != nil { - return ethtypes.EthAddress{}, xerrors.Errorf("failed to convert filecoin ID address %s to eth address: %w", idAddr, err) - } - - return ethAddr, nil -} - -func (a *EthModule) countTipsetMsgs(ctx context.Context, ts *types.TipSet) (int, error) { - blkMsgs, err := a.Chain.BlockMsgsForTipset(ctx, ts) - if err != nil { - return 0, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) - } - - count := 0 - for _, blkMsg := range blkMsgs { - // TODO: may need to run canonical ordering and deduplication here - count += len(blkMsg.BlsMessages) + len(blkMsg.SecpkMessages) - } - return count, nil -} - -func (a *EthModule) EthGetBlockTransactionCountByNumber(ctx context.Context, blkNum ethtypes.EthUint64) (ethtypes.EthUint64, error) { - ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(blkNum), nil, false) - if err != nil { - return ethtypes.EthUint64(0), xerrors.Errorf("error loading tipset %s: %w", ts, err) - } - - count, err := a.countTipsetMsgs(ctx, ts) - return ethtypes.EthUint64(count), err -} - -func (a *EthModule) EthGetBlockTransactionCountByHash(ctx context.Context, blkHash ethtypes.EthHash) (ethtypes.EthUint64, error) { - ts, err := a.Chain.GetTipSetByCid(ctx, blkHash.ToCid()) - if err != nil { - return ethtypes.EthUint64(0), xerrors.Errorf("error loading tipset %s: %w", ts, err) - } - count, err := a.countTipsetMsgs(ctx, ts) - return ethtypes.EthUint64(count), err -} - -func (a *EthModule) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthHash, fullTxInfo bool) (ethtypes.EthBlock, error) { - cache := a.EthBlkCache - if fullTxInfo { - cache = a.EthBlkTxCache - } - - // Attempt to retrieve the Ethereum block from cache - cid := blkHash.ToCid() - if cache != nil { - if ethBlock, found := cache.Get(cid); found { - if ethBlock != nil { - return *ethBlock, nil - } - // Log and remove the nil entry from cache - log.Errorw("nil value in eth block cache", "hash", blkHash.String()) - cache.Remove(cid) - } - } - - // Fetch the tipset using the block hash - ts, err := a.Chain.GetTipSetByCid(ctx, cid) - if err != nil { - return ethtypes.EthBlock{}, xerrors.Errorf("failed to load tipset by CID %s: %w", cid, err) - } - - // Generate an Ethereum block from the Filecoin tipset - blk, err := newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, a.Chain, a.StateAPI) - if err != nil { - return ethtypes.EthBlock{}, xerrors.Errorf("failed to create Ethereum block from Filecoin tipset: %w", err) - } - - // Add the newly created block to the cache and return - if cache != nil { - cache.Add(cid, &blk) - } - return blk, nil -} - -func (a *EthModule) EthGetBlockByNumber(ctx context.Context, blkParam string, fullTxInfo bool) (ethtypes.EthBlock, error) { - ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkParam, true) - if err != nil { - return ethtypes.EthBlock{}, err - } - return newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, a.Chain, a.StateAPI) -} - -func (a *EthModule) EthGetTransactionByHash(ctx context.Context, txHash *ethtypes.EthHash) (*ethtypes.EthTx, error) { - return a.EthGetTransactionByHashLimited(ctx, txHash, api.LookbackNoLimit) -} - -func (a *EthModule) EthGetTransactionByHashLimited(ctx context.Context, txHash *ethtypes.EthHash, limit abi.ChainEpoch) (*ethtypes.EthTx, error) { - // Ethereum's behavior is to return null when the txHash is invalid, so we use nil to check if txHash is valid - if txHash == nil { - return nil, nil - } - if a.ChainIndexer == nil { - return nil, ErrChainIndexerDisabled - } - - var c cid.Cid - var err error - c, err = a.ChainIndexer.GetCidFromHash(ctx, *txHash) - if err != nil && errors.Is(err, index.ErrNotFound) { - log.Debug("could not find transaction hash %s in chain indexer", txHash.String()) - } else if err != nil { - log.Errorf("failed to lookup transaction hash %s in chain indexer: %s", txHash.String(), err) - return nil, xerrors.Errorf("failed to lookup transaction hash %s in chain indexer: %w", txHash.String(), err) - } - - // This isn't an eth transaction we have the mapping for, so let's look it up as a filecoin message - if c == cid.Undef { - c = txHash.ToCid() - } - - // first, try to get the cid from mined transactions - msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, c, limit, true) - if err == nil && msgLookup != nil { - tx, err := newEthTxFromMessageLookup(ctx, msgLookup, -1, a.Chain, a.StateAPI) - if err == nil { - return &tx, nil - } - } - - // if not found, try to get it from the mempool - pending, err := a.MpoolAPI.MpoolPending(ctx, types.EmptyTSK) - if err != nil { - // inability to fetch mpool pending transactions is an internal node error - // that needs to be reported as-is - return nil, fmt.Errorf("cannot get pending txs from mpool: %s", err) - } - - for _, p := range pending { - if p.Cid() == c { - // We only return pending eth-account messages because we can't guarantee - // that the from/to addresses of other messages are conversable to 0x-style - // addresses. So we just ignore them. - // - // This should be "fine" as anyone using an "Ethereum-centric" block - // explorer shouldn't care about seeing pending messages from native - // accounts. - ethtx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(p) - if err != nil { - return nil, fmt.Errorf("could not convert Filecoin message into tx: %w", err) - } - - tx, err := ethtx.ToEthTx(p) - if err != nil { - return nil, fmt.Errorf("could not convert Eth transaction to EthTx: %w", err) - } - - return &tx, nil - } - } - // Ethereum clients expect an empty response when the message was not found - return nil, nil -} - -func (a *EthModule) EthGetMessageCidByTransactionHash(ctx context.Context, txHash *ethtypes.EthHash) (*cid.Cid, error) { - // Ethereum's behavior is to return null when the txHash is invalid, so we use nil to check if txHash is valid - if txHash == nil { - return nil, nil - } - if a.ChainIndexer == nil { - return nil, ErrChainIndexerDisabled - } - - var c cid.Cid - var err error - c, err = a.ChainIndexer.GetCidFromHash(ctx, *txHash) - if err != nil && errors.Is(err, index.ErrNotFound) { - log.Debug("could not find transaction hash %s in chain indexer", txHash.String()) - } else if err != nil { - log.Errorf("failed to lookup transaction hash %s in chain indexer: %s", txHash.String(), err) - return nil, xerrors.Errorf("failed to lookup transaction hash %s in chain indexer: %w", txHash.String(), err) - } - - if errors.Is(err, index.ErrNotFound) { - log.Debug("could not find transaction hash %s in lookup table", txHash.String()) - } else if a.ChainIndexer != nil { - return &c, nil - } - - // This isn't an eth transaction we have the mapping for, so let's try looking it up as a filecoin message - if c == cid.Undef { - c = txHash.ToCid() - } - - _, err = a.Chain.GetSignedMessage(ctx, c) - if err == nil { - // This is an Eth Tx, Secp message, Or BLS message in the mpool - return &c, nil - } - - _, err = a.Chain.GetMessage(ctx, c) - if err == nil { - // This is a BLS message - return &c, nil - } - - // Ethereum clients expect an empty response when the message was not found - return nil, nil -} - -func (a *EthModule) EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*ethtypes.EthHash, error) { - hash, err := ethTxHashFromMessageCid(ctx, cid, a.StateAPI) - if hash == ethtypes.EmptyEthHash { - // not found - return nil, nil - } - - return &hash, err -} - -func (a *EthModule) EthGetTransactionCount(ctx context.Context, sender ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthUint64, error) { - addr, err := sender.ToFilecoinAddress() - if err != nil { - return ethtypes.EthUint64(0), xerrors.Errorf("invalid address: %w", err) - } - - // Handle "pending" block parameter separately - if blkParam.PredefinedBlock != nil && *blkParam.PredefinedBlock == "pending" { - nonce, err := a.MpoolAPI.MpoolGetNonce(ctx, addr) - if err != nil { - return ethtypes.EthUint64(0), xerrors.Errorf("failed to get nonce from mpool: %w", err) - } - return ethtypes.EthUint64(nonce), nil - } - - // For all other cases, get the tipset based on the block parameter - 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) - } - - // Get the actor state at the specified tipset - actor, err := a.StateManager.LoadActor(ctx, addr, ts) - if err != nil { - if errors.Is(err, types.ErrActorNotFound) { - return 0, nil - } - return 0, xerrors.Errorf("failed to lookup actor %s: %w", sender, err) - } - - // Handle EVM actor case - if builtinactors.IsEvmActor(actor.Code) { - evmState, err := builtinevm.Load(a.Chain.ActorStore(ctx), actor) - if err != nil { - return 0, xerrors.Errorf("failed to load evm state: %w", err) - } - if alive, err := evmState.IsAlive(); err != nil { - return 0, err - } else if !alive { - return 0, nil - } - nonce, err := evmState.Nonce() - return ethtypes.EthUint64(nonce), err - } - - // For non-EVM actors, get the nonce from the actor state - return ethtypes.EthUint64(actor.Nonce), nil -} - -func (a *EthModule) EthGetTransactionReceipt(ctx context.Context, txHash ethtypes.EthHash) (*api.EthTxReceipt, error) { - return a.EthGetTransactionReceiptLimited(ctx, txHash, api.LookbackNoLimit) -} - -func (a *EthModule) EthGetTransactionReceiptLimited(ctx context.Context, txHash ethtypes.EthHash, limit abi.ChainEpoch) (*api.EthTxReceipt, error) { - var c cid.Cid - var err error - if a.ChainIndexer == nil { - return nil, ErrChainIndexerDisabled - } - - c, err = a.ChainIndexer.GetCidFromHash(ctx, txHash) - if err != nil && errors.Is(err, index.ErrNotFound) { - log.Debug("could not find transaction hash %s in chain indexer", txHash.String()) - } else if err != nil { - log.Errorf("failed to lookup transaction hash %s in chain indexer: %s", txHash.String(), err) - return nil, xerrors.Errorf("failed to lookup transaction hash %s in chain indexer: %w", txHash.String(), err) - } - - // This isn't an eth transaction we have the mapping for, so let's look it up as a filecoin message - if c == cid.Undef { - c = txHash.ToCid() - } - - msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, c, limit, true) - if err != nil { - return nil, xerrors.Errorf("failed to lookup Eth Txn %s as %s: %w", txHash, c, err) - } - if msgLookup == nil { - // This is the best we can do. In theory, we could have just not indexed this - // transaction, but there's no way to check that here. - return nil, nil - } - - tx, err := newEthTxFromMessageLookup(ctx, msgLookup, -1, a.Chain, a.StateAPI) - if err != nil { - return nil, xerrors.Errorf("failed to convert %s into an Eth Txn: %w", txHash, err) - } - - ts, err := a.Chain.GetTipSetFromKey(ctx, msgLookup.TipSet) - if err != nil { - return nil, xerrors.Errorf("failed to lookup tipset %s when constructing the eth txn receipt: %w", msgLookup.TipSet, err) - } - - // The tx is located in the parent tipset - parentTs, err := a.Chain.LoadTipSet(ctx, ts.Parents()) - if err != nil { - return nil, xerrors.Errorf("failed to lookup tipset %s when constructing the eth txn receipt: %w", ts.Parents(), err) - } - - baseFee := parentTs.Blocks()[0].ParentBaseFee - - receipt, err := newEthTxReceipt(ctx, tx, baseFee, msgLookup.Receipt, a.EthEventHandler) - if err != nil { - return nil, xerrors.Errorf("failed to create Eth receipt: %w", err) - } - - return &receipt, nil -} - -func (a *EthAPI) EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash ethtypes.EthHash, index ethtypes.EthUint64) (*ethtypes.EthTx, error) { - ts, err := a.Chain.GetTipSetByCid(ctx, blkHash.ToCid()) - if err != nil { - return nil, xerrors.Errorf("failed to get tipset by cid: %w", err) - } - - return a.getTransactionByTipsetAndIndex(ctx, ts, index) -} - -func (a *EthAPI) EthGetTransactionByBlockNumberAndIndex(ctx context.Context, blkParam string, index ethtypes.EthUint64) (*ethtypes.EthTx, error) { - ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkParam, true) - if err != nil { - return nil, err - } - - if ts == nil { - return nil, xerrors.Errorf("tipset not found for block %s", blkParam) - } - - tx, err := a.getTransactionByTipsetAndIndex(ctx, ts, index) - if err != nil { - return nil, xerrors.Errorf("failed to get transaction at index %d: %w", index, err) - } - - return tx, nil -} - -func (a *EthAPI) getTransactionByTipsetAndIndex(ctx context.Context, ts *types.TipSet, index ethtypes.EthUint64) (*ethtypes.EthTx, error) { - msgs, err := a.Chain.MessagesForTipset(ctx, ts) - if err != nil { - return nil, xerrors.Errorf("failed to get messages for tipset: %w", err) - } - - if uint64(index) >= uint64(len(msgs)) { - return nil, xerrors.Errorf("index %d out of range: tipset contains %d messages", index, len(msgs)) - } - - msg := msgs[index] - - cid, err := ts.Key().Cid() - if err != nil { - return nil, xerrors.Errorf("failed to get tipset key cid: %w", err) - } - - // First, get the state tree - st, err := a.StateManager.StateTree(ts.ParentState()) - if err != nil { - return nil, xerrors.Errorf("failed to load state tree: %w", err) - } - - tx, err := newEthTx(ctx, a.Chain, st, ts.Height(), cid, msg.Cid(), int(index)) - if err != nil { - return nil, xerrors.Errorf("failed to create Ethereum transaction: %w", err) - } - - return &tx, nil -} - -func (a *EthModule) EthGetBlockReceipts(ctx context.Context, blockParam ethtypes.EthBlockNumberOrHash) ([]*api.EthTxReceipt, error) { - return a.EthGetBlockReceiptsLimited(ctx, blockParam, api.LookbackNoLimit) -} - -func (a *EthModule) EthGetBlockReceiptsLimited(ctx context.Context, blockParam ethtypes.EthBlockNumberOrHash, limit abi.ChainEpoch) ([]*api.EthTxReceipt, error) { - ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blockParam) - if err != nil { - return nil, xerrors.Errorf("failed to get tipset: %w", err) - } - - tsCid, err := ts.Key().Cid() - if err != nil { - return nil, xerrors.Errorf("failed to get tipset key cid: %w", err) - } - - blkHash, err := ethtypes.EthHashFromCid(tsCid) - if err != nil { - return nil, xerrors.Errorf("failed to parse eth hash from cid: %w", err) - } - - // Execute the tipset to get the receipts, messages, and events - st, msgs, receipts, err := executeTipset(ctx, ts, a.Chain, a.StateAPI) - if err != nil { - return nil, xerrors.Errorf("failed to execute tipset: %w", err) - } - - // Load the state tree - stateTree, err := a.StateManager.StateTree(st) - if err != nil { - return nil, xerrors.Errorf("failed to load state tree: %w", err) - } - - baseFee := ts.Blocks()[0].ParentBaseFee - - ethReceipts := make([]*api.EthTxReceipt, 0, len(msgs)) - for i, msg := range msgs { - msg := msg - - tx, err := newEthTx(ctx, a.Chain, stateTree, ts.Height(), tsCid, msg.Cid(), i) - if err != nil { - return nil, xerrors.Errorf("failed to create EthTx: %w", err) - } - - receipt, err := newEthTxReceipt(ctx, tx, baseFee, receipts[i], a.EthEventHandler) - if err != nil { - return nil, xerrors.Errorf("failed to create Eth receipt: %w", err) - } - - // Set the correct Ethereum block hash - receipt.BlockHash = blkHash - - ethReceipts = append(ethReceipts, &receipt) - } - - return ethReceipts, nil -} - -// EthGetCode returns string value of the compiled bytecode -func (a *EthModule) EthGetCode(ctx context.Context, ethAddr ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { - to, err := ethAddr.ToFilecoinAddress() - if err != nil { - return nil, xerrors.Errorf("cannot get Filecoin address: %w", err) - } - - ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) - if err != nil { - return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) - } - - // StateManager.Call will panic if there is no parent - if ts.Height() == 0 { - return nil, xerrors.Errorf("block param must not specify genesis block") - } - - actor, err := a.StateManager.LoadActor(ctx, to, ts) - if err != nil { - if errors.Is(err, types.ErrActorNotFound) { - return nil, nil - } - return nil, xerrors.Errorf("failed to lookup contract %s: %w", ethAddr, err) - } - - // Not a contract. We could try to distinguish between accounts and "native" contracts here, - // but it's not worth it. - if !builtinactors.IsEvmActor(actor.Code) { - return nil, nil - } - - msg := &types.Message{ - From: builtinactors.SystemActorAddr, - To: to, - Value: big.Zero(), - Method: builtintypes.MethodsEVM.GetBytecode, - Params: nil, - GasLimit: buildconstants.BlockGasLimit, - GasFeeCap: big.Zero(), - GasPremium: big.Zero(), - } - - // Try calling until we find a height with no migration. - var res *api.InvocResult - for { - res, err = a.StateManager.Call(ctx, msg, ts) - if err != stmgr.ErrExpensiveFork { - break - } - ts, err = a.Chain.GetTipSetFromKey(ctx, ts.Parents()) - if err != nil { - return nil, xerrors.Errorf("getting parent tipset: %w", err) - } - } - - if err != nil { - return nil, xerrors.Errorf("failed to call GetBytecode: %w", err) - } - - if res.MsgRct == nil { - return nil, fmt.Errorf("no message receipt") - } - - if res.MsgRct.ExitCode.IsError() { - return nil, xerrors.Errorf("GetBytecode failed: %s", res.Error) - } - - var getBytecodeReturn evm.GetBytecodeReturn - if err := getBytecodeReturn.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil { - return nil, fmt.Errorf("failed to decode EVM bytecode CID: %w", err) - } - - // The contract has selfdestructed, so the code is "empty". - if getBytecodeReturn.Cid == nil { - return nil, nil - } - - blk, err := a.Chain.StateBlockstore().Get(ctx, *getBytecodeReturn.Cid) - if err != nil { - return nil, fmt.Errorf("failed to get EVM bytecode: %w", err) - } - - return blk.RawData(), nil -} - -func (a *EthModule) EthGetStorageAt(ctx context.Context, ethAddr ethtypes.EthAddress, position ethtypes.EthBytes, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { - ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) - if err != nil { - return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) - } - - l := len(position) - if l > 32 { - return nil, fmt.Errorf("supplied storage key is too long") - } - - // pad with zero bytes if smaller than 32 bytes - position = append(make([]byte, 32-l, 32), position...) - - to, err := ethAddr.ToFilecoinAddress() - if err != nil { - return nil, xerrors.Errorf("cannot get Filecoin address: %w", err) - } - - // use the system actor as the caller - from, err := address.NewIDAddress(0) - if err != nil { - return nil, fmt.Errorf("failed to construct system sender address: %w", err) - } - - actor, err := a.StateManager.LoadActor(ctx, to, ts) - if err != nil { - if errors.Is(err, types.ErrActorNotFound) { - return ethtypes.EthBytes(make([]byte, 32)), nil - } - return nil, xerrors.Errorf("failed to lookup contract %s: %w", ethAddr, err) - } - - if !builtinactors.IsEvmActor(actor.Code) { - return ethtypes.EthBytes(make([]byte, 32)), nil - } - - params, err := actors.SerializeParams(&evm.GetStorageAtParams{ - StorageKey: *(*[32]byte)(position), - }) - if err != nil { - return nil, fmt.Errorf("failed to serialize parameters: %w", err) - } - - msg := &types.Message{ - From: from, - To: to, - Value: big.Zero(), - Method: builtintypes.MethodsEVM.GetStorageAt, - Params: params, - GasLimit: buildconstants.BlockGasLimit, - GasFeeCap: big.Zero(), - GasPremium: big.Zero(), - } - - // Try calling until we find a height with no migration. - var res *api.InvocResult - for { - res, err = a.StateManager.Call(ctx, msg, ts) - if err != stmgr.ErrExpensiveFork { - break - } - ts, err = a.Chain.GetTipSetFromKey(ctx, ts.Parents()) - if err != nil { - return nil, xerrors.Errorf("getting parent tipset: %w", err) - } - } - - if err != nil { - return nil, xerrors.Errorf("Call failed: %w", err) - } - - if res.MsgRct == nil { - return nil, xerrors.Errorf("no message receipt") - } - - if res.MsgRct.ExitCode.IsError() { - return nil, xerrors.Errorf("failed to lookup storage slot: %s", res.Error) - } - - var ret abi.CborBytes - if err := ret.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil { - return nil, xerrors.Errorf("failed to unmarshal storage slot: %w", err) - } - - // pad with zero bytes if smaller than 32 bytes - ret = append(make([]byte, 32-len(ret), 32), ret...) - - return ethtypes.EthBytes(ret), nil -} - -func (a *EthModule) EthGetBalance(ctx context.Context, address ethtypes.EthAddress, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBigInt, error) { - filAddr, err := address.ToFilecoinAddress() - if err != nil { - return ethtypes.EthBigInt{}, err - } - - ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) - if err != nil { - return ethtypes.EthBigInt{}, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) - } - - st, _, err := a.StateManager.TipSetState(ctx, ts) - if err != nil { - return ethtypes.EthBigInt{}, xerrors.Errorf("failed to compute tipset state: %w", err) - } - - actor, err := a.StateManager.LoadActorRaw(ctx, filAddr, st) - if errors.Is(err, types.ErrActorNotFound) { - return ethtypes.EthBigIntZero, nil - } else if err != nil { - return ethtypes.EthBigInt{}, err - } - - return ethtypes.EthBigInt{Int: actor.Balance.Int}, nil -} - -func (a *EthModule) EthChainId(ctx context.Context) (ethtypes.EthUint64, error) { - return ethtypes.EthUint64(buildconstants.Eip155ChainId), nil -} - -func (a *EthModule) EthSyncing(ctx context.Context) (ethtypes.EthSyncingResult, error) { - state, err := a.SyncAPI.SyncState(ctx) - if err != nil { - return ethtypes.EthSyncingResult{}, fmt.Errorf("failed calling SyncState: %w", err) - } - - if len(state.ActiveSyncs) == 0 { - return ethtypes.EthSyncingResult{}, errors.New("no active syncs, try again") - } - - working := -1 - for i, ss := range state.ActiveSyncs { - if ss.Stage == api.StageIdle { - continue - } - working = i - } - if working == -1 { - working = len(state.ActiveSyncs) - 1 - } - - ss := state.ActiveSyncs[working] - if ss.Base == nil || ss.Target == nil { - return ethtypes.EthSyncingResult{}, errors.New("missing syncing information, try again") - } - - res := ethtypes.EthSyncingResult{ - DoneSync: ss.Stage == api.StageSyncComplete, - CurrentBlock: ethtypes.EthUint64(ss.Height), - StartingBlock: ethtypes.EthUint64(ss.Base.Height()), - HighestBlock: ethtypes.EthUint64(ss.Target.Height()), - } - - return res, nil -} - -func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthFeeHistory, error) { - params, err := jsonrpc.DecodeParams[ethtypes.EthFeeHistoryParams](p) - if err != nil { - return ethtypes.EthFeeHistory{}, xerrors.Errorf("decoding params: %w", err) - } - if params.BlkCount > 1024 { - return ethtypes.EthFeeHistory{}, fmt.Errorf("block count should be smaller than 1024") - } - rewardPercentiles := make([]float64, 0) - if params.RewardPercentiles != nil { - if len(*params.RewardPercentiles) > maxEthFeeHistoryRewardPercentiles { - return ethtypes.EthFeeHistory{}, errors.New("length of the reward percentile array cannot be greater than 100") - } - rewardPercentiles = append(rewardPercentiles, *params.RewardPercentiles...) - } - for i, rp := range rewardPercentiles { - if rp < 0 || rp > 100 { - return ethtypes.EthFeeHistory{}, fmt.Errorf("invalid reward percentile: %f should be between 0 and 100", rp) - } - if i > 0 && rp < rewardPercentiles[i-1] { - return ethtypes.EthFeeHistory{}, fmt.Errorf("invalid reward percentile: %f should be larger than %f", rp, rewardPercentiles[i-1]) - } - } - - ts, err := getTipsetByBlockNumber(ctx, a.Chain, params.NewestBlkNum, false) - if err != nil { - return ethtypes.EthFeeHistory{}, err - } - - var ( - basefee = ts.Blocks()[0].ParentBaseFee - oldestBlkHeight = uint64(1) - - // NOTE: baseFeePerGas should include the next block after the newest of the returned range, - // because the next base fee can be inferred from the messages in the newest block. - // However, this is NOT the case in Filecoin due to deferred execution, so the best - // we can do is duplicate the last value. - baseFeeArray = []ethtypes.EthBigInt{ethtypes.EthBigInt(basefee)} - rewardsArray = make([][]ethtypes.EthBigInt, 0) - gasUsedRatioArray = []float64{} - blocksIncluded int - ) - - for blocksIncluded < int(params.BlkCount) && ts.Height() > 0 { - basefee = ts.Blocks()[0].ParentBaseFee - _, msgs, rcpts, err := executeTipset(ctx, ts, a.Chain, a.StateAPI) - if err != nil { - return ethtypes.EthFeeHistory{}, xerrors.Errorf("failed to retrieve messages and receipts for height %d: %w", ts.Height(), err) - } - - txGasRewards := gasRewardSorter{} - for i, msg := range msgs { - effectivePremium := msg.VMMessage().EffectiveGasPremium(basefee) - txGasRewards = append(txGasRewards, gasRewardTuple{ - premium: effectivePremium, - gasUsed: rcpts[i].GasUsed, - }) - } - - rewards, totalGasUsed := calculateRewardsAndGasUsed(rewardPercentiles, txGasRewards) - maxGas := buildconstants.BlockGasLimit * int64(len(ts.Blocks())) - - // arrays should be reversed at the end - baseFeeArray = append(baseFeeArray, ethtypes.EthBigInt(basefee)) - gasUsedRatioArray = append(gasUsedRatioArray, float64(totalGasUsed)/float64(maxGas)) - rewardsArray = append(rewardsArray, rewards) - oldestBlkHeight = uint64(ts.Height()) - blocksIncluded++ - - parentTsKey := ts.Parents() - ts, err = a.Chain.LoadTipSet(ctx, parentTsKey) - if err != nil { - return ethtypes.EthFeeHistory{}, fmt.Errorf("cannot load tipset key: %v", parentTsKey) - } - } - - // Reverse the arrays; we collected them newest to oldest; the client expects oldest to newest. - for i, j := 0, len(baseFeeArray)-1; i < j; i, j = i+1, j-1 { - baseFeeArray[i], baseFeeArray[j] = baseFeeArray[j], baseFeeArray[i] - } - for i, j := 0, len(gasUsedRatioArray)-1; i < j; i, j = i+1, j-1 { - gasUsedRatioArray[i], gasUsedRatioArray[j] = gasUsedRatioArray[j], gasUsedRatioArray[i] - } - for i, j := 0, len(rewardsArray)-1; i < j; i, j = i+1, j-1 { - rewardsArray[i], rewardsArray[j] = rewardsArray[j], rewardsArray[i] - } - - ret := ethtypes.EthFeeHistory{ - OldestBlock: ethtypes.EthUint64(oldestBlkHeight), - BaseFeePerGas: baseFeeArray, - GasUsedRatio: gasUsedRatioArray, - } - if params.RewardPercentiles != nil { - ret.Reward = &rewardsArray - } - return ret, nil -} - -func (a *EthModule) NetVersion(_ context.Context) (string, error) { - return strconv.FormatInt(buildconstants.Eip155ChainId, 10), nil -} - -func (a *EthModule) NetListening(ctx context.Context) (bool, error) { - return true, nil -} - -func (a *EthModule) EthProtocolVersion(ctx context.Context) (ethtypes.EthUint64, error) { - height := a.Chain.GetHeaviestTipSet().Height() - return ethtypes.EthUint64(a.StateManager.GetNetworkVersion(ctx, height)), nil -} - -func (a *EthModule) EthMaxPriorityFeePerGas(ctx context.Context) (ethtypes.EthBigInt, error) { - gasPremium, err := a.GasAPI.GasEstimateGasPremium(ctx, 0, builtinactors.SystemActorAddr, 10000, types.EmptyTSK) - if err != nil { - return ethtypes.EthBigInt(big.Zero()), err - } - return ethtypes.EthBigInt(gasPremium), nil -} - -func (a *EthModule) EthGasPrice(ctx context.Context) (ethtypes.EthBigInt, error) { - // According to Geth's implementation, eth_gasPrice should return base + tip - // Ref: https://github.com/ethereum/pm/issues/328#issuecomment-853234014 - - ts := a.Chain.GetHeaviestTipSet() - baseFee := ts.Blocks()[0].ParentBaseFee - - premium, err := a.EthMaxPriorityFeePerGas(ctx) - if err != nil { - return ethtypes.EthBigInt(big.Zero()), nil - } - - gasPrice := big.Add(baseFee, big.Int(premium)) - return ethtypes.EthBigInt(gasPrice), nil -} - -func (a *EthModule) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) { - return ethSendRawTransaction(ctx, a.MpoolAPI, a.ChainIndexer, rawTx, false) -} - -func (a *EthAPI) EthSendRawTransactionUntrusted(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) { - return ethSendRawTransaction(ctx, a.MpoolAPI, a.ChainIndexer, rawTx, true) -} - -func ethSendRawTransaction(ctx context.Context, mpool MpoolAPI, indexer index.Indexer, rawTx ethtypes.EthBytes, untrusted bool) (ethtypes.EthHash, error) { - txArgs, err := ethtypes.ParseEthTransaction(rawTx) - if err != nil { - return ethtypes.EmptyEthHash, err - } - - txHash, err := txArgs.TxHash() - if err != nil { - return ethtypes.EmptyEthHash, err - } - - smsg, err := ethtypes.ToSignedFilecoinMessage(txArgs) - if err != nil { - return ethtypes.EmptyEthHash, err - } - - if untrusted { - if _, err = mpool.MpoolPushUntrusted(ctx, smsg); err != nil { - return ethtypes.EmptyEthHash, err - } - } else { - if _, err = mpool.MpoolPush(ctx, smsg); err != nil { - return ethtypes.EmptyEthHash, err - } - } - - // make it immediately available in the transaction hash lookup db, even though it will also - // eventually get there via the mpool - if indexer != nil { - if err := indexer.IndexEthTxHash(ctx, txHash, smsg.Cid()); err != nil { - log.Errorf("error indexing tx: %s", err) - } - } - - return ethtypes.EthHashFromTxBytes(rawTx), nil -} - -func (a *EthModule) Web3ClientVersion(ctx context.Context) (string, error) { - return string(build.NodeUserVersion()), nil -} - -func (a *EthModule) EthTraceBlock(ctx context.Context, blkNum string) ([]*ethtypes.EthTraceBlock, error) { - ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, true) - if err != nil { - return nil, err - } - - stRoot, trace, err := a.StateManager.ExecutionTrace(ctx, ts) - if err != nil { - return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) - } - - st, err := a.StateManager.StateTree(stRoot) - if err != nil { - return nil, xerrors.Errorf("failed load computed state-tree: %w", err) - } - - cid, err := ts.Key().Cid() - if err != nil { - return nil, xerrors.Errorf("failed to get tipset key cid: %w", err) - } - - blkHash, err := ethtypes.EthHashFromCid(cid) - if err != nil { - return nil, xerrors.Errorf("failed to parse eth hash from cid: %w", err) - } - - allTraces := make([]*ethtypes.EthTraceBlock, 0, len(trace)) - msgIdx := 0 - for _, ir := range trace { - // ignore messages from system actor - if ir.Msg.From == builtinactors.SystemActorAddr { - continue - } - - msgIdx++ - - txHash, err := a.EthGetTransactionHashByCid(ctx, ir.MsgCid) - if err != nil { - return nil, xerrors.Errorf("failed to get transaction hash by cid: %w", err) - } - if txHash == nil { - return nil, xerrors.Errorf("cannot find transaction hash for cid %s", ir.MsgCid) - } - - env, err := baseEnvironment(st, ir.Msg.From) - if err != nil { - return nil, xerrors.Errorf("when processing message %s: %w", ir.MsgCid, err) - } - - err = buildTraces(env, []int{}, &ir.ExecutionTrace) - if err != nil { - return nil, xerrors.Errorf("failed building traces for msg %s: %w", ir.MsgCid, err) - } - - for _, trace := range env.traces { - allTraces = append(allTraces, ðtypes.EthTraceBlock{ - EthTrace: trace, - BlockHash: blkHash, - BlockNumber: int64(ts.Height()), - TransactionHash: *txHash, - TransactionPosition: msgIdx, - }) - } - } - - return allTraces, nil -} - -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") - } - ts, err := getTipsetByBlockNumber(ctx, a.Chain, blkNum, true) - if err != nil { - return nil, err - } - - stRoot, trace, err := a.StateManager.ExecutionTrace(ctx, ts) - if err != nil { - return nil, xerrors.Errorf("failed when calling ExecutionTrace: %w", err) - } - - st, err := a.StateManager.StateTree(stRoot) - if err != nil { - return nil, xerrors.Errorf("failed load computed state-tree: %w", err) - } - - allTraces := make([]*ethtypes.EthTraceReplayBlockTransaction, 0, len(trace)) - for _, ir := range trace { - // ignore messages from system actor - if ir.Msg.From == builtinactors.SystemActorAddr { - continue - } - - txHash, err := a.EthGetTransactionHashByCid(ctx, ir.MsgCid) - if err != nil { - return nil, xerrors.Errorf("failed to get transaction hash by cid: %w", err) - } - if txHash == nil { - return nil, xerrors.Errorf("cannot find transaction hash for cid %s", ir.MsgCid) - } - - env, err := baseEnvironment(st, ir.Msg.From) - if err != nil { - return nil, xerrors.Errorf("when processing message %s: %w", ir.MsgCid, err) - } - - err = buildTraces(env, []int{}, &ir.ExecutionTrace) - if err != nil { - return nil, xerrors.Errorf("failed building traces for msg %s: %w", ir.MsgCid, err) - } - - var output []byte - if len(env.traces) > 0 { - switch r := env.traces[0].Result.(type) { - case *ethtypes.EthCallTraceResult: - output = r.Output - case *ethtypes.EthCreateTraceResult: - output = r.Code - } - } - - allTraces = append(allTraces, ðtypes.EthTraceReplayBlockTransaction{ - Output: output, - TransactionHash: *txHash, - Trace: env.traces, - StateDiff: nil, - VmTrace: nil, - }) - } - - return allTraces, nil -} - -func (a *EthModule) EthTraceTransaction(ctx context.Context, txHash string) ([]*ethtypes.EthTraceTransaction, error) { - - // convert from string to internal type - ethTxHash, err := ethtypes.ParseEthHash(txHash) - if err != nil { - return nil, xerrors.Errorf("cannot parse eth hash: %w", err) - } - - tx, err := a.EthGetTransactionByHash(ctx, ðTxHash) - if err != nil { - return nil, xerrors.Errorf("cannot get transaction by hash: %w", err) - } - - if tx == nil { - return nil, xerrors.Errorf("transaction not found") - } - - // tx.BlockNumber is nil when the transaction is still in the mpool/pending - if tx.BlockNumber == nil { - return nil, xerrors.Errorf("no trace for pending transactions") - } - - blockTraces, err := a.EthTraceBlock(ctx, strconv.FormatUint(uint64(*tx.BlockNumber), 10)) - if err != nil { - return nil, xerrors.Errorf("cannot get trace for block: %w", err) - } - - txTraces := make([]*ethtypes.EthTraceTransaction, 0, len(blockTraces)) - for _, blockTrace := range blockTraces { - if blockTrace.TransactionHash == ethTxHash { - // Create a new EthTraceTransaction from the block trace - txTrace := ethtypes.EthTraceTransaction{ - EthTrace: blockTrace.EthTrace, - BlockHash: blockTrace.BlockHash, - BlockNumber: blockTrace.BlockNumber, - TransactionHash: blockTrace.TransactionHash, - TransactionPosition: blockTrace.TransactionPosition, - } - txTraces = append(txTraces, &txTrace) - } - } - - return txTraces, nil -} - -func (a *EthModule) EthTraceFilter(ctx context.Context, filter ethtypes.EthTraceFilterCriteria) ([]*ethtypes.EthTraceFilterResult, error) { - // Define EthBlockNumberFromString as a private function within EthTraceFilter - getEthBlockNumberFromString := func(ctx context.Context, block *string) (ethtypes.EthUint64, error) { - head := a.Chain.GetHeaviestTipSet() - - blockValue := "latest" - if block != nil { - blockValue = *block - } - - switch blockValue { - case "earliest": - return 0, xerrors.Errorf("block param \"earliest\" is not supported") - case "pending": - return ethtypes.EthUint64(head.Height()), nil - case "latest": - parent, err := a.Chain.GetTipSetFromKey(ctx, head.Parents()) - if err != nil { - return 0, fmt.Errorf("cannot get parent tipset") - } - return ethtypes.EthUint64(parent.Height()), nil - case "safe": - latestHeight := head.Height() - 1 - safeHeight := latestHeight - ethtypes.SafeEpochDelay - return ethtypes.EthUint64(safeHeight), nil - default: - blockNum, err := ethtypes.EthUint64FromHex(blockValue) - if err != nil { - return 0, xerrors.Errorf("cannot parse fromBlock: %w", err) - } - return blockNum, err - } - } - - fromBlock, err := getEthBlockNumberFromString(ctx, filter.FromBlock) - if err != nil { - return nil, xerrors.Errorf("cannot parse fromBlock: %w", err) - } - - toBlock, err := getEthBlockNumberFromString(ctx, filter.ToBlock) - if err != nil { - return nil, xerrors.Errorf("cannot parse toBlock: %w", err) - } - - var results []*ethtypes.EthTraceFilterResult - - if filter.Count != nil { - // If filter.Count is specified and it is 0, return an empty result set immediately. - if *filter.Count == 0 { - return []*ethtypes.EthTraceFilterResult{}, nil - } - - // If filter.Count is specified and is greater than the EthTraceFilterMaxResults config return error - if uint64(*filter.Count) > a.EthTraceFilterMaxResults { - return nil, xerrors.Errorf("invalid response count, requested %d, maximum supported is %d", *filter.Count, a.EthTraceFilterMaxResults) - } - } - - traceCounter := ethtypes.EthUint64(0) - for blkNum := fromBlock; blkNum <= toBlock; blkNum++ { - blockTraces, err := a.EthTraceBlock(ctx, strconv.FormatUint(uint64(blkNum), 10)) - if err != nil { - if errors.Is(err, &api.ErrNullRound{}) { - continue - } - return nil, xerrors.Errorf("cannot get trace for block %d: %w", blkNum, err) - } - - for _, _blockTrace := range blockTraces { - // Create a copy of blockTrace to avoid pointer quirks - blockTrace := *_blockTrace - match, err := matchFilterCriteria(&blockTrace, filter, filter.FromAddress, filter.ToAddress) - if err != nil { - return nil, xerrors.Errorf("cannot match filter for block %d: %w", blkNum, err) - } - if !match { - continue - } - traceCounter++ - if filter.After != nil && traceCounter <= *filter.After { - continue - } - - txTrace := ethtypes.EthTraceFilterResult{ - EthTrace: blockTrace.EthTrace, - BlockHash: blockTrace.BlockHash, - BlockNumber: blockTrace.BlockNumber, - TransactionHash: blockTrace.TransactionHash, - TransactionPosition: blockTrace.TransactionPosition, - } - results = append(results, &txTrace) - - // If Count is specified, limit the results - if filter.Count != nil && ethtypes.EthUint64(len(results)) >= *filter.Count { - return results, nil - } else if filter.Count == nil && uint64(len(results)) > a.EthTraceFilterMaxResults { - return nil, xerrors.Errorf("too many results, maximum supported is %d, try paginating requests with After and Count", a.EthTraceFilterMaxResults) - } - } - } - - return results, nil -} - -// matchFilterCriteria checks if a trace matches the filter criteria. -func matchFilterCriteria(trace *ethtypes.EthTraceBlock, filter ethtypes.EthTraceFilterCriteria, fromDecodedAddresses []ethtypes.EthAddress, toDecodedAddresses []ethtypes.EthAddress) (bool, error) { - - var traceTo ethtypes.EthAddress - var traceFrom ethtypes.EthAddress - - switch trace.Type { - case "call": - action, ok := trace.Action.(*ethtypes.EthCallTraceAction) - if !ok { - return false, xerrors.Errorf("invalid call trace action") - } - traceTo = action.To - traceFrom = action.From - case "create": - result, okResult := trace.Result.(*ethtypes.EthCreateTraceResult) - if !okResult { - return false, xerrors.Errorf("invalid create trace result") - } - - action, okAction := trace.Action.(*ethtypes.EthCreateTraceAction) - if !okAction { - return false, xerrors.Errorf("invalid create trace action") - } - - if result.Address == nil { - return false, xerrors.Errorf("address is nil in create trace result") - } - - traceTo = *result.Address - traceFrom = action.From - default: - return false, xerrors.Errorf("invalid trace type: %s", trace.Type) - } - - // Match FromAddress - if len(fromDecodedAddresses) > 0 { - fromMatch := false - for _, ethAddr := range fromDecodedAddresses { - if traceFrom == ethAddr { - fromMatch = true - break - } - } - if !fromMatch { - return false, nil - } - } - - // Match ToAddress - if len(toDecodedAddresses) > 0 { - toMatch := false - for _, ethAddr := range toDecodedAddresses { - if traceTo == ethAddr { - toMatch = true - break - } - } - if !toMatch { - return false, nil - } - } - - return true, nil -} - -func (a *EthModule) applyMessage(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (res *api.InvocResult, err error) { - ts, err := a.Chain.GetTipSetFromKey(ctx, tsk) - if err != nil { - return nil, xerrors.Errorf("cannot get tipset: %w", err) - } - - if ts.Height() > 0 { - pts, err := a.Chain.GetTipSetFromKey(ctx, ts.Parents()) - if err != nil { - return nil, xerrors.Errorf("failed to find a non-forking epoch: %w", err) - } - // Check for expensive forks from the parents to the tipset, including nil tipsets - if a.StateManager.HasExpensiveForkBetween(pts.Height(), ts.Height()+1) { - return nil, stmgr.ErrExpensiveFork - } - } - - st, _, err := a.StateManager.TipSetState(ctx, ts) - if err != nil { - return nil, xerrors.Errorf("cannot get tipset state: %w", err) - } - res, err = a.StateManager.ApplyOnStateWithGas(ctx, st, msg, ts) - if err != nil { - return nil, xerrors.Errorf("ApplyWithGasOnState failed: %w", err) - } - - if res.MsgRct.ExitCode.IsError() { - reason := "none" - var cbytes abi.CborBytes - if err := cbytes.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil { - log.Warnw("failed to unmarshal cbor bytes from message receipt return", "error", err) - reason = "ERROR: revert reason is not cbor encoded bytes" - } // else leave as empty bytes - if len(cbytes) > 0 { - reason = parseEthRevert(cbytes) - } - return nil, api.NewErrExecutionReverted(res.MsgRct.ExitCode, reason, res.Error, cbytes) - } - - return res, nil -} - -func (a *EthModule) EthEstimateGas(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthUint64, error) { - params, err := jsonrpc.DecodeParams[ethtypes.EthEstimateGasParams](p) - if err != nil { - return ethtypes.EthUint64(0), xerrors.Errorf("decoding params: %w", err) - } - - msg, err := ethCallToFilecoinMessage(ctx, params.Tx) - if err != nil { - return ethtypes.EthUint64(0), err - } - - // Set the gas limit to the zero sentinel value, which makes - // gas estimation actually run. - msg.GasLimit = 0 - - var ts *types.TipSet - if params.BlkParam == nil { - ts = a.Chain.GetHeaviestTipSet() - } else { - ts, err = getTipsetByEthBlockNumberOrHash(ctx, a.Chain, *params.BlkParam) - if err != nil { - return ethtypes.EthUint64(0), xerrors.Errorf("failed to process block param: %v; %w", params.BlkParam, err) - } - } - - gassedMsg, err := a.GasAPI.GasEstimateMessageGas(ctx, msg, nil, ts.Key()) - if err != nil { - // On failure, GasEstimateMessageGas doesn't actually return the invocation result, - // it just returns an error. That means we can't get the revert reason. - // - // So we re-execute the message with EthCall (well, applyMessage which contains the - // guts of EthCall). This will give us an ethereum specific error with revert - // information. - msg.GasLimit = buildconstants.BlockGasLimit - if _, err2 := a.applyMessage(ctx, msg, ts.Key()); err2 != nil { - // If err2 is an ExecutionRevertedError, return it - var ed *api.ErrExecutionReverted - if errors.As(err2, &ed) { - return ethtypes.EthUint64(0), err2 - } - - // Otherwise, return the error from applyMessage with failed to estimate gas - err = err2 - } - - return ethtypes.EthUint64(0), xerrors.Errorf("failed to estimate gas: %w", err) - } - - expectedGas, err := ethGasSearch(ctx, a.Chain, a.Stmgr, a.Mpool, gassedMsg, ts) - if err != nil { - return 0, xerrors.Errorf("gas search failed: %w", err) - } - - return ethtypes.EthUint64(expectedGas), nil -} - -// gasSearch does an exponential search to find a gas value to execute the -// message with. It first finds a high gas limit that allows the message to execute -// by doubling the previous gas limit until it succeeds then does a binary -// search till it gets within a range of 1% -func gasSearch( - ctx context.Context, - smgr *stmgr.StateManager, - msgIn *types.Message, - priorMsgs []types.ChainMsg, - ts *types.TipSet, -) (int64, error) { - msg := *msgIn - - high := msg.GasLimit - low := msg.GasLimit - - applyTsMessages := true - if os.Getenv("LOTUS_SKIP_APPLY_TS_MESSAGE_CALL_WITH_GAS") == "1" { - applyTsMessages = false - } - - canSucceed := func(limit int64) (bool, error) { - msg.GasLimit = limit - - res, err := smgr.CallWithGas(ctx, &msg, priorMsgs, ts, applyTsMessages) - if err != nil { - return false, xerrors.Errorf("CallWithGas failed: %w", err) - } - - if res.MsgRct.ExitCode.IsSuccess() { - return true, nil - } - - return false, nil - } - - for { - ok, err := canSucceed(high) - if err != nil { - return -1, xerrors.Errorf("searching for high gas limit failed: %w", err) - } - if ok { - break - } - - low = high - high = high * 2 - - if high > buildconstants.BlockGasLimit { - high = buildconstants.BlockGasLimit - break - } - } - - checkThreshold := high / 100 - for (high - low) > checkThreshold { - median := (low + high) / 2 - ok, err := canSucceed(median) - if err != nil { - return -1, xerrors.Errorf("searching for optimal gas limit failed: %w", err) - } - - if ok { - high = median - } else { - low = median - } - - checkThreshold = median / 100 - } - - return high, nil -} - -func traceContainsExitCode(et types.ExecutionTrace, ex exitcode.ExitCode) bool { - if et.MsgRct.ExitCode == ex { - return true - } - - for _, et := range et.Subcalls { - if traceContainsExitCode(et, ex) { - return true - } - } - - return false -} - -// ethGasSearch executes a message for gas estimation using the previously estimated gas. -// If the message fails due to an out of gas error then a gas search is performed. -// See gasSearch. -func ethGasSearch( - ctx context.Context, - cstore *store.ChainStore, - smgr *stmgr.StateManager, - mpool *messagepool.MessagePool, - msgIn *types.Message, - ts *types.TipSet, -) (int64, error) { - msg := *msgIn - currTs := ts - - res, priorMsgs, ts, err := gasEstimateCallWithGas(ctx, cstore, smgr, mpool, &msg, currTs) - if err != nil { - return -1, xerrors.Errorf("gas estimation failed: %w", err) - } - - if res.MsgRct.ExitCode.IsSuccess() { - return msg.GasLimit, nil - } - - if traceContainsExitCode(res.ExecutionTrace, exitcode.SysErrOutOfGas) { - ret, err := gasSearch(ctx, smgr, &msg, priorMsgs, ts) - if err != nil { - return -1, xerrors.Errorf("gas estimation search failed: %w", err) - } - - ret = int64(float64(ret) * mpool.GetConfig().GasLimitOverestimation) - return ret, nil - } - - return -1, xerrors.Errorf("message execution failed: exit %s, reason: %s", res.MsgRct.ExitCode, res.Error) -} - -func (a *EthModule) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam ethtypes.EthBlockNumberOrHash) (ethtypes.EthBytes, error) { - msg, err := ethCallToFilecoinMessage(ctx, tx) - if err != nil { - return nil, xerrors.Errorf("failed to convert ethcall to filecoin message: %w", err) - } - - ts, err := getTipsetByEthBlockNumberOrHash(ctx, a.Chain, blkParam) - if err != nil { - return nil, xerrors.Errorf("failed to process block param: %v; %w", blkParam, err) - } - - invokeResult, err := a.applyMessage(ctx, msg, ts.Key()) - if err != nil { - return nil, err - } - - if msg.To == builtintypes.EthereumAddressManagerActorAddr { - return ethtypes.EthBytes{}, nil - } else if len(invokeResult.MsgRct.Return) > 0 { - return cbg.ReadByteArray(bytes.NewReader(invokeResult.MsgRct.Return), uint64(len(invokeResult.MsgRct.Return))) - } - - return ethtypes.EthBytes{}, nil -} - -// TODO: For now, we're fetching logs from the index for the entire block and then filtering them by the transaction hash -// This allows us to use the current schema of the event Index DB that has been optimised to use the "tipset_key_cid" index -// However, this can be replaced to filter logs in the event Index DB by the "msgCid" if we pass it down to the query generator -func (e *EthEventHandler) getEthLogsForBlockAndTransaction(ctx context.Context, blockHash *ethtypes.EthHash, txHash ethtypes.EthHash) ([]ethtypes.EthLog, error) { - ces, err := e.ethGetEventsForFilter(ctx, ðtypes.EthFilterSpec{BlockHash: blockHash}) - if err != nil { - return nil, err - } - logs, err := ethFilterLogsFromEvents(ctx, ces, e.SubManager.StateAPI) - if err != nil { - return nil, err - } - var out []ethtypes.EthLog - for _, log := range logs { - if log.TransactionHash == txHash { - out = append(out, log) - } - } - return out, nil -} - -func (e *EthEventHandler) EthGetLogs(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) { - ces, err := e.ethGetEventsForFilter(ctx, filterSpec) - if err != nil { - return nil, xerrors.Errorf("failed to get events for filter: %w", err) - } - return ethFilterResultFromEvents(ctx, ces, e.SubManager.StateAPI) -} - -func (e *EthEventHandler) ethGetEventsForFilter(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) ([]*index.CollectedEvent, error) { - if e.EventFilterManager == nil { - return nil, api.ErrNotSupported - } - - if e.EventFilterManager.ChainIndexer == nil { - return nil, ErrChainIndexerDisabled - } - - pf, err := e.parseEthFilterSpec(filterSpec) - if err != nil { - return nil, xerrors.Errorf("failed to parse eth filter spec: %w", err) - } - - head := e.Chain.GetHeaviestTipSet() - // should not ask for events for a tipset >= head because of deferred execution - if pf.tipsetCid != cid.Undef { - ts, err := e.Chain.GetTipSetByCid(ctx, pf.tipsetCid) - if err != nil { - return nil, xerrors.Errorf("failed to get tipset by cid: %w", err) - } - if ts.Height() >= head.Height() { - return nil, xerrors.New("cannot ask for events for a tipset at or greater than head") - } - } - - if pf.minHeight >= head.Height() || pf.maxHeight >= head.Height() { - return nil, xerrors.New("cannot ask for events for a tipset at or greater than head") - } - - ef := &index.EventFilter{ - MinHeight: pf.minHeight, - MaxHeight: pf.maxHeight, - TipsetCid: pf.tipsetCid, - Addresses: pf.addresses, - KeysWithCodec: pf.keys, - Codec: multicodec.Raw, - MaxResults: e.EventFilterManager.MaxFilterResults, - } - - ces, err := e.EventFilterManager.ChainIndexer.GetEventsForFilter(ctx, ef) - if err != nil { - return nil, xerrors.Errorf("failed to get events for filter from chain indexer: %w", err) - } - - return ces, nil -} - -func (e *EthEventHandler) EthGetFilterChanges(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { - if e.FilterStore == nil { - return nil, api.ErrNotSupported - } - - f, err := e.FilterStore.Get(ctx, types.FilterID(id)) - if err != nil { - return nil, err - } - - switch fc := f.(type) { - case filterEventCollector: - return ethFilterResultFromEvents(ctx, fc.TakeCollectedEvents(ctx), e.SubManager.StateAPI) - case filterTipSetCollector: - return ethFilterResultFromTipSets(fc.TakeCollectedTipSets(ctx)) - case filterMessageCollector: - return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx)) - } - - return nil, xerrors.Errorf("unknown filter type") -} - -func (e *EthEventHandler) EthGetFilterLogs(ctx context.Context, id ethtypes.EthFilterID) (*ethtypes.EthFilterResult, error) { - if e.FilterStore == nil { - return nil, api.ErrNotSupported - } - - f, err := e.FilterStore.Get(ctx, types.FilterID(id)) - if err != nil { - return nil, err - } - - switch fc := f.(type) { - case filterEventCollector: - return ethFilterResultFromEvents(ctx, fc.TakeCollectedEvents(ctx), e.SubManager.StateAPI) - } - - return nil, xerrors.Errorf("wrong filter type") -} - -// parseBlockRange is similar to actor event's parseHeightRange but with slightly different semantics -// -// * "block" instead of "height" -// * strings that can have "latest" and "earliest" and nil -// * hex strings for actual heights -func parseBlockRange(heaviest abi.ChainEpoch, fromBlock, toBlock *string, maxRange abi.ChainEpoch) (minHeight abi.ChainEpoch, maxHeight abi.ChainEpoch, err error) { - if fromBlock == nil || *fromBlock == "latest" || len(*fromBlock) == 0 { - minHeight = heaviest - } else if *fromBlock == "earliest" { - minHeight = 0 - } else { - if !strings.HasPrefix(*fromBlock, "0x") { - return 0, 0, xerrors.Errorf("FromBlock is not a hex") - } - epoch, err := ethtypes.EthUint64FromHex(*fromBlock) - if err != nil { - return 0, 0, xerrors.Errorf("invalid epoch") - } - minHeight = abi.ChainEpoch(epoch) - } - - if toBlock == nil || *toBlock == "latest" || len(*toBlock) == 0 { - // here latest means the latest at the time - maxHeight = -1 - } else if *toBlock == "earliest" { - maxHeight = 0 - } else { - if !strings.HasPrefix(*toBlock, "0x") { - return 0, 0, xerrors.Errorf("ToBlock is not a hex") - } - epoch, err := ethtypes.EthUint64FromHex(*toBlock) - if err != nil { - return 0, 0, xerrors.Errorf("invalid epoch") - } - maxHeight = abi.ChainEpoch(epoch) - } - - // Validate height ranges are within limits set by node operator - if minHeight == -1 && maxHeight > 0 { - // Here the client is looking for events between the head and some future height - if maxHeight-heaviest > maxRange { - return 0, 0, xerrors.Errorf("invalid epoch range: to block is too far in the future (maximum: %d)", maxRange) - } - } else if minHeight >= 0 && maxHeight == -1 { - // Here the client is looking for events between some time in the past and the current head - if heaviest-minHeight > maxRange { - return 0, 0, xerrors.Errorf("invalid epoch range: from block is too far in the past (maximum: %d)", maxRange) - } - } else if minHeight >= 0 && maxHeight >= 0 { - if minHeight > maxHeight { - return 0, 0, xerrors.Errorf("invalid epoch range: to block (%d) must be after from block (%d)", minHeight, maxHeight) - } else if maxHeight-minHeight > maxRange { - return 0, 0, xerrors.Errorf("invalid epoch range: range between to and from blocks is too large (maximum: %d)", maxRange) - } - } - return minHeight, maxHeight, nil -} - -type parsedFilter struct { - minHeight abi.ChainEpoch - maxHeight abi.ChainEpoch - tipsetCid cid.Cid - addresses []address.Address - keys map[string][]types.ActorEventBlock -} - -func (e *EthEventHandler) parseEthFilterSpec(filterSpec *ethtypes.EthFilterSpec) (*parsedFilter, error) { - var ( - minHeight abi.ChainEpoch - maxHeight abi.ChainEpoch - tipsetCid cid.Cid - addresses []address.Address - keys = map[string][][]byte{} - ) - - if filterSpec.BlockHash != nil { - if filterSpec.FromBlock != nil || filterSpec.ToBlock != nil { - return nil, xerrors.Errorf("must not specify block hash and from/to block") - } - - tipsetCid = filterSpec.BlockHash.ToCid() - } else { - var err error - // Because of deferred execution, we need to subtract 1 from the heaviest tipset height for the "heaviest" parameter - minHeight, maxHeight, err = parseBlockRange(e.Chain.GetHeaviestTipSet().Height()-1, filterSpec.FromBlock, filterSpec.ToBlock, e.MaxFilterHeightRange) - if err != nil { - return nil, err - } - } - - // Convert all addresses to filecoin f4 addresses - for _, ea := range filterSpec.Address { - a, err := ea.ToFilecoinAddress() - if err != nil { - return nil, xerrors.Errorf("invalid address %x", ea) - } - addresses = append(addresses, a) - } - - keys, err := parseEthTopics(filterSpec.Topics) - if err != nil { - return nil, err - } - - return &parsedFilter{ - minHeight: minHeight, - maxHeight: maxHeight, - tipsetCid: tipsetCid, - addresses: addresses, - keys: keysToKeysWithCodec(keys), - }, nil -} - -func keysToKeysWithCodec(keys map[string][][]byte) map[string][]types.ActorEventBlock { - keysWithCodec := make(map[string][]types.ActorEventBlock) - for k, v := range keys { - for _, vv := range v { - keysWithCodec[k] = append(keysWithCodec[k], types.ActorEventBlock{ - Codec: uint64(multicodec.Raw), // FEVM smart contract events are always encoded with the `raw` Codec. - Value: vv, - }) - } - } - return keysWithCodec -} - -func (e *EthEventHandler) EthNewFilter(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) (ethtypes.EthFilterID, error) { - if e.FilterStore == nil || e.EventFilterManager == nil { - return ethtypes.EthFilterID{}, api.ErrNotSupported - } - - pf, err := e.parseEthFilterSpec(filterSpec) - if err != nil { - return ethtypes.EthFilterID{}, err - } - - f, err := e.EventFilterManager.Install(ctx, pf.minHeight, pf.maxHeight, pf.tipsetCid, pf.addresses, pf.keys) - if err != nil { - return ethtypes.EthFilterID{}, xerrors.Errorf("failed to install event filter: %w", err) - } - - if err := e.FilterStore.Add(ctx, f); err != nil { - // Could not record in store, attempt to delete filter to clean up - err2 := e.EventFilterManager.Remove(ctx, f.ID()) - if err2 != nil { - return ethtypes.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err) - } - - return ethtypes.EthFilterID{}, err - } - return ethtypes.EthFilterID(f.ID()), nil -} - -func (e *EthEventHandler) EthNewBlockFilter(ctx context.Context) (ethtypes.EthFilterID, error) { - if e.FilterStore == nil || e.TipSetFilterManager == nil { - return ethtypes.EthFilterID{}, api.ErrNotSupported - } - - f, err := e.TipSetFilterManager.Install(ctx) - if err != nil { - return ethtypes.EthFilterID{}, err - } - - if err := e.FilterStore.Add(ctx, f); err != nil { - // Could not record in store, attempt to delete filter to clean up - err2 := e.TipSetFilterManager.Remove(ctx, f.ID()) - if err2 != nil { - return ethtypes.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err) - } - - return ethtypes.EthFilterID{}, err - } - - return ethtypes.EthFilterID(f.ID()), nil -} - -func (e *EthEventHandler) EthNewPendingTransactionFilter(ctx context.Context) (ethtypes.EthFilterID, error) { - if e.FilterStore == nil || e.MemPoolFilterManager == nil { - return ethtypes.EthFilterID{}, api.ErrNotSupported - } - - f, err := e.MemPoolFilterManager.Install(ctx) - if err != nil { - return ethtypes.EthFilterID{}, err - } - - if err := e.FilterStore.Add(ctx, f); err != nil { - // Could not record in store, attempt to delete filter to clean up - err2 := e.MemPoolFilterManager.Remove(ctx, f.ID()) - if err2 != nil { - return ethtypes.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err) - } - - return ethtypes.EthFilterID{}, err - } - - return ethtypes.EthFilterID(f.ID()), nil -} - -func (e *EthEventHandler) EthUninstallFilter(ctx context.Context, id ethtypes.EthFilterID) (bool, error) { - if e.FilterStore == nil { - return false, api.ErrNotSupported - } - - f, err := e.FilterStore.Get(ctx, types.FilterID(id)) - if err != nil { - if errors.Is(err, filter.ErrFilterNotFound) { - return false, nil - } - return false, err - } - - if err := e.uninstallFilter(ctx, f); err != nil { - return false, err - } - - return true, nil -} - -func (e *EthEventHandler) uninstallFilter(ctx context.Context, f filter.Filter) error { - switch f.(type) { - case filter.EventFilter: - err := e.EventFilterManager.Remove(ctx, f.ID()) - if err != nil && !errors.Is(err, filter.ErrFilterNotFound) { - return err - } - case *filter.TipSetFilter: - err := e.TipSetFilterManager.Remove(ctx, f.ID()) - if err != nil && !errors.Is(err, filter.ErrFilterNotFound) { - return err - } - case *filter.MemPoolFilter: - err := e.MemPoolFilterManager.Remove(ctx, f.ID()) - if err != nil && !errors.Is(err, filter.ErrFilterNotFound) { - return err - } - default: - return xerrors.Errorf("unknown filter type") - } - - return e.FilterStore.Remove(ctx, f.ID()) -} - -const ( - EthSubscribeEventTypeHeads = "newHeads" - EthSubscribeEventTypeLogs = "logs" - EthSubscribeEventTypePendingTransactions = "newPendingTransactions" -) - -func (e *EthEventHandler) EthSubscribe(ctx context.Context, p jsonrpc.RawParams) (ethtypes.EthSubscriptionID, error) { - params, err := jsonrpc.DecodeParams[ethtypes.EthSubscribeParams](p) - if err != nil { - return ethtypes.EthSubscriptionID{}, xerrors.Errorf("decoding params: %w", err) - } - - if e.SubManager == nil { - return ethtypes.EthSubscriptionID{}, api.ErrNotSupported - } - - ethCb, ok := jsonrpc.ExtractReverseClient[api.EthSubscriberMethods](ctx) - if !ok { - return ethtypes.EthSubscriptionID{}, xerrors.Errorf("connection doesn't support callbacks") - } - - sub, err := e.SubManager.StartSubscription(e.SubscribtionCtx, ethCb.EthSubscription, e.uninstallFilter) - if err != nil { - return ethtypes.EthSubscriptionID{}, err - } - - switch params.EventType { - case EthSubscribeEventTypeHeads: - f, err := e.TipSetFilterManager.Install(ctx) - if err != nil { - // clean up any previous filters added and stop the sub - _, _ = e.EthUnsubscribe(ctx, sub.id) - return ethtypes.EthSubscriptionID{}, err - } - sub.addFilter(ctx, f) - - case EthSubscribeEventTypeLogs: - keys := map[string][][]byte{} - if params.Params != nil { - var err error - keys, err = parseEthTopics(params.Params.Topics) - if err != nil { - // clean up any previous filters added and stop the sub - _, _ = e.EthUnsubscribe(ctx, sub.id) - return ethtypes.EthSubscriptionID{}, err - } - } - - var addresses []address.Address - if params.Params != nil { - for _, ea := range params.Params.Address { - a, err := ea.ToFilecoinAddress() - if err != nil { - return ethtypes.EthSubscriptionID{}, xerrors.Errorf("invalid address %x", ea) - } - addresses = append(addresses, a) - } - } - - f, err := e.EventFilterManager.Install(ctx, -1, -1, cid.Undef, addresses, keysToKeysWithCodec(keys)) - if err != nil { - // clean up any previous filters added and stop the sub - _, _ = e.EthUnsubscribe(ctx, sub.id) - return ethtypes.EthSubscriptionID{}, err - } - sub.addFilter(ctx, f) - case EthSubscribeEventTypePendingTransactions: - f, err := e.MemPoolFilterManager.Install(ctx) - if err != nil { - // clean up any previous filters added and stop the sub - _, _ = e.EthUnsubscribe(ctx, sub.id) - return ethtypes.EthSubscriptionID{}, err - } - - sub.addFilter(ctx, f) - default: - return ethtypes.EthSubscriptionID{}, xerrors.Errorf("unsupported event type: %s", params.EventType) - } - - return sub.id, nil -} - -func (e *EthEventHandler) EthUnsubscribe(ctx context.Context, id ethtypes.EthSubscriptionID) (bool, error) { - if e.SubManager == nil { - return false, api.ErrNotSupported - } - - err := e.SubManager.StopSubscription(ctx, id) - if err != nil { - return false, nil - } - - return true, nil -} - -// GC runs a garbage collection loop, deleting filters that have not been used within the ttl window -func (e *EthEventHandler) GC(ctx context.Context, ttl time.Duration) { - if e.FilterStore == nil { - return - } - - tt := time.NewTicker(time.Minute * 30) - defer tt.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-tt.C: - fs := e.FilterStore.NotTakenSince(time.Now().Add(-ttl)) - for _, f := range fs { - if err := e.uninstallFilter(ctx, f); err != nil { - log.Warnf("Failed to remove actor event filter during garbage collection: %v", err) - } - } - } - } -} - -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) - } - - return rewards, gasUsedTotal -} - -type gasRewardTuple struct { - gasUsed int64 - premium abi.TokenAmount -} - -// sorted in ascending order -type gasRewardSorter []gasRewardTuple - -func (g gasRewardSorter) Len() int { return len(g) } -func (g gasRewardSorter) Swap(i, j int) { - g[i], g[j] = g[j], g[i] -} -func (g gasRewardSorter) Less(i, j int) bool { - return g[i].premium.Int.Cmp(g[j].premium.Int) == -1 + eth.EthFilecoinAPI + eth.EthBasicAPI + eth.EthTransactionAPI + eth.EthLookupAPI + eth.EthTraceAPI + eth.EthGasAPI + eth.EthEventsAPI + eth.EthSendAPI } diff --git a/node/impl/full/eth_test.go b/node/impl/full/eth_test.go deleted file mode 100644 index 6f9d8f297ee..00000000000 --- a/node/impl/full/eth_test.go +++ /dev/null @@ -1,295 +0,0 @@ -package full - -import ( - "bytes" - "encoding/hex" - "fmt" - "testing" - - "github.com/ipfs/go-cid" - "github.com/multiformats/go-multicodec" - "github.com/stretchr/testify/require" - cbg "github.com/whyrusleeping/cbor-gen" - - "github.com/filecoin-project/go-state-types/abi" - "github.com/filecoin-project/go-state-types/big" - - "github.com/filecoin-project/lotus/chain/types" - "github.com/filecoin-project/lotus/chain/types/ethtypes" -) - -func TestParseBlockRange(t *testing.T) { - pstring := func(s string) *string { return &s } - - tcs := map[string]struct { - heaviest abi.ChainEpoch - from *string - to *string - maxRange abi.ChainEpoch - minOut abi.ChainEpoch - maxOut abi.ChainEpoch - errStr string - }{ - "fails when both are specified and range is greater than max allowed range": { - heaviest: 100, - from: pstring("0x100"), - to: pstring("0x200"), - maxRange: 10, - minOut: 0, - maxOut: 0, - errStr: "too large", - }, - "fails when min is specified and range is greater than max allowed range": { - heaviest: 500, - from: pstring("0x10"), - to: pstring("latest"), - maxRange: 10, - minOut: 0, - maxOut: 0, - errStr: "too far in the past", - }, - "fails when max is specified and range is greater than max allowed range": { - heaviest: 500, - from: pstring("earliest"), - to: pstring("0x10000"), - maxRange: 10, - minOut: 0, - maxOut: 0, - errStr: "too large", - }, - "works when range is valid": { - heaviest: 500, - from: pstring("earliest"), - to: pstring("latest"), - maxRange: 1000, - minOut: 0, - maxOut: -1, - }, - "works when range is valid and specified": { - heaviest: 500, - from: pstring("0x10"), - to: pstring("0x30"), - maxRange: 1000, - minOut: 16, - maxOut: 48, - }, - } - - for name, tc := range tcs { - tc2 := tc - t.Run(name, func(t *testing.T) { - min, max, err := parseBlockRange(tc2.heaviest, tc2.from, tc2.to, tc2.maxRange) - require.Equal(t, tc2.minOut, min) - require.Equal(t, tc2.maxOut, max) - if tc2.errStr != "" { - fmt.Println(err) - require.Error(t, err) - require.Contains(t, err.Error(), tc2.errStr) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestEthLogFromEvent(t *testing.T) { - // basic empty - data, topics, ok := ethLogFromEvent(nil) - require.True(t, ok) - require.Nil(t, data) - require.Empty(t, topics) - require.NotNil(t, topics) - - // basic topic - data, topics, ok = ethLogFromEvent([]types.EventEntry{{ - Flags: 0, - Key: "t1", - Codec: cid.Raw, - Value: make([]byte, 32), - }}) - require.True(t, ok) - require.Nil(t, data) - require.Len(t, topics, 1) - require.Equal(t, topics[0], ethtypes.EthHash{}) - - // basic topic with data - data, topics, ok = ethLogFromEvent([]types.EventEntry{{ - Flags: 0, - Key: "t1", - Codec: cid.Raw, - Value: make([]byte, 32), - }, { - Flags: 0, - Key: "d", - Codec: cid.Raw, - Value: []byte{0x0}, - }}) - require.True(t, ok) - require.Equal(t, data, []byte{0x0}) - require.Len(t, topics, 1) - require.Equal(t, topics[0], ethtypes.EthHash{}) - - // skip topic - _, _, ok = ethLogFromEvent([]types.EventEntry{{ - Flags: 0, - Key: "t2", - Codec: cid.Raw, - Value: make([]byte, 32), - }}) - require.False(t, ok) - - // duplicate topic - _, _, ok = ethLogFromEvent([]types.EventEntry{{ - Flags: 0, - Key: "t1", - Codec: cid.Raw, - Value: make([]byte, 32), - }, { - Flags: 0, - Key: "t1", - Codec: cid.Raw, - Value: make([]byte, 32), - }}) - require.False(t, ok) - - // duplicate data - _, _, ok = ethLogFromEvent([]types.EventEntry{{ - Flags: 0, - Key: "d", - Codec: cid.Raw, - Value: make([]byte, 32), - }, { - Flags: 0, - Key: "d", - Codec: cid.Raw, - Value: make([]byte, 32), - }}) - require.False(t, ok) - - // unknown key is fine - data, topics, ok = ethLogFromEvent([]types.EventEntry{{ - Flags: 0, - Key: "t5", - Codec: cid.Raw, - Value: make([]byte, 32), - }, { - Flags: 0, - Key: "t1", - Codec: cid.Raw, - Value: make([]byte, 32), - }}) - require.True(t, ok) - require.Nil(t, data) - require.Len(t, topics, 1) - require.Equal(t, topics[0], ethtypes.EthHash{}) -} - -func TestReward(t *testing.T) { - baseFee := big.NewInt(100) - testcases := []struct { - maxFeePerGas, maxPriorityFeePerGas big.Int - answer big.Int - }{ - {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(200), answer: big.NewInt(200)}, - {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(300), answer: big.NewInt(300)}, - {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(500), answer: big.NewInt(500)}, - {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(600), answer: big.NewInt(500)}, - {maxFeePerGas: big.NewInt(600), maxPriorityFeePerGas: big.NewInt(1000), answer: big.NewInt(500)}, - {maxFeePerGas: big.NewInt(50), maxPriorityFeePerGas: big.NewInt(200), answer: big.NewInt(0)}, - } - for _, tc := range testcases { - msg := &types.Message{GasFeeCap: tc.maxFeePerGas, GasPremium: tc.maxPriorityFeePerGas} - reward := msg.EffectiveGasPremium(baseFee) - require.Equal(t, 0, reward.Int.Cmp(tc.answer.Int), reward, tc.answer) - } -} - -func TestRewardPercentiles(t *testing.T) { - testcases := []struct { - percentiles []float64 - txGasRewards gasRewardSorter - answer []int64 - }{ - { - percentiles: []float64{25, 50, 75}, - txGasRewards: []gasRewardTuple{}, - answer: []int64{MinGasPremium, MinGasPremium, MinGasPremium}, - }, - { - percentiles: []float64{25, 50, 75, 100}, - txGasRewards: []gasRewardTuple{ - {gasUsed: int64(0), premium: big.NewInt(300)}, - {gasUsed: int64(100), premium: big.NewInt(200)}, - {gasUsed: int64(350), premium: big.NewInt(100)}, - {gasUsed: int64(500), premium: big.NewInt(600)}, - {gasUsed: int64(300), premium: big.NewInt(700)}, - }, - answer: []int64{200, 700, 700, 700}, - }, - } - for _, tc := range testcases { - rewards, totalGasUsed := calculateRewardsAndGasUsed(tc.percentiles, tc.txGasRewards) - var gasUsed int64 - for _, tx := range tc.txGasRewards { - gasUsed += tx.gasUsed - } - ans := []ethtypes.EthBigInt{} - for _, bi := range tc.answer { - ans = append(ans, ethtypes.EthBigInt(big.NewInt(bi))) - } - require.Equal(t, totalGasUsed, gasUsed) - require.Equal(t, len(ans), len(tc.percentiles)) - require.Equal(t, ans, rewards) - } -} - -func TestABIEncoding(t *testing.T) { - // Generated from https://abi.hashex.org/ - const expected = "000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001b1111111111111111111020200301000000044444444444444444010000000000" - const data = "111111111111111111102020030100000004444444444444444401" - - expectedBytes, err := hex.DecodeString(expected) - require.NoError(t, err) - - dataBytes, err := hex.DecodeString(data) - require.NoError(t, err) - - require.Equal(t, expectedBytes, encodeAsABIHelper(22, 81, dataBytes)) -} - -func TestDecodePayload(t *testing.T) { - // "empty" - b, err := decodePayload(nil, 0) - require.NoError(t, err) - require.Empty(t, b) - - // raw empty - _, err = decodePayload(nil, uint64(multicodec.Raw)) - require.NoError(t, err) - require.Empty(t, b) - - // raw non-empty - b, err = decodePayload([]byte{1}, uint64(multicodec.Raw)) - require.NoError(t, err) - require.EqualValues(t, b, []byte{1}) - - // Invalid cbor bytes - _, err = decodePayload(nil, uint64(multicodec.DagCbor)) - require.Error(t, err) - - // valid cbor bytes - var w bytes.Buffer - require.NoError(t, cbg.WriteByteArray(&w, []byte{1})) - b, err = decodePayload(w.Bytes(), uint64(multicodec.DagCbor)) - require.NoError(t, err) - require.EqualValues(t, b, []byte{1}) - - // regular cbor also works. - b, err = decodePayload(w.Bytes(), uint64(multicodec.Cbor)) - require.NoError(t, err) - require.EqualValues(t, b, []byte{1}) - - // random codec should fail - _, err = decodePayload(w.Bytes(), 42) - require.Error(t, err) -} diff --git a/node/impl/full/gas.go b/node/impl/full/gas.go index dd335da9ddf..addff4df1c3 100644 --- a/node/impl/full/gas.go +++ b/node/impl/full/gas.go @@ -2,28 +2,19 @@ package full import ( "context" - "math" - "math/rand" - "os" - "sort" - lru "github.com/hashicorp/golang-lru/v2" "go.uber.org/fx" "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" - "github.com/filecoin-project/go-state-types/builtin" - "github.com/filecoin-project/go-state-types/exitcode" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build/buildconstants" - lbuiltin "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/messagepool" "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/node/impl/gasutils" "github.com/filecoin-project/lotus/node/modules/dtypes" ) @@ -43,7 +34,7 @@ type GasModule struct { Mpool *messagepool.MessagePool GetMaxFee dtypes.DefaultMaxFeeFunc - PriceCache *GasPriceCache + PriceCache *gasutils.GasPriceCache } var _ GasModuleAPI = (*GasModule)(nil) @@ -57,113 +48,25 @@ type GasAPI struct { Chain *store.ChainStore Mpool *messagepool.MessagePool - PriceCache *GasPriceCache + PriceCache *gasutils.GasPriceCache } -func NewGasPriceCache() *GasPriceCache { - // 50 because we usually won't access more than 40 - c, err := lru.New2Q[types.TipSetKey, []GasMeta](50) - if err != nil { - // err only if parameter is bad - panic(err) - } - - return &GasPriceCache{ - c: c, - } -} - -type GasPriceCache struct { - c *lru.TwoQueueCache[types.TipSetKey, []GasMeta] -} - -type GasMeta struct { - Price big.Int - Limit int64 -} - -func (g *GasPriceCache) GetTSGasStats(ctx context.Context, cstore *store.ChainStore, ts *types.TipSet) ([]GasMeta, error) { - i, has := g.c.Get(ts.Key()) - if has { - return i, nil - } - - var prices []GasMeta - msgs, err := cstore.MessagesForTipset(ctx, ts) - if err != nil { - return nil, xerrors.Errorf("loading messages: %w", err) - } - for _, msg := range msgs { - prices = append(prices, GasMeta{ - Price: msg.VMMessage().GasPremium, - Limit: msg.VMMessage().GasLimit, - }) - } - - g.c.Add(ts.Key(), prices) - - return prices, nil -} - -const MinGasPremium = 100e3 -const MaxSpendOnFeeDenom = 100 - func (a *GasAPI) GasEstimateFeeCap( ctx context.Context, msg *types.Message, maxqueueblks int64, tsk types.TipSetKey, ) (types.BigInt, error) { - return gasEstimateFeeCap(a.Chain, msg, maxqueueblks) + return gasutils.GasEstimateFeeCap(a.Chain, msg, maxqueueblks) } + func (m *GasModule) GasEstimateFeeCap( ctx context.Context, msg *types.Message, maxqueueblks int64, tsk types.TipSetKey, ) (types.BigInt, error) { - return gasEstimateFeeCap(m.Chain, msg, maxqueueblks) -} -func gasEstimateFeeCap(cstore *store.ChainStore, msg *types.Message, maxqueueblks int64) (types.BigInt, error) { - ts := cstore.GetHeaviestTipSet() - - parentBaseFee := ts.Blocks()[0].ParentBaseFee - increaseFactor := math.Pow(1.+1./float64(buildconstants.BaseFeeMaxChangeDenom), float64(maxqueueblks)) - - feeInFuture := types.BigMul(parentBaseFee, types.NewInt(uint64(increaseFactor*(1<<8)))) - out := types.BigDiv(feeInFuture, types.NewInt(1<<8)) - - if msg.GasPremium != types.EmptyInt { - out = types.BigAdd(out, msg.GasPremium) - } - - return out, nil -} - -// finds 55th percntile instead of median to put negative pressure on gas price -func medianGasPremium(prices []GasMeta, blocks int) abi.TokenAmount { - sort.Slice(prices, func(i, j int) bool { - // sort desc by price - return prices[i].Price.GreaterThan(prices[j].Price) - }) - - at := buildconstants.BlockGasTarget * int64(blocks) / 2 // 50th - at += buildconstants.BlockGasTarget * int64(blocks) / (2 * 20) // move 5% further - prev1, prev2 := big.Zero(), big.Zero() - for _, price := range prices { - prev1, prev2 = price.Price, prev1 - at -= price.Limit - if at < 0 { - break - } - } - - premium := prev1 - if prev2.Sign() != 0 { - premium = big.Div(types.BigAdd(prev1, prev2), types.NewInt(2)) - } - - return premium + return gasutils.GasEstimateFeeCap(m.Chain, msg, maxqueueblks) } func (a *GasAPI) GasEstimateGasPremium( @@ -173,8 +76,9 @@ func (a *GasAPI) GasEstimateGasPremium( gaslimit int64, _ types.TipSetKey, ) (types.BigInt, error) { - return gasEstimateGasPremium(ctx, a.Chain, a.PriceCache, nblocksincl) + return gasutils.GasEstimateGasPremium(ctx, a.Chain, a.PriceCache, nblocksincl) } + func (m *GasModule) GasEstimateGasPremium( ctx context.Context, nblocksincl uint64, @@ -182,57 +86,7 @@ func (m *GasModule) GasEstimateGasPremium( gaslimit int64, _ types.TipSetKey, ) (types.BigInt, error) { - return gasEstimateGasPremium(ctx, m.Chain, m.PriceCache, nblocksincl) -} -func gasEstimateGasPremium(ctx context.Context, cstore *store.ChainStore, cache *GasPriceCache, nblocksincl uint64) (types.BigInt, error) { - if nblocksincl == 0 { - nblocksincl = 1 - } - - var prices []GasMeta - var blocks int - - ts := cstore.GetHeaviestTipSet() - for i := uint64(0); i < nblocksincl*2; i++ { - if ts.Height() == 0 { - break // genesis - } - - pts, err := cstore.LoadTipSet(ctx, ts.Parents()) - if err != nil { - return types.BigInt{}, err - } - - blocks += len(pts.Blocks()) - meta, err := cache.GetTSGasStats(ctx, cstore, pts) - if err != nil { - return types.BigInt{}, err - } - prices = append(prices, meta...) - - ts = pts - } - - premium := medianGasPremium(prices, blocks) - - if types.BigCmp(premium, types.NewInt(MinGasPremium)) < 0 { - switch nblocksincl { - case 1: - premium = types.NewInt(2 * MinGasPremium) - case 2: - premium = types.NewInt(1.5 * MinGasPremium) - default: - premium = types.NewInt(MinGasPremium) - } - } - - // add some noise to normalize behaviour of message selection - const precision = 32 - // mean 1, stddev 0.005 => 95% within +-1% - noise := 1 + rand.NormFloat64()*0.005 - premium = types.BigMul(premium, types.NewInt(uint64(noise*(1<> 10 - - return ret, nil + return gasutils.GasEstimateGasLimit(ctx, m.Chain, m.Stmgr, m.Mpool, msgIn, ts) } func (m *GasModule) GasEstimateMessageGas(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec, _ types.TipSetKey) (*types.Message, error) { diff --git a/node/impl/gasutils/gasutils.go b/node/impl/gasutils/gasutils.go new file mode 100644 index 00000000000..3200ec224d5 --- /dev/null +++ b/node/impl/gasutils/gasutils.go @@ -0,0 +1,318 @@ +package gasutils + +import ( + "context" + "math" + "math/rand/v2" + "os" + "sort" + + lru "github.com/hashicorp/golang-lru/v2" + logging "github.com/ipfs/go-log/v2" + "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" + "github.com/filecoin-project/go-state-types/builtin" + "github.com/filecoin-project/go-state-types/exitcode" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/build/buildconstants" + lbuiltin "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/messagepool" + "github.com/filecoin-project/lotus/chain/state" + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/types" +) + +var log = logging.Logger("node/gasutils") + +const MinGasPremium = 100e3 +const MaxSpendOnFeeDenom = 100 + +type StateManagerAPI interface { + ParentState(ts *types.TipSet) (*state.StateTree, error) + CallWithGas(ctx context.Context, msg *types.Message, priorMsgs []types.ChainMsg, ts *types.TipSet, applyTsMessages bool) (*api.InvocResult, error) + ResolveToDeterministicAddress(ctx context.Context, addr address.Address, ts *types.TipSet) (address.Address, error) +} + +type ChainStoreAPI interface { + GetHeaviestTipSet() *types.TipSet + GetTipSetFromKey(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) + LoadTipSet(ctx context.Context, tsk types.TipSetKey) (*types.TipSet, error) + MessagesForTipset(ctx context.Context, ts *types.TipSet) ([]types.ChainMsg, error) +} + +type MessagePoolAPI interface { + PendingFor(ctx context.Context, a address.Address) ([]*types.SignedMessage, *types.TipSet) +} + +type GasMeta struct { + Price big.Int + Limit int64 +} + +type GasPriceCache struct { + c *lru.TwoQueueCache[types.TipSetKey, []GasMeta] +} + +func NewGasPriceCache() *GasPriceCache { + // 50 because we usually won't access more than 40 + c, err := lru.New2Q[types.TipSetKey, []GasMeta](50) + if err != nil { + // err only if parameter is bad + panic(err) + } + + return &GasPriceCache{ + c: c, + } +} + +func (g *GasPriceCache) GetTSGasStats(ctx context.Context, cstore ChainStoreAPI, ts *types.TipSet) ([]GasMeta, error) { + i, has := g.c.Get(ts.Key()) + if has { + return i, nil + } + + var prices []GasMeta + msgs, err := cstore.MessagesForTipset(ctx, ts) + if err != nil { + return nil, xerrors.Errorf("loading messages: %w", err) + } + for _, msg := range msgs { + prices = append(prices, GasMeta{ + Price: msg.VMMessage().GasPremium, + Limit: msg.VMMessage().GasLimit, + }) + } + + g.c.Add(ts.Key(), prices) + + return prices, nil +} + +// GasEstimateCallWithGas invokes a message "msgIn" on the earliest available tipset with pending +// messages in the message pool. The function returns the result of the message invocation, the +// pending messages, the tipset used for the invocation, and an error if occurred. +// The returned information can be used to make subsequent calls to CallWithGas with the same parameters. +func GasEstimateCallWithGas( + ctx context.Context, + cstore ChainStoreAPI, + smgr StateManagerAPI, + mpool MessagePoolAPI, + msgIn *types.Message, + currTs *types.TipSet, +) (*api.InvocResult, []types.ChainMsg, *types.TipSet, error) { + msg := *msgIn + fromA, err := smgr.ResolveToDeterministicAddress(ctx, msgIn.From, currTs) + if err != nil { + return nil, []types.ChainMsg{}, nil, xerrors.Errorf("getting key address: %w", err) + } + + pending, ts := mpool.PendingFor(ctx, fromA) + priorMsgs := make([]types.ChainMsg, 0, len(pending)) + for _, m := range pending { + if m.Message.Nonce == msg.Nonce { + break + } + priorMsgs = append(priorMsgs, m) + } + + applyTsMessages := true + if os.Getenv("LOTUS_SKIP_APPLY_TS_MESSAGE_CALL_WITH_GAS") == "1" { + applyTsMessages = false + } + + // Try calling until we find a height with no migration. + var res *api.InvocResult + for { + res, err = smgr.CallWithGas(ctx, &msg, priorMsgs, ts, applyTsMessages) + if err != stmgr.ErrExpensiveFork { + break + } + ts, err = cstore.GetTipSetFromKey(ctx, ts.Parents()) + if err != nil { + return nil, []types.ChainMsg{}, nil, xerrors.Errorf("getting parent tipset: %w", err) + } + } + if err != nil { + return nil, []types.ChainMsg{}, nil, xerrors.Errorf("CallWithGas failed: %w", err) + } + + return res, priorMsgs, ts, nil +} + +func GasEstimateGasLimit( + ctx context.Context, + cstore ChainStoreAPI, + smgr StateManagerAPI, + mpool *messagepool.MessagePool, + msgIn *types.Message, + currTs *types.TipSet, +) (int64, error) { + msg := *msgIn + msg.GasLimit = buildconstants.BlockGasLimit + msg.GasFeeCap = big.Zero() + msg.GasPremium = big.Zero() + + res, _, ts, err := GasEstimateCallWithGas(ctx, cstore, smgr, mpool, &msg, currTs) + if err != nil { + return -1, xerrors.Errorf("gas estimation failed: %w", err) + } + + if res.MsgRct.ExitCode == exitcode.SysErrOutOfGas { + return -1, &api.ErrOutOfGas{} + } + + if res.MsgRct.ExitCode != exitcode.Ok { + return -1, xerrors.Errorf("message execution failed: exit %s, reason: %s", res.MsgRct.ExitCode, res.Error) + } + + ret := res.MsgRct.GasUsed + + log.Debugw("GasEstimateMessageGas CallWithGas Result", "GasUsed", ret, "ExitCode", res.MsgRct.ExitCode) + + transitionalMulti := 1.0 + // Overestimate gas around the upgrade + if ts.Height() <= buildconstants.UpgradeHyggeHeight && (buildconstants.UpgradeHyggeHeight-ts.Height() <= 20) { + func() { + + // Bare transfers get about 3x more expensive: https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0057.md#product-considerations + if msgIn.Method == builtin.MethodSend { + transitionalMulti = 3.0 + return + } + + st, err := smgr.ParentState(ts) + if err != nil { + return + } + act, err := st.GetActor(msg.To) + if err != nil { + return + } + + if lbuiltin.IsStorageMinerActor(act.Code) { + switch msgIn.Method { + case 3: + transitionalMulti = 1.92 + case 4: + transitionalMulti = 1.72 + case 6: + transitionalMulti = 1.06 + case 7: + transitionalMulti = 1.2 + case 16: + transitionalMulti = 1.19 + case 18: + transitionalMulti = 1.73 + case 23: + transitionalMulti = 1.73 + case 26: + transitionalMulti = 1.15 + case 27: + transitionalMulti = 1.18 + default: + } + } + }() + } + ret = (ret * int64(transitionalMulti*1024)) >> 10 + + return ret, nil +} + +func GasEstimateFeeCap(cstore ChainStoreAPI, msg *types.Message, maxqueueblks int64) (types.BigInt, error) { + ts := cstore.GetHeaviestTipSet() + + parentBaseFee := ts.Blocks()[0].ParentBaseFee + increaseFactor := math.Pow(1.+1./float64(buildconstants.BaseFeeMaxChangeDenom), float64(maxqueueblks)) + + feeInFuture := types.BigMul(parentBaseFee, types.NewInt(uint64(increaseFactor*(1<<8)))) + out := types.BigDiv(feeInFuture, types.NewInt(1<<8)) + + if msg.GasPremium != types.EmptyInt { + out = types.BigAdd(out, msg.GasPremium) + } + + return out, nil +} + +func GasEstimateGasPremium(ctx context.Context, cstore ChainStoreAPI, cache *GasPriceCache, nblocksincl uint64) (types.BigInt, error) { + if nblocksincl == 0 { + nblocksincl = 1 + } + + var prices []GasMeta + var blocks int + + ts := cstore.GetHeaviestTipSet() + for i := uint64(0); i < nblocksincl*2; i++ { + if ts.Height() == 0 { + break // genesis + } + + pts, err := cstore.LoadTipSet(ctx, ts.Parents()) + if err != nil { + return types.BigInt{}, err + } + + blocks += len(pts.Blocks()) + meta, err := cache.GetTSGasStats(ctx, cstore, pts) + if err != nil { + return types.BigInt{}, err + } + prices = append(prices, meta...) + + ts = pts + } + + premium := medianGasPremium(prices, blocks) + + if types.BigCmp(premium, types.NewInt(MinGasPremium)) < 0 { + switch nblocksincl { + case 1: + premium = types.NewInt(2 * MinGasPremium) + case 2: + premium = types.NewInt(1.5 * MinGasPremium) + default: + premium = types.NewInt(MinGasPremium) + } + } + + // add some noise to normalize behaviour of message selection + const precision = 32 + // mean 1, stddev 0.005 => 95% within +-1% + noise := 1 + rand.NormFloat64()*0.005 + premium = types.BigMul(premium, types.NewInt(uint64(noise*(1< 0 { - blkCache, err = arc.NewARC[cid.Cid, *ethtypes.EthBlock](cfg.EthBlkCacheSize) - if err != nil { - return nil, xerrors.Errorf("failed to create block cache: %w", err) - } - - blkTxCache, err = arc.NewARC[cid.Cid, *ethtypes.EthBlock](cfg.EthBlkCacheSize) - if err != nil { - return nil, xerrors.Errorf("failed to create block transaction cache: %w", err) - } - } - - return &full.EthModule{ - Chain: cs, - Mpool: mp, - StateManager: sm, - - ChainAPI: chainapi, - MpoolAPI: mpoolapi, - StateAPI: stateapi, - SyncAPI: syncapi, - EthEventHandler: ethEventHandler, - - EthTraceFilterMaxResults: cfg.EthTraceFilterMaxResults, - - EthBlkCache: blkCache, - EthBlkTxCache: blkTxCache, - - ChainIndexer: chainIndexer, - }, nil - } -}