diff --git a/rpc/handlers/eth_getlogs_local.go b/rpc/handlers/eth_getlogs_local.go index 26c2899..a67052f 100644 --- a/rpc/handlers/eth_getlogs_local.go +++ b/rpc/handlers/eth_getlogs_local.go @@ -30,6 +30,7 @@ import ( "github.com/filecoin-project/go-address" abi "github.com/filecoin-project/go-state-types/abi" "github.com/ipfs/go-cid" + "golang.org/x/xerrors" "github.com/Reiers/lantern/chain/msgsearch" ltypes "github.com/Reiers/lantern/chain/types" @@ -52,8 +53,24 @@ type ethLogFilter struct { topics []map[string]bool } -// localEthGetLogs returns (logs[], true, nil) on a clean local resolution, -// or (nil, false, nil) to fall back to the bridge. +// ErrLocalRangeTooWide is returned by localEthGetLogs when the requested +// block range exceeds localGetLogsMaxRange. The caller uses it to surface +// an accurate error to bridge-off clients instead of masking it as +// "FEVM method requires --vm-bridge-rpc" (#76). +var ErrLocalRangeTooWide = xerrors.New("eth_getLogs: block range exceeds local scan cap; chunk the query into smaller windows") + +// ErrLocalOutOfRetention is returned when a requested epoch is not in the +// local header store retention window. Bridge-off clients should either +// widen their retention (Full tier), narrow the query, or accept the +// bridge fallback. +var ErrLocalOutOfRetention = xerrors.New("eth_getLogs: requested epoch not in local retention window") + +// localEthGetLogs returns (logs[], true, nil) on a clean local resolution. +// (nil, false, nil) means "cannot serve, no specific reason" — the caller +// should fall back to the bridge. +// (nil, false, err) means "cannot serve, and here is a specific reason" — +// the caller should prefer this err over errBridgeUnconfigured when the +// bridge itself is not configured. func (c *ChainAPI) localEthGetLogs(ctx context.Context, filterRaw any) (any, bool, error) { if c.HeaderStore == nil || c.BlockGetter == nil { return nil, false, nil @@ -73,7 +90,7 @@ func (c *ChainAPI) localEthGetLogs(ctx context.Context, filterRaw any) (any, boo // blockHash form: single block. Resolve its height locally. h, served := c.heightForBlockHash(f.blockHash, head) if !served { - return nil, false, nil + return nil, false, ErrLocalOutOfRetention } from, to = h, h } else { @@ -92,7 +109,7 @@ func (c *ChainAPI) localEthGetLogs(ctx context.Context, filterRaw any) (any, boo // Bound the scan: a huge range against local state is expensive and // usually means an indexer-style query better served by an upstream. if to-from > localGetLogsMaxRange { - return nil, false, nil + return nil, false, ErrLocalRangeTooWide } bg := newRetryingBlockGetter(c.BlockGetter, 2, 8*time.Second) @@ -103,7 +120,7 @@ func (c *ChainAPI) localEthGetLogs(ctx context.Context, filterRaw any) (any, boo for ep := from; ep <= to; ep++ { ts, err := c.HeaderStore.GetTipSetByHeight(ep) if err != nil || ts == nil { - return nil, false, nil // gap in local range -> bridge + return nil, false, ErrLocalOutOfRetention // gap in local range -> bridge } child, err := searcher.FindChild(ts) if err != nil { @@ -145,7 +162,11 @@ func (c *ChainAPI) localEthGetLogs(ctx context.Context, filterRaw any) (any, boo return out, true, nil } -const localGetLogsMaxRange = abi.ChainEpoch(2880) // ~24h at 30s blocks +// localGetLogsMaxRange caps a single eth_getLogs local scan. Raised from +// 2880 (24h) to 20160 (~7d at 30s blocks) so first-boot Curio watchers +// can backfill a normal weekly window without chunking. Beyond that +// clients should chunk (ErrLocalRangeTooWide). +const localGetLogsMaxRange = abi.ChainEpoch(20160) // logsForReceipt walks one receipt's events AMT and returns the ETH logs // that pass the filter. served=false signals a structural decode failure diff --git a/rpc/handlers/extra.go b/rpc/handlers/extra.go index 0c14c07..7c11fb2 100644 --- a/rpc/handlers/extra.go +++ b/rpc/handlers/extra.go @@ -303,20 +303,32 @@ func (c *ChainAPI) EthMaxPriorityFeePerGas(_ context.Context) (string, error) { return "0x0", nil } -// EthGasPrice returns the chain's current floor base fee in attoFIL, -// hex-encoded. This is the EIP-1559 'gasPrice' compatibility shim; -// strictly speaking Filecoin uses base-fee + premium per message, but -// reporting MinimumBaseFee gives viem clients a workable estimate -// when they call gasPrice during transaction preparation. +// EthGasPrice returns the EIP-1559 'gasPrice' compatibility quote in +// attoFIL, hex-encoded. Strictly speaking Filecoin uses base-fee + +// premium per message; this shim returns (parentBaseFee + gasPremium) +// so a tx builder that prices off gasPrice matches Lotus's answer within +// a few %. Falls back to just base fee if premium can't be estimated, +// and to MinimumBaseFee only when no live head is available. // -// Now wired to the live head base fee (via EthBaseFee) so a tx builder -// that prices off gasPrice during a base-fee spike doesn't underprice and -// stall. Falls back to MinimumBaseFee only when no live head is available. +// Fixes gasPrice-vs-Glif delta observed on 2026-07-23 (~14% low). func (c *ChainAPI) EthGasPrice(ctx context.Context) (string, error) { - if bf, err := c.EthBaseFee(ctx); err == nil && bf != "" && bf != "0x0" { - return bf, nil + baseFeeHex, err := c.EthBaseFee(ctx) + if err != nil || baseFeeHex == "" || baseFeeHex == "0x0" { + return fmt.Sprintf("0x%x", build.MinimumBaseFee), nil } - return fmt.Sprintf("0x%x", build.MinimumBaseFee), nil + baseFee, ok := new(stdbig.Int).SetString(strings.TrimPrefix(baseFeeHex, "0x"), 16) + if !ok { + return baseFeeHex, nil + } + // nblocksincl=10 mirrors what lotus's EthGasPrice uses. Errors are + // non-fatal: on failure we return just the base fee (previous + // behaviour) so we never return an error to the caller. + prem, perr := c.GasEstimateGasPremium(ctx, 10, address.Undef, 10000, types.TipSetKey{}) + if perr != nil || prem.Int == nil { + return baseFeeHex, nil + } + total := new(stdbig.Int).Add(baseFee, prem.Int) + return "0x" + total.Text(16), nil } // EthBaseFee returns the base fee for the next block as a hex-quantity string @@ -985,15 +997,32 @@ func (c *ChainAPI) EthGetBlockByHash(ctx context.Context, blockHash string, full // by client-side payment rail watchers (FilecoinPay rail event // indexing). Lantern doesn't run an FEVM log index of its own; the // upstream's index is the source of truth. +// +// Bridge-off (lantern#76): when the local path cannot serve (range too +// wide, block out of retention, etc) we surface the SPECIFIC reason +// instead of masking it as "FEVM method requires --vm-bridge-rpc". +// That mask is the correct error for genuinely-FEVM-only methods (e.g. +// eth_call), but for eth_getLogs it is misleading: the client can +// almost always chunk the query and get a served answer. func (c *ChainAPI) EthGetLogs(ctx context.Context, filter any) (any, error) { // Local-first (lantern#73): decode logs from per-receipt event AMTs so // a bridge-off node (stock Curio / maxboom) serves PDP settlement + // FilecoinPay rail watchers without a VMBridge. Falls back to the // bridge for ranges/blocks outside the local window or on decode gaps. - if out, served, err := c.localEthGetLogs(ctx, filter); served { - return out, err + out, served, localErr := c.localEthGetLogs(ctx, filter) + if served { + return out, localErr + } + // Not served locally. Prefer the bridge if configured. + if c.Bridge != nil { + return c.forwardEth(ctx, "eth_getLogs", []any{filter}) + } + // Bridge-off. Surface the specific local reason if we have one; else + // fall back to the generic FEVM-required error (parity with #74). + if localErr != nil { + return nil, localErr } - return c.forwardEth(ctx, "eth_getLogs", []any{filter}) + return nil, errBridgeUnconfigured } // forwardEth is the common shape: marshal params, post to bridge,