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
4 changes: 4 additions & 0 deletions evmrpc/block_txcount_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ func (*parityTxCountTMClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (*parityTxCountTMClient) GenesisInitialHeight() int64 {
return 1
}

func (*parityTxCountTMClient) EvmTxByHash(common.Hash) (tmtypes.Tx, bool) {
return nil, false
}
Expand Down
4 changes: 4 additions & 0 deletions evmrpc/genesis_latest_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ func (*freshChainClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (*freshChainClient) GenesisInitialHeight() int64 {
return 1
}

func (*freshChainClient) EvmTxByHash(common.Hash) (tmtypes.Tx, bool) {
return nil, false
}
Expand Down
4 changes: 4 additions & 0 deletions evmrpc/height_availability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ func (*heightTestClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (*heightTestClient) GenesisInitialHeight() int64 {
return 1
}

func (*heightTestClient) EvmTxByHash(common.Hash) (tmtypes.Tx, bool) {
return nil, false
}
Expand Down
4 changes: 4 additions & 0 deletions evmrpc/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ func (*MockClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (*MockClient) GenesisInitialHeight() int64 {
return 1
}

func (*MockClient) EvmTxByHash(hash common.Hash) (tmtypes.Tx, bool) {
tx, err := Encoder(UnconfirmedTx)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions evmrpc/simulate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,10 @@ func (c *fixedBlockClient) EvmNextPendingNonce(common.Address) uint64 {
return 0
}

func (c *fixedBlockClient) GenesisInitialHeight() int64 {
return 1
}

func (c *fixedBlockClient) EvmTxByHash(common.Hash) (tmtypes.Tx, bool) {
return nil, false
}
Expand Down
4 changes: 4 additions & 0 deletions evmrpc/tests/mock_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func (c *MockClient) EvmNextPendingNonce(_ common.Address) uint64 {
return 0
}

func (c *MockClient) GenesisInitialHeight() int64 {
return 1
}

func (*MockClient) EvmTxByHash(common.Hash) (tmtypes.Tx, bool) {
return nil, false
}
Expand Down
1 change: 1 addition & 0 deletions evmrpc/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ type lowLatestTMClient struct {
}

func (c *lowLatestTMClient) EvmNextPendingNonce(common.Address) uint64 { return 0 }
func (c *lowLatestTMClient) GenesisInitialHeight() int64 { return 1 }

func (c *lowLatestTMClient) EvmTxByHash(common.Hash) (tmtypes.Tx, bool) { return nil, false }

Expand Down
24 changes: 24 additions & 0 deletions evmrpc/watermark_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/ethereum/go-ethereum/rpc"
"github.com/sei-protocol/sei-chain/sei-cosmos/client"
sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
genesistypes "github.com/sei-protocol/sei-chain/sei-cosmos/types/genesis"
"github.com/sei-protocol/sei-chain/sei-db/db_engine/types"
"github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt"
"github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes"
Expand Down Expand Up @@ -44,6 +45,16 @@ func NewWatermarkManager(
}
}

// genesisInitialHeight returns the chain's genesis InitialHeight, sourced from
// the local client's in-memory genesis doc, falling back to
// genesis.DefaultGenesisInitialHeight when the client cannot supply it.
func (m *WatermarkManager) genesisInitialHeight() int64 {
if h := m.tmClient.GenesisInitialHeight(); h > 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] nit: the h > 0 fallback to DefaultGenesisInitialHeight (1) is a silent substitution that is wrong in the one case it exists for. If GenDoc is unavailable on a chain whose real InitialHeight is >1, the floor becomes 1 and earliest resolves to a never-committed version — reintroducing the failed to load state at height 1 failure this PR explicitly set out to avoid by not hardcoding 1.

In practice this is unreachable: rpcEnv.GenDoc is always populated (sei-tendermint/node/node.go:234) and ValidateAndComplete normalizes InitialHeight to ≥1, so the branch is defensive only. Two small suggestions: (a) since the value is immutable for the node's lifetime, resolve it once in NewWatermarkManager instead of on every Watermarks() call (which is every read path); and (b) note the trade-off in the comment — falling back to 1 vs. returning 0 to disable the floor are both defensible, but the current choice silently contradicts the interface doc on LocalClient.GenesisInitialHeight ("or 0 if it is unavailable") and the PR description.

return h
}
return genesistypes.DefaultGenesisInitialHeight

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The DefaultGenesisInitialHeight (=1) fallback reintroduces the literal-1 floor that the rest of the change is explicitly designed to avoid: if GenDoc were ever nil on a chain with InitialHeight > 1, earliest resolves to 1 and CreateQueryContext(1) fails with failed to load state at height 1. In practice GenesisDoc.ValidateAndComplete (sei-tendermint/types/genesis.go:134) guarantees InitialHeight >= 1 and local.New guarantees a non-nil Environment, so this is a defensive branch only — but consider either noting that here, or skipping the floor entirely when the initial height is unknown so the failure mode is "unchanged behavior" rather than "error at a never-committed height".

}
Comment on lines +48 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 This PR adds a cheap, RPC-free WatermarkManager.genesisInitialHeight() specifically to avoid the tmClient.Genesis(ctx) call that fails with "genesis response is large" on archive nodes with a >16MiB genesis, but three other 'earliest' resolvers (getBlockNumber in utils.go backing eth_call/eth_estimateGas/eth_createAccessList, Backend.ConvertBlockNumber in simulate.go which panics on error, and DebugAPI.resolveDebugTraceBlockNumber in tracers.go backing debug_traceCall/debug_traceBlockByNumber) still call the old Genesis(ctx) RPC. This is a pre-existing issue, not introduced or worsened by this PR (none of these three files are touched here), but it means the fix is incomplete: on the target large-genesis archive nodes, eth_getBalance/getCode/getTransactionCount/getStorageAt at 'earliest' will now work while eth_call/eth_estimateGas/debug_traceCall at 'earliest' still fail. Worth a follow-up to migrate these three sites to the newly-added tmClient.GenesisInitialHeight().

Extended reasoning...

What the bug is. This PR introduces WatermarkManager.genesisInitialHeight() (evmrpc/watermark_manager.go:48-56), backed by the new LocalClient.GenesisInitialHeight() method, specifically so that resolving the earliest tag no longer needs to call tmClient.Genesis(ctx). The PR description is explicit about why: Genesis() fails with "genesis response is large, please use the genesis_chunked API instead" once a node's genesis doc exceeds a single 16 MiB chunk — exactly the archive/large-genesis node class this PR targets, and this failure applies to the in-process local client too (per the PR's own 'Sourcing InitialHeight' section: 'no Genesis() RPC — that call fails with genesis response is large on archive nodes...').\n\nHowever, three other 'earliest'-tag resolvers elsewhere in evmrpc were left untouched and still call tmClient.Genesis(ctx) directly for the identical purpose:\n\n- getBlockNumber in evmrpc/utils.go:71-77 (case rpc.EarliestBlockNumber: genesisRes, err := tmClient.Genesis(ctx) ... numberPtr = &genesisRes.Genesis.InitialHeight)\n- Backend.ConvertBlockNumber in evmrpc/simulate.go:404-419, which additionally panics on a Genesis() error (panic(\"could not get genesis info from tendermint\"))\n- DebugAPI.resolveDebugTraceBlockNumber in evmrpc/tracers.go:148-156\n\nThe trigger path. getBlockNumber backs Backend.StateAndHeaderByNumberOrHash (via isLatest/getBlockByNumberOrHash), which is what eth_call, eth_estimateGas, and eth_createAccessList use to resolve their block parameter — the exact endpoints this PR's description calls out as fixed. ConvertBlockNumber backs Backend.BlockByNumber (used by tracing), and resolveDebugTraceBlockNumber backs the height guards for debug_traceCall/debug_traceBlockByNumber. None of these three call sites were modified to use the new GenesisInitialHeight(), even though it's exposed on the same LocalClient interface they already hold a reference to.\n\nWhy existing code doesn't prevent it. These three sites predate this PR and were never routed through WatermarkManager's earliest resolution; they each independently reimplement 'call Genesis(), read InitialHeight'. The PR fixes the watermark-based earliest-state paths (eth_getBalance/getCode/getTransactionCount/getStorageAt, which route through ResolveHeight -> Watermarks()), but doesn't touch these three independent implementations.\n\nImpact. On a large-genesis archive node (genesis doc > 16 MiB, > 1 chunk): eth_getBalance(addr, \"earliest\"), "getCode", "getTransactionCount", and "getStorageAt" now succeed via the new RPC-free floor. But eth_call(..., \"earliest\"), eth_estimateGas(..., \"earliest\"), eth_createAccessList(..., \"earliest\"), and debug_traceCall(..., \"earliest\")/debug_traceBlockByNumber(\"earliest\") still fail with the genesis-too-large RPC error — and any tracing call reaching ConvertBlockNumber with earliest outright panics the RPC handler goroutine rather than returning a clean error.\n\nStep-by-step proof. (1) Node has a genesis doc spanning >1 chunk (16 MiB threshold, per sei-tendermint's internal/rpc/core/net.go). (2) Client calls eth_call(txArgs, \"earliest\"). (3) This routes to Backend.StateAndHeaderByNumberOrHash -> getBlockByNumberOrHash -> getBlockNumber(ctx, tmClient, rpc.EarliestBlockNumber) (evmrpc/utils.go:71). (4) getBlockNumber hits case rpc.EarliestBlockNumber and calls tmClient.Genesis(ctx) (utils.go:72). (5) Because genesis exceeds 1 chunk, this RPC returns the error "genesis response is large, please use the genesis_chunked API instead". (6) getBlockNumber returns that error, and eth_call fails entirely — even though, on the very same node, eth_getBalance(addr, \"earliest\") now succeeds via WatermarkManager.Watermarks() -> genesisInitialHeight(), which never calls Genesis().\n\nFix. Migrate getBlockNumber, Backend.ConvertBlockNumber, and DebugAPI.resolveDebugTraceBlockNumber to call tmClient.GenesisInitialHeight() (now available on the same LocalClient interface each of these holds) instead of tmClient.Genesis(ctx). This would also let ConvertBlockNumber drop its panic-on-error path since GenesisInitialHeight() returns a plain int64 with a 0-unavailable sentinel rather than an error.\n\nWhy pre_existing, not normal. None of evmrpc/utils.go, evmrpc/simulate.go, or evmrpc/tracers.go are in this PR's changed-files set. This PR does not add new callers to these three functions, does not make their large-genesis Genesis() failure any more likely to trigger, and in fact moves the opposite direction (away from Genesis()) in the code it does touch. A reviewer (seidroid[bot]) explicitly noted during review that 'getBlockNumber already had this latent issue for explicit earliest requests.' This is a genuine, actionable, in-scope completeness gap worth a fast follow-up, but it's a pre-existing limitation the PR doesn't introduce or worsen, so it shouldn't block this merge.


// Watermarks returns the earliest block height, earliest state height, and
// latest height that are safe to serve. Earliest heights are inclusive.
// It is possible that block latest < block earliest, in case there are no blocks yet.
Expand All @@ -66,6 +77,19 @@ func (m *WatermarkManager) Watermarks(ctx context.Context) (int64, int64, int64,
latest = min(latest, m.stateStore.GetLatestVersion())
stateEarliest = m.stateStore.GetEarliestVersion()
}

// Floor the earliest state height at genesis. A never-pruned store reports
// its earliest version as 0 (the earliest-version key is only written by
// pruning or state-sync), which would otherwise resolve `earliest` to the
// tip via CreateQueryContext's height-0 coercion. Guarding on
// stateEarliest < latest leaves the pre-commit window (no blocks yet, where
// stateEarliest == latest) reading the genesis checkState, and the clamp to
// latest keeps the floor from ever exceeding the safe latest. Using the real
// InitialHeight (not a literal 1) keeps this correct for chains started
// above height 1, whose version 1 was never committed.
if stateEarliest < latest {
stateEarliest = max(stateEarliest, min(m.genesisInitialHeight(), latest))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Worth calling out a user-visible consequence beyond the earliest tag: because the floor is applied to stateEarliest, which ResolveHeight feeds to ensureWithinWatermarks for explicit heights too, state reads at "0x0" on a post-genesis node now fail with requested height 0 has been pruned; earliest available is 1.

Erroring is clearly better than the current behavior (silently serving tip state), and the PR calls this out as intentional hardening. But note it leaves the block and state surfaces inconsistent for the same tag: eth_getBlockByNumber("0x0") still returns the synthetic genesis block (evmrpc/block.go:41-53, genesisBlockHash), while eth_getBalance(addr, "0x0") now errors. Since block 0 is a common client idiom for "genesis", consider either mapping height 0 to the genesis floor on the state path as well, or documenting the divergence in evmrpc/AGENTS.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Behavior change worth confirming as intended: on SS-enabled nodes this also raises the floor used by ensureWithinWatermarks for explicit heights, so eth_getBalance(addr, "0x0") (and getCode/getStorageAt/eth_call, all of which go through ResolveHeight) now returns requested height 0 has been pruned; earliest available is 1 where it previously fell through to the tip.

That matches what SS-disabled nodes already do (stateEarliest == latest there), and an error beats silently serving tip state, but two things make it worth an explicit decision + test:

  • The PR body lists "block 0" as part of the reported bug, and the fix converts it to an error rather than to genesis state.
  • This codebase does model a genesis block at height 0: evmrpc/block.go:41-45 returns a synthetic genesis block for eth_getBlockByNumber("0x0"), and GetBlockNumberByNrOrHash maps genesisBlockHash back to height 0 (evmrpc/utils.go:51-55). So clients can fetch that block but get a "pruned" error querying state at it.

If erroring is the intended contract, a test pinning it (alongside TestExplicitReadBelowGenesisFloorRejected) would prevent future drift.

}
return blockEarliest, stateEarliest, latest, nil
}

Expand Down
Loading
Loading