Skip to content

ERA file consumer and producer via LCLI#9273

Open
dapplion wants to merge 15 commits into
sigp:unstablefrom
dapplion:era-lcli-upstream
Open

ERA file consumer and producer via LCLI#9273
dapplion wants to merge 15 commits into
sigp:unstablefrom
dapplion:era-lcli-upstream

Conversation

@dapplion
Copy link
Copy Markdown
Collaborator

@dapplion dapplion commented May 7, 2026

Description

ERA file import/export for Lighthouse. Format spec: https://github.com/status-im/nimbus-eth2/blob/stable/docs/the_auditors_handbook/src/02.4_the_era_file_format.md.

Implemented as lcli subcommands so the beacon startup path stays untouched; once exercised this can graduate into beacon. Depends on the reconstruct_historic_states_on_range from #9222.

Import a directory of ERA files into a fresh datadir

lcli consume-era-files --datadir <path> --era-dir <path> [--era-trusted-state <era>:<root>]

Safety checks (ERA data integrity)

  • Reference state (highest era, or the --era-trusted-state ERA) verified against the supplied state root, otherwise
    against genesis_validators_root.
  • Each era's block_roots / state_roots checked against historical_roots (pre-Capella) / historical_summaries
    (post-Capella) of the reference state.
  • Each block root checked against the boundary state's block_roots.
  • Block signatures NOT verified.

Produce ERA files from a fully reconstructed archive node

lcli produce-era-files --datadir <path> --output-dir <path>

After import, store metadata (split, anchor, fork choice) is advanced so the node boots via the regular resume_from_db path.

TODO tests

dapplion added 13 commits May 7, 2026 04:28
Test vectors are now hosted at dapplion/era-test-vectors and downloaded
via Makefile (same pattern as slashing_protection interchange tests).
- Add docs to EraFileDir, import_all, and module-level usage example
- Rename let _span to let _ for debug spans
- Remove unused _start_slot variable
- Extract parse_era_filename with unit tests
- Add rejects_wrong_trusted_slot test
EraFileDir::new now takes genesis_validators_root and EraImportTrust:
- TrustedStateRoot(era_number, root): uses that ERA as reference,
  verifies its state root, imports only ERAs 0..=era_number
- Untrusted: uses highest ERA in directory as reference

Trust checks (genesis_validators_root, state root) moved from
import_all/import_era_file into EraFileDir::new. Removed all
expects/unwraps from production code.
Add `init_genesis_store` + `advance_store_to_era` so that after ERA import
the store metadata (split, anchor, fork choice) is fully set up for the
regular `resume_from_db` → `build()` startup path.

Key changes in consumer.rs:
- `init_genesis_store`: standalone genesis init (block, state, anchor, fork choice)
- `advance_store_to_era`: advances split/anchor/fork choice to ERA boundary
- `write_state_root_index_for_era`: writes both BeaconStateRoots (slot→root)
  and BeaconColdStateSummary (root→slot) for every slot
- Uses `from_persisted` instead of `get_forkchoice_store` to avoid deriving
  a wrong anchor block root from the ERA boundary state's latest_block_header

Test `chain_boots_from_imported_db` verifies:
- canonical_head matches expected head root
- Every slot's state is accessible via state_root_at_slot → get_state
- Every slot's block is accessible via block_root_at_slot → get_blinded_block
- Blocks form a valid parent chain (parent_root linkage)

Also fixes producer to use get_blinded_block + make_full_block for cold blocks
where get_full_block fails when prune_payloads is enabled.
Separate store initialization from the ERA consumer since it's not part
of the production beacon node path.
@dapplion dapplion requested a review from michaelsproul as a code owner May 7, 2026 02:39
@dapplion dapplion added work-in-progress PR is a work-in-progress database labels May 7, 2026
`hot_storage_strategy` reads `hot_hdiff_start_slot()` which returns
`anchor_slot`. With the anchor still at slot 0 from `init_genesis_store`,
storing the head state at the ERA boundary produces `DiffFrom(intermediate)`
and fails because the hot DB has no preceding diffs (we just imported into
the cold DB).

Update the anchor in memory first so the strategy sees `anchor_slot ==
head_slot` and stores the head as a `Snapshot`. The kv-op is still added to
the same atomic batch as `set_split` and the persisted fork choice for
crash-consistent persistence.
Two related fixes for the genesis store init:

1. Store genesis state in the hot DB via `put_state` in addition to the
   existing `put_cold_state`. The hot state summary is required by
   `HotColdDB::load_split`, which resolves `split.block_root` via summary
   on subsequent reopens — needed for crash-resume.

2. Initialize the anchor in memory before `put_state`. `put_state` calls
   `hot_storage_strategy`, which reads `anchor_slot`; without an
   initialized anchor it errors with `AnchorUninitialized`. The anchor's
   kv-op is still included in the same atomic batch with `set_split` and
   the persisted fork choice for crash-consistent persistence.

`put_cold_state` is retained because era 1's `DiffFrom` strategy chains
against the slot-0 snapshot in the cold DB (without it, `put_cold_state`
in the first era errors with `MissingSnapshot(Slot(0))`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

database work-in-progress PR is a work-in-progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant