fix(replay): repair genesis history for v2 (StorageV2) archives#24
fix(replay): repair genesis history for v2 (StorageV2) archives#24procdump wants to merge 6 commits into
Conversation
|
Claude finished @procdump's task in 4m 5s —— View job Code Review —
|
Genesis storage/account history in reth's v2 (hashed-state) genesis init is written wrong: insert_storage_history keys StoragesHistory by the plain slot while the v2 read looks up keccak256(slot) (genesis storage reads 0x0 at historical blocks), and IndexAccountHistoryStage clears AccountsHistory and rebuilds from changesets, dropping immutable genesis accounts (empty code on historical eth_call). Genesis has no changeset, so the normal hashed indexing can't reconstruct either — it must be repaired explicitly. Add two RethEnv methods (archive-replay only): - fix_genesis_history: re-key genesis storage history to hashed slots. For each genesis (addr, hashed_slot) it read-merges the genesis block into the EARLIEST existing shard (via storage_history_shards), rather than overwriting the open shard. This preserves the post-genesis history of genesis slots that are also mutated later (e.g. balance/registry slots) and places block 0 in the shard the historical read consults for early blocks. Idempotent; per-slot prefix reads, no full-table scan. - fix_genesis_account_history: re-seed a block-0 AccountsHistory entry for genesis accounts with <=1 shard via read-merge; multi-shard accounts (post-genesis changesets) are left untouched. Wire into rayls-replay: run both automatically at the end of a normal replay (freshly built archives boot correct with no extra step), and keep --fix_genesis_history to repair archives built before this (e.g. mainnet). Both no-op on v1 archives and on already-correct entries. Defer open_db past the maintenance/unwind early returns so those modes don't open the consensus DB.
Extract the fix logic into fix_genesis_history_with / fix_genesis_account _history_with (take &ProviderFactory + use_hashed_state, return the count fixed); the &self methods delegate. Lets a unit test drive the real logic against a manually-built v2 storage stack without constructing a full RethEnv. Add test_fix_genesis_history_rekeys_and_preserves_post_genesis: builds a v2 provider factory, runs canonical genesis init (which writes genesis storage history under the buggy plain slot), simulates the post-genesis history a replay appends under the hashed slot, then re-keys and asserts: - precondition: hashed key lacks the genesis entry, plain key carries it - immutable slot seeded with [0] - single-shard mutable slot -> [0, 5] (post-genesis change preserved) - multi-shard slot -> genesis prepended into the earliest shard, open shard untouched - idempotent second run re-keys nothing Runs under cargo test -p rayls-execution-evm --features archive-replay.
Establishes the key difference between the two halves of the bug: the v2 historical read looks up AccountsHistory by the PLAIN address (no hashing), and insert_account_history writes the plain address too — so, unlike StoragesHistory, there is no key mismatch. Genesis init seeds account history correctly, so fix_genesis_account_history is a no-op on a replay-built archive (which only appends, never runs the first-sync clear). test_fix_genesis_account_history_seeds_after_clear_and_skips_multishard: - genesis init already seeds AccountsHistory[addr]=[0] -> fix no-ops - after simulating IndexAccountHistoryStage's first-sync clear, the fix re-seeds block 0 for cleared single-shard accounts - multi-shard (post-genesis changeset) accounts are left untouched - idempotent So the account fix is defensive (covers the cleared-history case a normal node produces); the necessary, always-applicable fix is the storage re-key.
Observers hardcoded --full, which prunes account/storage history to ~10k blocks. Gate it behind DISABLE_PRUNING in both launch paths (local-testnet.sh and start-local-observer.sh): when set, observers run without --full so history is retained and their datadir can seed rayls-replay. Validators are unaffected. Documented in .env.example (left commented so an inline DISABLE_PRUNING=1 is not overwritten when .env is sourced).
Prepending block 0 into the earliest shard overflows it when that shard is already full (a hot genesis-seeded slot — e.g. an ERC-20 balance for a genesis-funded holder — with >= NUM_OF_INDICES_IN_SHARD post-genesis changes), producing a 2001-entry shard that violates reth's per-shard invariant. Keep the fast in-place prepend for the common case (single open shard with room), but when it would overflow, gather the slot's full block list, prepend genesis, delete the old shards and re-split into <=2000 chunks (non-final keyed by highest block, final keyed u64::MAX). Batch deletes precede puts so stale shard keys don't linger. Handles arbitrary cascades (N input shards -> N+1 output). Extend the storage test's slot_c to a full sealed shard [1..=2000] + full open shard [2001..=4000]: asserts the 4001 blocks re-chunk into three <=2000 shards, block 0 in the earliest, open shard keyed u64::MAX, every block preserved, and idempotent on re-run.
b4be69c to
5c84c6d
Compare
|
Claude finished @procdump's task in 4m 21s —— View job Code Review —
|
|
Claude finished @procdump's task in 3m 36s —— View job Code Review —
|
- Rename CLI flag --fix_genesis_history -> --fix-genesis-history to match the hyphenated style of every other flag; update the inline comment. - fix_genesis_history: debug_assert the re-chunk block list is strictly ascending, documenting the invariant storage_history_shards ordering relies on. - Document that the retained plain-slot genesis entries are intentional (never read by v2; deleting them is avoidable risk for no gain). - Document fix_genesis_account_history's limitation: a multi-shard genesis account in a cleared-history archive is skipped and genesis has no changeset to rebuild from, so it won't regain block 0 (non-issue for replay archives). - Strengthen the storage test: replace the loose 'fixed >= 3' with an outcome check that every genesis alloc storage slot carries block 0 under its hashed key after the fix. Not addressed (per request): deleting the stale plain-slot entries; and the account overflow guard, since the multi-shard skip already makes an oversized single shard unreachable in practice.
|
Claude finished @procdump's task in 1m 55s —— View job Code Review —
|
Background: what is StorageV2?
reth has two on-disk storage layouts, chosen per node at datadir init and recorded in DB metadata (
--storage.v2/StorageSettings), not by any chain hardfork:AccountsHistory,StoragesHistory) live in RocksDB, receipts/changesets in static files. Storage is keyed bykeccak256(slot).StorageV2 doesn't change consensus, block validity, or the state root — only the on-disk layout. Rayls production snapshots (and archives built from them) are v2.
The problem
On v2 archives, historical reads of genesis-seeded storage return
0x0— e.g.eth_getStorageAtat an early block for the static validator set, oreth_getCodefor immutable system contracts.Root cause is in reth's v2 genesis init (
init_genesis_with_settings), which writes genesis state and genesis history through separate paths and keys them inconsistently:insert_storage_historykeysStoragesHistoryby the plain slot, but the v2 read looks it up bykeccak256(slot)(StorageSlotKey::to_hashed). The read never finds the genesis entry →NotYetWritten→0x0. Present in every v2 genesis. (Genesis state is fine:insert_genesis_hasheswritesHashedStoragesunderkeccak256(slot)— which is why current-block reads work but historical ones don't.)IndexAccountHistoryStage's first-sync clear, whichrayls-replaynever runs.Genesis has no changeset (it's the initial state, not a change from a prior block), so the normal changeset-driven hashed indexing can't reconstruct genesis history — it must be repaired explicitly. This is fundamentally an upstream reth (v1.11.0) defect; we pin reth by tag and can't patch it, so the repair lives in the archive tooling.
What's changed
fix_genesis_history(storage re-key — the necessary fix): for each genesis(address, hashed slot), read-merges the genesis block into the earliest existing shard (viastorage_history_shards), never overwriting the open shard. This preserves the post-genesis history of genesis slots that are also mutated later (balances, registry state) and places block 0 where an early-block read looks. When prepending would overflow a full shard (a hot genesis slot with ≥NUM_OF_INDICES_IN_SHARD= 2000 post-genesis changes), the slot's shards are re-chunked into ≤2000 pieces (stale keys deleted, cascades handled). Per-slot prefix reads, no full-table scan; idempotent.fix_genesis_account_history(account seed — defensive): re-seeds a block-0 entry for≤1-shard genesis accounts via read-merge; multi-shard accounts (post-genesis changesets) are left untouched. A no-op on replay-built archives; included as insurance for cleared-history archives.Wiring in
rayls-replay: both run automatically at the end of a normal replay, so freshly built archives boot correct with no extra step.--fix_genesis_historyremains for repairing archives built before this (e.g. current mainnet). Both no-op on v1 archives and on already-correct entries.open_dbis deferred past the maintenance/unwind early-return paths so those modes don't open the consensus DB.Test-network tooling:
DISABLE_PRUNINGenv var runs the local observer as a full archive (drops reth's--full, which otherwise prunes account/storage history beyond ~10k blocks), so its datadir can seedrayls-replay. Validators unaffected.Testing
Full-DB unit tests (
cargo test -p rayls-execution-evm --features archive-replay) build a v2 storage stack, run canonical genesis init, and assert re-keying end to end:[0][0, 5](post-genesis change not clobbered)u64::MAX, all blocks preservedVerified end to end on a local 4-node network: rebuilt an archive via
rayls-replayand confirmedeth_getStorageAt(RLS_PROXY, ERC1967_IMPL_SLOT, --block 1)returns the implementation address, with a negative control (the un-fixed source datadir returns0x0).Notes for reviewers
archive-replay-gated, so it affectsrayls-replayonly; the liverayls-networknode is unchanged. Live-synced archive observers are therefore not covered — repair their datadir withrayls-replay --fix_genesis_historyif needed.archive-replay-gated tests execute under CI'scargo test --workspace(feature unification viabin/rayls-replayshould enable them).