diff --git a/CHANGELOG.md b/CHANGELOG.md index a9582a28257..b3a4547fb3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - fix(gas): stricter bounds for GasEstimateGasPremium lookback ([filecoin-project/lotus#13555](https://github.com/filecoin-project/lotus/pull/13555)) - fix(api): `StateSearchMsg` should respect `lookbackLimit` [filecoin-project/lotus#13562](https://github.com/filecoin-project/lotus/pull/13562) - fix(ecfinality): account for null rounds in EC finality calculator chain walk, aligning with FRC-0089 theoretical model and fixing depth-to-height conversion ([filecoin-project/lotus#13565](https://github.com/filecoin-project/lotus/pull/13565)) +- fix(eth): tighten block range handling for `trace_filter` and `eth_getLogs`, including consistent `-32005` limit-exceeded errors and gateway range enforcement for `trace_filter` ([filecoin-project/lotus#13561](https://github.com/filecoin-project/lotus/pull/13561)) # Node and Miner v1.35.0 / 2026-02-19 diff --git a/api/api_errors.go b/api/api_errors.go index 480518ff099..810023a1f31 100644 --- a/api/api_errors.go +++ b/api/api_errors.go @@ -32,6 +32,10 @@ const ( EPaymentChannelDisabled ) +// ELimitExceeded is the standard Ethereum JSON-RPC error code for request limit +// violations (EIP-1474). Used when a block range exceeds the configured maximum. +const ELimitExceeded jsonrpc.ErrorCode = -32005 + var ( RPCErrors = jsonrpc.NewErrors() @@ -67,6 +71,8 @@ var ( _ error = (*ErrNullRound)(nil) _ jsonrpc.RPCErrorCodec = (*ErrNullRound)(nil) _ error = (*errPaymentChannelDisabled)(nil) + _ error = (*ErrBlockRangeExceeded)(nil) + _ jsonrpc.RPCErrorCodec = (*ErrBlockRangeExceeded)(nil) ) func init() { @@ -82,6 +88,7 @@ func init() { RPCErrors.Register(EExecutionReverted, new(*ErrExecutionReverted)) RPCErrors.Register(ENullRound, new(*ErrNullRound)) RPCErrors.Register(EPaymentChannelDisabled, new(*errPaymentChannelDisabled)) + RPCErrors.Register(ELimitExceeded, new(*ErrBlockRangeExceeded)) } func ErrorIsIn(err error, errorTypes []error) bool { @@ -246,3 +253,40 @@ type errPaymentChannelDisabled struct{} func (errPaymentChannelDisabled) Error() string { return "payment channels disabled (EnablePaymentChannelManager=false)" } + +// ErrBlockRangeExceeded signals that a request's block range exceeds the configured +// maximum. Returned with the standard Ethereum JSON-RPC -32005 "limit exceeded" code. +type ErrBlockRangeExceeded struct { + Message string +} + +func NewErrBlockRangeExceeded(maxBlockRange, given uint64) *ErrBlockRangeExceeded { + return &ErrBlockRangeExceeded{ + Message: fmt.Sprintf("block range exceeds maximum of %d (got %d)", maxBlockRange, given), + } +} + +func (e *ErrBlockRangeExceeded) Error() string { + return e.Message +} + +func (e *ErrBlockRangeExceeded) FromJSONRPCError(jerr jsonrpc.JSONRPCError) error { + if jerr.Code != ELimitExceeded { + return fmt.Errorf("unexpected error code: %d", jerr.Code) + } + e.Message = jerr.Message + return nil +} + +func (e *ErrBlockRangeExceeded) ToJSONRPCError() (jsonrpc.JSONRPCError, error) { + return jsonrpc.JSONRPCError{ + Code: ELimitExceeded, + Message: e.Error(), + }, nil +} + +// Is performs a non-strict type check so errors.Is works regardless of field values. +func (e *ErrBlockRangeExceeded) Is(target error) bool { + _, ok := target.(*ErrBlockRangeExceeded) + return ok +} diff --git a/cmd/lotus-gateway/main.go b/cmd/lotus-gateway/main.go index e897f563c45..d1e6c01de18 100644 --- a/cmd/lotus-gateway/main.go +++ b/cmd/lotus-gateway/main.go @@ -162,6 +162,11 @@ var runCmd = &cli.Command{ Usage: "The maximum number of filters plus subscriptions that a single websocket connection can maintain", Value: gateway.DefaultEthMaxFiltersPerConn, }, + &cli.Int64Flag{ + Name: "eth-trace-filter-max-block-range", + Usage: "Maximum block range allowed for expensive trace_filter requests (0 = no limit)", + Value: gateway.DefaultEthTraceFilterMaxBlockRange, + }, &cli.BoolFlag{ Name: "cors", Usage: "Enable CORS headers to allow cross-origin requests from web browsers", @@ -206,6 +211,7 @@ var runCmd = &cli.Command{ rateLimitTimeout = cctx.Duration("rate-limit-timeout") perHostConnectionsPerMinute = cctx.Int("conn-per-minute") maxFiltersPerConn = cctx.Int("eth-max-filters-per-conn") + traceFilterMaxBlockRange = cctx.Int64("eth-trace-filter-max-block-range") enableCORS = cctx.Bool("cors") enableRequestLogging = cctx.Bool("request-logging") ) @@ -236,6 +242,7 @@ var runCmd = &cli.Command{ gateway.WithRateLimit(globalRateLimit), gateway.WithRateLimitTimeout(rateLimitTimeout), gateway.WithEthMaxFiltersPerConn(maxFiltersPerConn), + gateway.WithEthTraceFilterMaxBlockRange(traceFilterMaxBlockRange), ) handler, err := gateway.Handler( gwapi, @@ -246,7 +253,7 @@ var runCmd = &cli.Command{ gateway.WithRequestLogging(enableRequestLogging), ) if err != nil { - return xerrors.Errorf("failed to set up gateway HTTP handler") + return xerrors.Errorf("failed to set up gateway HTTP handler: %w", err) } stopFunc, err := node.ServeRPC(handler, "lotus-gateway", maddr) diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index 473375588f6..900b23a230d 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -288,7 +288,7 @@ #MaxFilterResults = 10000 # MaxFilterHeightRange specifies the maximum range of heights that can be used in a filter (to avoid querying - # the entire chain) + # the entire chain). Applies to eth_getLogs and trace_filter block range limits. # # type: uint64 # env var: LOTUS_EVENTS_MAXFILTERHEIGHTRANGE diff --git a/gateway/eth_utils.go b/gateway/eth_utils.go new file mode 100644 index 00000000000..378100c1faa --- /dev/null +++ b/gateway/eth_utils.go @@ -0,0 +1,74 @@ +package gateway + +// Block tag resolution utilities for gateway-level range checking. +// +// The "finalized" tag is not resolved here because its true height depends on +// F3 consensus and/or the EC probability calculator, which cannot be determined +// from head height alone. A static estimate like head-ChainFinality would +// undercount the range when "finalized" is a toBlock (actual finalized height +// is typically much closer to head). If trace_filter gains "finalized" support, +// the gateway will need to query the node for the real finalized height to +// perform accurate range checks. + +import ( + "github.com/filecoin-project/go-state-types/abi" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/types/ethtypes" +) + +// checkEthTraceFilterBlockRange checks whether the block range in the given +// trace filter criteria exceeds the gateway's configured maximum. +func (gw *Node) checkEthTraceFilterBlockRange(headHeight abi.ChainEpoch, filter ethtypes.EthTraceFilterCriteria) error { + if gw.ethTraceFilterMaxBlockRange <= 0 { + return nil + } + fromBlk := ethtypes.BlockTagLatest + if filter.FromBlock != nil { + fromBlk = *filter.FromBlock + } + toBlk := ethtypes.BlockTagLatest + if filter.ToBlock != nil { + toBlk = *filter.ToBlock + } + // Default for omitted fromBlock/toBlock is "latest", matching trace_filter + // and eth_getLogs semantics (OpenEthereum/Erigon trace_filter spec). + from, fromOk := resolveTraceFilterBlockTag(fromBlk, headHeight) + to, toOk := resolveTraceFilterBlockTag(toBlk, headHeight) + // If either tag couldn't be resolved (e.g. "finalized"), skip the range + // check and let the node handle validation. If from >= to the node's + // iteration is a no-op. + maxRange := uint64(gw.ethTraceFilterMaxBlockRange) + if fromOk && toOk && to > from && uint64(to-from) > maxRange { + return api.NewErrBlockRangeExceeded(maxRange, uint64(to-from)) + } + return nil +} + +// resolveTraceFilterBlockTag resolves the block tags supported by trace_filter +// to a numeric height. Returns (0, false) for unsupported or unparseable tags. +func resolveTraceFilterBlockTag(tag string, headHeight abi.ChainEpoch) (ethtypes.EthUint64, bool) { + switch tag { + case ethtypes.BlockTagPending: + return ethtypes.EthUint64(headHeight), true + case ethtypes.BlockTagLatest: + if headHeight > 0 { + return ethtypes.EthUint64(headHeight - 1), true + } + return 0, true + case ethtypes.BlockTagSafe: + // Matches trace.go's getEthBlockNumberFromString which uses (head-1)-SafeEpochDelay. + // Note: the authoritative TipSetResolver uses head-SafeEpochDelay (no -1); if this + // function is reused beyond trace_filter, revisit this. + if headHeight > ethtypes.SafeEpochDelay+1 { + return ethtypes.EthUint64(headHeight - 1 - ethtypes.SafeEpochDelay), true + } + return 0, true + default: + var num ethtypes.EthUint64 + if err := num.UnmarshalJSON([]byte(`"` + tag + `"`)); err != nil { + return 0, false + } + return num, true + } +} diff --git a/gateway/node.go b/gateway/node.go index ac9635f5468..f978b512379 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -25,10 +25,11 @@ import ( var log = logger.Logger("gateway") const ( - DefaultMaxLookbackDuration = time.Hour * 24 // Default duration that a gateway request can look back in chain history - DefaultMaxMessageLookbackEpochs = abi.ChainEpoch(20) // Default number of epochs that a gateway message lookup can look back in chain history - DefaultRateLimitTimeout = time.Second * 5 // Default timeout for rate limiting requests; where a request would take longer to wait than this value, it will be rejected - DefaultEthMaxFiltersPerConn = 16 // Default maximum number of ETH filters and subscriptions per websocket connection + DefaultMaxLookbackDuration = time.Hour * 24 // Default duration that a gateway request can look back in chain history + DefaultMaxMessageLookbackEpochs = abi.ChainEpoch(20) // Default number of epochs that a gateway message lookup can look back in chain history + DefaultRateLimitTimeout = time.Second * 5 // Default timeout for rate limiting requests; where a request would take longer to wait than this value, it will be rejected + DefaultEthMaxFiltersPerConn = 16 // Default maximum number of ETH filters and subscriptions per websocket connection + DefaultEthTraceFilterMaxBlockRange = int64(100) // Default maximum block range for EthTraceFilter on the gateway basicRateLimitTokens = 1 walletRateLimitTokens = 1 @@ -39,24 +40,26 @@ const ( ) type Node struct { - v1Proxy *reverseProxyV1 - v2Proxy *reverseProxyV2 - maxLookbackDuration time.Duration - maxMessageLookbackEpochs abi.ChainEpoch - rateLimiter *rate.Limiter - rateLimitTimeout time.Duration - ethMaxFiltersPerConn int - errLookback error + v1Proxy *reverseProxyV1 + v2Proxy *reverseProxyV2 + maxLookbackDuration time.Duration + maxMessageLookbackEpochs abi.ChainEpoch + rateLimiter *rate.Limiter + rateLimitTimeout time.Duration + ethMaxFiltersPerConn int + ethTraceFilterMaxBlockRange int64 + errLookback error } type options struct { - v1SubHandler *EthSubHandler - v2SubHandler *EthSubHandler - maxLookbackDuration time.Duration - maxMessageLookbackEpochs abi.ChainEpoch - rateLimit int - rateLimitTimeout time.Duration - ethMaxFiltersPerConn int + v1SubHandler *EthSubHandler + v2SubHandler *EthSubHandler + maxLookbackDuration time.Duration + maxMessageLookbackEpochs abi.ChainEpoch + rateLimit int + rateLimitTimeout time.Duration + ethMaxFiltersPerConn int + ethTraceFilterMaxBlockRange int64 } type Option func(*options) @@ -115,13 +118,23 @@ func WithEthMaxFiltersPerConn(ethMaxFiltersPerConn int) Option { } } +// WithEthTraceFilterMaxBlockRange sets the maximum block range allowed for EthTraceFilter +// requests on the gateway. This is typically tighter than the node-level MaxFilterHeightRange +// because trace replay is significantly more expensive than log lookups. +func WithEthTraceFilterMaxBlockRange(maxBlockRange int64) Option { + return func(opts *options) { + opts.ethTraceFilterMaxBlockRange = maxBlockRange + } +} + // NewNode creates a new gateway node. func NewNode(v1 v1api.FullNode, v2 v2api.FullNode, opts ...Option) *Node { options := &options{ - maxLookbackDuration: DefaultMaxLookbackDuration, - maxMessageLookbackEpochs: DefaultMaxMessageLookbackEpochs, - rateLimitTimeout: DefaultRateLimitTimeout, - ethMaxFiltersPerConn: DefaultEthMaxFiltersPerConn, + maxLookbackDuration: DefaultMaxLookbackDuration, + maxMessageLookbackEpochs: DefaultMaxMessageLookbackEpochs, + rateLimitTimeout: DefaultRateLimitTimeout, + ethMaxFiltersPerConn: DefaultEthMaxFiltersPerConn, + ethTraceFilterMaxBlockRange: DefaultEthTraceFilterMaxBlockRange, } for _, opt := range opts { opt(options) @@ -132,12 +145,13 @@ func NewNode(v1 v1api.FullNode, v2 v2api.FullNode, opts ...Option) *Node { limit = rate.Every(time.Second / time.Duration(options.rateLimit)) } gateway := &Node{ - maxLookbackDuration: options.maxLookbackDuration, - maxMessageLookbackEpochs: options.maxMessageLookbackEpochs, - rateLimiter: rate.NewLimiter(limit, MaxRateLimitTokens), // allow for a burst of MaxRateLimitTokens - rateLimitTimeout: options.rateLimitTimeout, - errLookback: fmt.Errorf("lookbacks of more than %s are disallowed", options.maxLookbackDuration), - ethMaxFiltersPerConn: options.ethMaxFiltersPerConn, + maxLookbackDuration: options.maxLookbackDuration, + maxMessageLookbackEpochs: options.maxMessageLookbackEpochs, + rateLimiter: rate.NewLimiter(limit, MaxRateLimitTokens), // allow for a burst of MaxRateLimitTokens + rateLimitTimeout: options.rateLimitTimeout, + errLookback: fmt.Errorf("lookbacks of more than %s are disallowed", options.maxLookbackDuration), + ethMaxFiltersPerConn: options.ethMaxFiltersPerConn, + ethTraceFilterMaxBlockRange: options.ethTraceFilterMaxBlockRange, } gateway.v1Proxy = &reverseProxyV1{ gateway: gateway, diff --git a/gateway/proxy_eth_v1.go b/gateway/proxy_eth_v1.go index 42d2dda674a..bd86a2ea917 100644 --- a/gateway/proxy_eth_v1.go +++ b/gateway/proxy_eth_v1.go @@ -682,6 +682,14 @@ func (pv1 *reverseProxyV1) EthTraceFilter(ctx context.Context, filter ethtypes.E } } + head, err := pv1.ChainHead(ctx) + if err != nil { + return nil, err + } + if err := pv1.gateway.checkEthTraceFilterBlockRange(head.Height(), filter); err != nil { + return nil, err + } + return pv1.server.EthTraceFilter(ctx, filter) } diff --git a/gateway/proxy_v2.go b/gateway/proxy_v2.go index dc205c40032..fa09a6673af 100644 --- a/gateway/proxy_v2.go +++ b/gateway/proxy_v2.go @@ -375,6 +375,14 @@ func (pv2 *reverseProxyV2) EthTraceFilter(ctx context.Context, filter ethtypes.E } } + head, err := pv2.ChainGetTipSet(ctx, types.TipSetSelectors.Latest) + if err != nil { + return nil, err + } + if err := pv2.gateway.checkEthTraceFilterBlockRange(head.Height(), filter); err != nil { + return nil, err + } + return pv2.server.EthTraceFilter(ctx, filter) } diff --git a/itests/gateway_test.go b/itests/gateway_test.go index ee4b1370b6b..79fc4d175c4 100644 --- a/itests/gateway_test.go +++ b/itests/gateway_test.go @@ -35,6 +35,7 @@ import ( "github.com/filecoin-project/lotus/itests/multisig" res "github.com/filecoin-project/lotus/lib/result" "github.com/filecoin-project/lotus/node" + "github.com/filecoin-project/lotus/node/config" ) const ( @@ -613,3 +614,166 @@ func TestGatewayF3(t *testing.T) { require.Nil(t, cert) }) } + +// TestEthBlockRangeLimits verifies that block range limits are enforced on +// EthTraceFilter and EthGetLogs, returning the standard Ethereum JSON-RPC +// -32005 "limit exceeded" error code via ErrBlockRangeExceeded. +func TestEthBlockRangeLimits(t *testing.T) { + const ( + nodeMaxRange uint64 = 10 + gatewayMaxRange int64 = 5 + ) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + full, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), + kit.WithCfgOpt(func(cfg *config.FullNode) error { + cfg.Events.MaxFilterHeightRange = nodeMaxRange + return nil + }), + ) + ens.InterconnectAll().BeginMining(100 * time.Millisecond) + + full.WaitTillChain(ctx, kit.HeightAtLeast(20)) + + head, err := full.EthBlockNumber(ctx) + require.NoError(t, err) + + // Block range that exceeds the node's MaxFilterHeightRange + fromBlock := fmt.Sprintf("0x%x", uint64(head)-nodeMaxRange-5) + toBlock := fmt.Sprintf("0x%x", uint64(head)) + // Block range within the node's limit + withinFrom := fmt.Sprintf("0x%x", uint64(head)-nodeMaxRange+1) + + t.Run("trace_filter_node", func(t *testing.T) { + req := require.New(t) + + filter := ethtypes.EthTraceFilterCriteria{ + FromBlock: &fromBlock, + ToBlock: &toBlock, + } + _, err := full.EthTraceFilter(ctx, filter) + req.Error(err) + + var blockRangeErr *api.ErrBlockRangeExceeded + req.ErrorAs(err, &blockRangeErr) + req.Contains(blockRangeErr.Error(), "block range exceeds maximum") + + withinFilter := ethtypes.EthTraceFilterCriteria{ + FromBlock: &withinFrom, + ToBlock: &toBlock, + } + _, err = full.EthTraceFilter(ctx, withinFilter) + req.NoError(err) + }) + + t.Run("eth_getLogs_node", func(t *testing.T) { + req := require.New(t) + + filter := ethtypes.EthFilterSpec{ + FromBlock: &fromBlock, + ToBlock: &toBlock, + } + _, err := full.EthGetLogs(ctx, &filter) + req.Error(err) + + var blockRangeErr *api.ErrBlockRangeExceeded + req.ErrorAs(err, &blockRangeErr) + req.Contains(blockRangeErr.Error(), "block range exceeds maximum") + }) + + t.Run("trace_filter_gateway", func(t *testing.T) { + req := require.New(t) + + // Set up gateway with a tighter trace filter limit + v1EthSubHandler := gateway.NewEthSubHandler() + v2EthSubHandler := gateway.NewEthSubHandler() + gwapi := gateway.NewNode( + full, full.V2, + gateway.WithV1EthSubHandler(v1EthSubHandler), + gateway.WithV2EthSubHandler(v2EthSubHandler), + gateway.WithEthTraceFilterMaxBlockRange(gatewayMaxRange), + gateway.WithMaxLookbackDuration(24*365*time.Hour), // test timestamps don't match wall clock + ) + handler, err := gateway.Handler(gwapi) + req.NoError(err) + t.Cleanup(func() { _ = handler.Shutdown(ctx) }) + + l, err := net.Listen("tcp", "127.0.0.1:0") + req.NoError(err) + srv, _, _ := kit.CreateRPCServer(t, handler, l) + gwAddr := srv.Listener.Addr().String() + + gapiv1, v1Closer, err := client.NewGatewayRPCV1(ctx, "ws://"+gwAddr+"/rpc/v1", nil, + jsonrpc.WithClientHandler("Filecoin", v1EthSubHandler), + jsonrpc.WithClientHandlerAlias("eth_subscription", "Filecoin.EthSubscription"), + ) + req.NoError(err) + t.Cleanup(v1Closer) + + gapiv2, v2Closer, err := client.NewGatewayRPCV2(ctx, "ws://"+gwAddr+"/rpc/v2", nil, + jsonrpc.WithClientHandler("Filecoin", v2EthSubHandler), + jsonrpc.WithClientHandlerAlias("eth_subscription", "Filecoin.EthSubscription"), + ) + req.NoError(err) + t.Cleanup(v2Closer) + + // Range exceeding gateway limit (but within node limit) + gwFromBlock := fmt.Sprintf("0x%x", uint64(head)-uint64(gatewayMaxRange)-2) + gwWithinFrom := fmt.Sprintf("0x%x", uint64(head)-uint64(gatewayMaxRange)+1) + + for _, tc := range []struct { + name string + caller interface { + EthTraceFilter(context.Context, ethtypes.EthTraceFilterCriteria) ([]*ethtypes.EthTraceFilterResult, error) + } + rpcVersion int + }{ + {"v1", gapiv1, 1}, + {"v2", gapiv2, 2}, + } { + t.Run(tc.name, func(t *testing.T) { + req := require.New(t) + + filter := ethtypes.EthTraceFilterCriteria{ + FromBlock: &gwFromBlock, + ToBlock: &toBlock, + } + + _, err := tc.caller.EthTraceFilter(ctx, filter) + req.Error(err) + + var blockRangeErr *api.ErrBlockRangeExceeded + req.ErrorAs(err, &blockRangeErr) + req.Contains(blockRangeErr.Error(), "block range exceeds maximum") + + // Verify raw JSON-RPC error code is -32005 + rawPayload := fmt.Sprintf( + `{"jsonrpc":"2.0","method":"Filecoin.EthTraceFilter","params":[{"fromBlock":"%s","toBlock":"%s"}],"id":1}`, + gwFromBlock, toBlock, + ) + status, body := makeManualRpcCall(t, tc.rpcVersion, http.DefaultClient, gwAddr, rawPayload) + req.Equal(http.StatusOK, status) + + var rpcResp struct { + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error,omitempty"` + } + req.NoError(json.Unmarshal([]byte(body), &rpcResp)) + req.NotNil(rpcResp.Error) + req.Equal(-32005, rpcResp.Error.Code) + req.Contains(rpcResp.Error.Message, "block range exceeds maximum") + + withinFilter := ethtypes.EthTraceFilterCriteria{ + FromBlock: &gwWithinFrom, + ToBlock: &toBlock, + } + _, err = tc.caller.EthTraceFilter(ctx, withinFilter) + req.NoError(err) + }) + } + }) +} diff --git a/node/builder_chain.go b/node/builder_chain.go index eec37f2b502..5f6140d51fe 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -317,12 +317,12 @@ func ConfigFullNode(c interface{}) Option { Override(new(full.EthTransactionAPIV1), modules.MakeEthTransactionV1(cfg.Fevm)), Override(new(full.EthLookupAPIV1), modules.MakeEthLookupV1), - Override(new(full.EthTraceAPIV1), modules.MakeEthTraceV1(cfg.Fevm)), + Override(new(full.EthTraceAPIV1), modules.MakeEthTraceV1(cfg.Fevm, cfg.Events)), Override(new(full.EthGasAPIV1), modules.MakeEthGasV1), Override(new(full.EthTransactionAPIV2), modules.MakeEthTransactionV2(cfg.Fevm)), Override(new(full.EthLookupAPIV2), modules.MakeEthLookupV2), - Override(new(full.EthTraceAPIV2), modules.MakeEthTraceV2(cfg.Fevm)), + Override(new(full.EthTraceAPIV2), modules.MakeEthTraceV2(cfg.Fevm, cfg.Events)), Override(new(full.EthGasAPIV2), modules.MakeEthGasV2), ), If(!cfg.Fevm.EnableEthRPC, diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index 47f1f9083c3..485c0f31d87 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -231,7 +231,7 @@ of filters per connection.`, Type: "uint64", Comment: `MaxFilterHeightRange specifies the maximum range of heights that can be used in a filter (to avoid querying -the entire chain)`, +the entire chain). Applies to eth_getLogs and trace_filter block range limits.`, }, }, "FaultReporterConfig": { diff --git a/node/config/types.go b/node/config/types.go index fb8ebb60cfb..13686a33b00 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -571,7 +571,7 @@ type EventsConfig struct { MaxFilterResults int // MaxFilterHeightRange specifies the maximum range of heights that can be used in a filter (to avoid querying - // the entire chain) + // the entire chain). Applies to eth_getLogs and trace_filter block range limits. MaxFilterHeightRange uint64 } diff --git a/node/impl/eth/events.go b/node/impl/eth/events.go index 6f035c866ad..ef4e6e29879 100644 --- a/node/impl/eth/events.go +++ b/node/impl/eth/events.go @@ -82,6 +82,12 @@ func NewEthEventsAPI( func (e *ethEvents) EthGetLogs(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) (*ethtypes.EthFilterResult, error) { ces, err := e.ethGetEventsForFilter(ctx, filterSpec) if err != nil { + // Propagate ErrBlockRangeExceeded unwrapped so go-jsonrpc can match the + // type and return the correct -32005 error code. + var bre *api.ErrBlockRangeExceeded + if errors.As(err, &bre) { + return nil, bre + } return nil, xerrors.Errorf("failed to get events for filter: %w", err) } return ethFilterResultFromEvents(ctx, ces, e.chainStore, e.stateManager) @@ -618,18 +624,18 @@ func parseBlockRange(heaviest abi.ChainEpoch, fromBlock, toBlock *string, maxRan 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) + return 0, 0, api.NewErrBlockRangeExceeded(uint64(maxRange), uint64(maxHeight-heaviest)) } } 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) + return 0, 0, api.NewErrBlockRangeExceeded(uint64(maxRange), uint64(heaviest-minHeight)) } } 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) + return 0, 0, xerrors.Errorf("invalid epoch range: to block (%d) must be after from block (%d)", maxHeight, minHeight) } 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 0, 0, api.NewErrBlockRangeExceeded(uint64(maxRange), uint64(maxHeight-minHeight)) } } return minHeight, maxHeight, nil diff --git a/node/impl/eth/events_test.go b/node/impl/eth/events_test.go index 8849d870160..be873c20360 100644 --- a/node/impl/eth/events_test.go +++ b/node/impl/eth/events_test.go @@ -1,7 +1,6 @@ package eth import ( - "fmt" "testing" "github.com/ipfs/go-cid" @@ -9,6 +8,7 @@ import ( "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" ) @@ -32,7 +32,7 @@ func TestParseBlockRange(t *testing.T) { maxRange: 10, minOut: 0, maxOut: 0, - errStr: "too large", + errStr: "block range exceeds maximum", }, "fails when min is specified and range is greater than max allowed range": { heaviest: 500, @@ -41,7 +41,7 @@ func TestParseBlockRange(t *testing.T) { maxRange: 10, minOut: 0, maxOut: 0, - errStr: "too far in the past", + errStr: "block range exceeds maximum", }, "fails when max is specified and range is greater than max allowed range": { heaviest: 500, @@ -50,7 +50,7 @@ func TestParseBlockRange(t *testing.T) { maxRange: 10, minOut: 0, maxOut: 0, - errStr: "too large", + errStr: "block range exceeds maximum", }, "works when range is valid": { heaviest: 500, @@ -77,9 +77,10 @@ func TestParseBlockRange(t *testing.T) { 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) + var blockRangeErr *api.ErrBlockRangeExceeded + require.ErrorAs(t, err, &blockRangeErr) } else { require.NoError(t, err) } diff --git a/node/impl/eth/trace.go b/node/impl/eth/trace.go index 85a2df5fb18..1ca1451a170 100644 --- a/node/impl/eth/trace.go +++ b/node/impl/eth/trace.go @@ -37,6 +37,7 @@ type ethTrace struct { tipsetResolver TipSetResolver traceFilterMaxResults uint64 + maxFilterHeightRange uint64 } func NewEthTraceAPI( @@ -45,6 +46,7 @@ func NewEthTraceAPI( ethTransactionApi EthTransactionAPI, tipsetResolver TipSetResolver, ethTraceFilterMaxResults uint64, + maxFilterHeightRange uint64, ) EthTraceAPI { return ðTrace{ chainStore: chainStore, @@ -52,6 +54,7 @@ func NewEthTraceAPI( ethTransactionApi: ethTransactionApi, tipsetResolver: tipsetResolver, traceFilterMaxResults: ethTraceFilterMaxResults, + maxFilterHeightRange: maxFilterHeightRange, } } @@ -278,6 +281,10 @@ func (e *ethTrace) EthTraceFilter(ctx context.Context, filter ethtypes.EthTraceF return nil, xerrors.Errorf("cannot parse toBlock: %w", err) } + if e.maxFilterHeightRange != 0 && toBlock > fromBlock && uint64(toBlock-fromBlock) > e.maxFilterHeightRange { + return nil, api.NewErrBlockRangeExceeded(e.maxFilterHeightRange, uint64(toBlock-fromBlock)) + } + results := []*ethtypes.EthTraceFilterResult{} if filter.Count != nil { diff --git a/node/modules/eth.go b/node/modules/eth.go index 224417a237e..63404b78597 100644 --- a/node/modules/eth.go +++ b/node/modules/eth.go @@ -155,7 +155,7 @@ func makeEthTransaction(params EthTransactionParams, tipSetResolver eth.TipSetRe ) } -func MakeEthTraceV1(cfg config.FevmConfig) func( +func MakeEthTraceV1(cfg config.FevmConfig, eventsCfg config.EventsConfig) func( chainStore eth.ChainStore, stateManager eth.StateManager, ethTransaction full.EthTransactionAPIV1, @@ -167,11 +167,11 @@ func MakeEthTraceV1(cfg config.FevmConfig) func( ethTransaction full.EthTransactionAPIV1, tipsetResolver full.EthTipSetResolverV1, ) full.EthTraceAPIV1 { - return eth.NewEthTraceAPI(chainStore, stateManager, ethTransaction, tipsetResolver, cfg.EthTraceFilterMaxResults) + return eth.NewEthTraceAPI(chainStore, stateManager, ethTransaction, tipsetResolver, cfg.EthTraceFilterMaxResults, eventsCfg.MaxFilterHeightRange) } } -func MakeEthTraceV2(cfg config.FevmConfig) func( +func MakeEthTraceV2(cfg config.FevmConfig, eventsCfg config.EventsConfig) func( chainStore eth.ChainStore, stateManager eth.StateManager, ethTransaction full.EthTransactionAPIV2, @@ -183,7 +183,7 @@ func MakeEthTraceV2(cfg config.FevmConfig) func( ethTransaction full.EthTransactionAPIV2, tipsetResolver full.EthTipSetResolverV2, ) full.EthTraceAPIV2 { - return eth.NewEthTraceAPI(chainStore, stateManager, ethTransaction, tipsetResolver, cfg.EthTraceFilterMaxResults) + return eth.NewEthTraceAPI(chainStore, stateManager, ethTransaction, tipsetResolver, cfg.EthTraceFilterMaxResults, eventsCfg.MaxFilterHeightRange) } }