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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ This is a release candidate of a patch release that extends EC finality tooling
- fix(gas): stricter bounds for `GasEstimateGasPremium` lookback ([filecoin-project/lotus#13556](https://github.com/filecoin-project/lotus/pull/13556))
- fix: remove duplicate SQL statement entries from `preparedStatementMapping` ([filecoin-project/lotus#13545](https://github.com/filecoin-project/lotus/pull/13545))
- fix(api): `StateSearchMsg` should respect `lookbackLimit` [filecoin-project/lotus#13562](https://github.com/filecoin-project/lotus/pull/13562)
- 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))
- 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))


## 👌 Improvements

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),
}
}

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
}
16 changes: 13 additions & 3 deletions chain/ecfinality/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,28 @@ func (c *ECFinalityCache) GetFinalizedTipSet(ctx context.Context) (*types.TipSet
return s.FinalizedTipSet, nil
}

// walkChain walks back from head collecting block counts for the calculator.
// Each LoadTipSet call typically hits the ChainStore's ARC cache.
// walkChain walks back from head collecting block counts per epoch for the
// calculator. Null rounds (epochs with no blocks) are included as 0 entries
// so that the calculator sees the real timeline and the returned array depth
// corresponds directly to epoch height differences. Each LoadTipSet call
// typically hits the ChainStore's ARC cache.
func (c *ECFinalityCache) walkChain(ctx context.Context, head *types.TipSet) ([]int, error) {
needed := c.windowSize
chain := make([]int, 0, needed)
ts := head
for len(chain) < needed {
for {
chain = append(chain, len(ts.Cids()))
if len(chain) >= needed {
break
}
parent, err := c.cs.LoadTipSet(ctx, ts.Parents())
if err != nil {
return nil, err
}
// Insert 0 entries for null rounds between this tipset and its parent.
for nulls := int(ts.Height()-parent.Height()) - 1; nulls > 0 && len(chain) < needed; nulls-- {
chain = append(chain, 0)
}
ts = parent
}
// Reverse to chronological order (oldest first).
Expand Down
4 changes: 2 additions & 2 deletions chain/ecfinality/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const (
// BisectLow and BisectHigh define the search range for the bisect algorithm
// that finds the epoch depth at which the finality guarantee is met. A low
// bound of 3 avoids evaluating trivially shallow depths; a high bound of
// 200 accommodates degraded chains that take longer to finalize.
// 450 accommodates degraded chains that take longer to finalize.
BisectLow = 3
BisectHigh = 200
BisectHigh = 450

// DefaultBlocksPerEpoch is the Filecoin mainnet expected block production rate.
DefaultBlocksPerEpoch = 5.0
Expand Down
6 changes: 3 additions & 3 deletions chain/ecfinality/calculator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ func TestFindThresholdDepth_HealthyChain(t *testing.T) {
func TestFindThresholdDepth_DegradedChain(t *testing.T) {
req := require.New(t)

// All-2s chain is too degraded to achieve 2^-30 within the bisect
// search range (BisectHigh=200), so threshold is not found
// All-1s chain is too degraded to achieve 2^-30 within the bisect
// search range (BisectHigh=450), so threshold is not found
chain := make([]int, 905)
for i := range chain {
chain[i] = 2
chain[i] = 1
}
guarantee := math.Pow(2, -30)

Expand Down
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
13 changes: 10 additions & 3 deletions cmd/lotus-shed/finality.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,20 @@ machine-readable output of all 900 epochs.`,
headEpoch = int(head.Height())
readLength := int(policy.ChainFinality) + 5
chain = append(chain, len(head.Cids()))
for range readLength - 1 {
head, err = api.ChainGetTipSet(ctx, head.Parents())
for len(chain) < readLength {
parent, err := api.ChainGetTipSet(ctx, head.Parents())
if err != nil {
return err
}
chain = append(chain, len(head.Cids()))
// Insert 0 entries for null rounds between this tipset and its parent.
for nulls := int(head.Height()-parent.Height()) - 1; nulls > 0 && len(chain) < readLength; nulls-- {
chain = append(chain, 0)
}
chain = append(chain, len(parent.Cids()))
head = parent
}
// Trim to exact length in case null round insertion overshot.
chain = chain[:readLength]
// API walk produces most-recent-first; reverse to match the
// expected ordering (index 0 = earliest epoch).
slices.Reverse(chain)
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 {
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
}
}
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
Loading
Loading