Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
44 changes: 44 additions & 0 deletions api/api_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -67,6 +71,8 @@ var (
_ error = (*ErrNullRound)(nil)
_ jsonrpc.RPCErrorCodec = (*ErrNullRound)(nil)
_ error = (*errPaymentChannelDisabled)(nil)
_ error = (*ErrBlockRangeExceeded)(nil)
_ jsonrpc.RPCErrorCodec = (*ErrBlockRangeExceeded)(nil)
)

func init() {
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
}
Comment thread
rvagg marked this conversation as resolved.
}

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
}
9 changes: 8 additions & 1 deletion cmd/lotus-gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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")
)
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion documentation/en/default-lotus-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions gateway/eth_utils.go
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
rjan90 marked this conversation as resolved.
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
Comment thread
rvagg marked this conversation as resolved.
}
}
72 changes: 43 additions & 29 deletions gateway/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions gateway/proxy_eth_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Comment thread
rvagg marked this conversation as resolved.
return pv1.server.EthTraceFilter(ctx, filter)
}

Expand Down
8 changes: 8 additions & 0 deletions gateway/proxy_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Comment thread
rvagg marked this conversation as resolved.
return pv2.server.EthTraceFilter(ctx, filter)
}

Expand Down
Loading
Loading