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
33 changes: 27 additions & 6 deletions rpc/handlers/eth_getlogs_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
57 changes: 43 additions & 14 deletions rpc/handlers/extra.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading