Skip to content

feat(beacon-node): add block state cache and checkpoint datastore - #452

Merged
wemeetagain merged 6 commits into
mainfrom
gr/state-cache-block-datastore
Jul 8, 2026
Merged

feat(beacon-node): add block state cache and checkpoint datastore#452
wemeetagain merged 6 commits into
mainfrom
gr/state-cache-block-datastore

Conversation

@GrapeBaBa

@GrapeBaBa GrapeBaBa commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Part 1 for #406

  • block_state_cacheFIFOBlockStateCache port: a head-pinned FIFO over recent post-states keyed by state root. Keeps two structures — an insertion-ordered map (so getSeedState returns a stable reload anchor that doesn't churn when the head moves) and a head-pinned FIFO key order (so eviction takes the oldest tail and never the current head). Per-add pruning, with the just-added key protected so max_states == 1 transiently holds one extra. Read-tracking metrics (size / reads / seconds-since-last-read).
  • cp_datastore — the checkpoint-state byte datastore the persistent checkpoint cache persists to. Two backends behind a vtable: File (the file backend fans batch removal out over io.async) and an in-memory Map fake for tests. removeMany stays in the interface so each backend picks its own strategy (concurrent fan-out for I/O, sequential for the non-thread-safe map).
  • key — checkpoint ⇄ datastore-key (0x-hex filename) encoding.
  • metrics — block state-cache metric definitions.

@GrapeBaBa
GrapeBaBa requested a review from a team as a code owner June 28, 2026 06:04
@GrapeBaBa
GrapeBaBa force-pushed the gr/state-cache-block-datastore branch 2 times, most recently from eed7e48 to df96b1d Compare June 29, 2026 03:41
Start the `beacon_node` module by porting two of lodestar's state-cache
pieces under beacon_node/chain/state_cache:

- block_state_cache: FIFOBlockStateCache port — a head-pinned FIFO over
  recent post-states keyed by state root, with read-tracking metrics
  (size / reads / seconds-since-last-read) and a stable seed state.
- cp_datastore: the checkpoint-state byte datastore (file + in-memory
  backends) the persistent checkpoint cache persists to; the file backend
  fans batch removal out over `io.async`.
- key: checkpoint <-> datastore-key (0x-hex filename) encoding.
- metrics: block state-cache metric definitions.

Adds the `beacon_node` build module and a `test:beacon_node` CI step.
The persistent checkpoint cache (and its metrics) follow in a stacked PR.
@GrapeBaBa
GrapeBaBa force-pushed the gr/state-cache-block-datastore branch from df96b1d to b6a2e9a Compare June 29, 2026 04:40
@GrapeBaBa

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6a2e9a207

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/beacon_node/chain/state_cache/cp_datastore.zig
Resolve append-list conflicts in build.zig.zon (clock + beacon_node test
modules) and CI.yml (clock + beacon-node test steps) after the clock module
(#354) merged to main.
Review findings on #452, all independently re-verified (no blocking):

- block_state_cache: add() returns the canonical resident state; the
  duplicate path used to destroy the caller's pointer while the documented
  add-then-setHeadState flow reused it (use-after-free / double free).
  get() now documents the no-in-place-mutation contract.
- block_state_cache: getSeedState returns null on an empty cache (legal
  via the debug-API clear() or cold-start) instead of asserting.
- block_state_cache: recordRead/scanReadStats use the shared time helpers
  instead of hand-rolled timestamp/seconds conversions.
- cp_datastore: isSafeCheckpointState validates the on-disk epoch via the
  division inverse before the boundary multiply; a huge foreign epoch used
  to overflow and panic at boot instead of reading as unsafe.
- cp_datastore: writes go temp-then-rename (per-key '<name>.tmp'), so a
  final name only ever holds complete bytes and a torn write can no longer
  poison its key; initStore sweeps stale temp debris, tolerant of foreign
  entries, and closes any prior dir handle before reopening (fd leak).
- cp_datastore: readKeys accepts .unknown directory entries (d_type-less
  filesystems report regular files as unknown, hiding every persisted
  state); read maps IsDir to absent.
- cp_datastore: readLatestSafe scans candidates by a 72-byte prefix read
  (new readPrefix vtable primitive) and full-reads only the winner; the
  old scan read entire multi-hundred-MB states per rejected candidate.
- metrics: the init test restores the module-global noop metrics on exit,
  so later tests in the same binary don't observe leaked gauge state.
- ssz_bytes: the state byte readers are now tested against real SSZ
  serialization of electra/phase0 states (the previous fixture hand-wrote
  the same offset constants, so a wrong constant passed); the module's
  tests are now aggregated by the state_transition test root.
- build.zig.zon: drop unused .config/.fork_types beacon_node imports; add
  .time.

Final files written by the pre-fix code are not repaired retroactively: a
truncated final left by an old torn write is still trusted by the exists
check. No deployed data dirs exist, so no migration is needed.

test:beacon_node 94/94, test:state_transition 111/111.
@GrapeBaBa

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread src/beacon_node/chain/state_cache/metrics.zig Outdated
Comment thread src/beacon_node/chain/state_cache/block_state_cache.zig Outdated

@twoeths twoeths left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

looks great, I dropped minor comments

GrapeBaBa added 3 commits July 7, 2026 08:08
- Rename the global `cache_metrics` to `block_metrics` so it reads
  unambiguously against the checkpoint cache's metrics in the stacked
  follow-up.
- Reword add()'s doc to describe the state handoff as an ownership
  transfer (transfers on success; stays with the caller on
  hashTreeRoot failure), matching the ownership vocabulary used
  elsewhere.
Use the reviewer's suggested block_cache_metrics — it reads as "the block
cache's metrics" and matches the BlockStateCacheMetrics type; the stacked
checkpoint cache follows with checkpoint_cache_metrics for symmetry.
The cache holds its size flat (every add evicts once full), so
std.HashMap's grow() - the only point that clears tombstones - never
fires, and tombstone-based deletion degrades every probe permanently
under the add/evict churn (ziglang/zig#17851; the std rehash() doc
names exactly this long-lived insert+delete pattern). ArrayHashMap's
index deletes by backward shift and cannot accumulate tombstones; map
iteration order is unused (both orderings live in the intrusive
lists), so eviction uses swapRemove.

Measured with an isolated ordering microbenchmark (landing
separately): the HashMap variant settles at ~10x its clean probe cost
at the real N=64, the ArrayHashMap variant stays flat. Both are far
below the per-add state clone+rehash cost - this removes a structural
degradation, not a user-visible slowdown.
twoeths
twoeths previously approved these changes Jul 8, 2026
@GrapeBaBa

Copy link
Copy Markdown
Contributor Author

Heads-up on a small functional change pushed mid-review: the lookup map switched from AutoHashMapUnmanaged to AutoArrayHashMapUnmanaged + swapRemove (fe5521d). Rationale: this cache holds its size flat (every add evicts once full), so HashMap.grow() — the only point that clears tombstones — never fires, and tombstone-based deletion degrades probes permanently under the add/evict churn (ziglang/zig#17851; the std rehash() doc names exactly this pattern). ArrayHashMap's index deletes by backward shift and is immune; map order is unused (both orderings live in the intrusive lists). Sub-noise either way in production — this removes a structural degradation, not a visible slowdown.

@wemeetagain
wemeetagain merged commit 2145faa into main Jul 8, 2026
30 checks passed
@wemeetagain
wemeetagain deleted the gr/state-cache-block-datastore branch July 8, 2026 19:01
@github-actions github-actions Bot mentioned this pull request Jul 30, 2026
wemeetagain pushed a commit that referenced this pull request Aug 19, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.0.0](v0.1.2...v1.0.0)
(2026-08-19)


### Features

* add `state.getBuildersLength()` binding
([#472](#472))
([be2b5ab](be2b5ab))
* **beacon-node:** add block state cache and checkpoint datastore
([#452](#452))
([2145faa](2145faa))
* bindings to `getExpectedWithdrawals` and native tweaks
([#350](#350))
([f47bc66](f47bc66))
* **bindings:** add pubkey cache syncPubkeys
([#537](#537))
([542779f](542779f))
* **bindings:** aggregate cached public keys by validator index
([#397](#397))
([2f90603](2f90603))
* **bindings:** align `BeaconStateView` with `IBeaconStateView`
([#347](#347))
([b8ec273](b8ec273))
* **bindings:** configurable pubkey cache growth step
([#481](#481))
([133ef24](133ef24))
* **bindings:** expose more APIs for STF
([#444](#444))
([7fe2609](7fe2609))
* **bls:** add small MSM for npoints &lt; 32
([#393](#393))
([b430638](b430638))
* **blst:** use external buffers for blst operations
([#358](#358))
([78e4678](78e4678))
* **ci:** conditionally publish bindings with tag
([#355](#355))
([ea77919](ea77919))
* **clock:** add clock module for slot/epoch timing
([#354](#354))
([385b077](385b077))
* **fork_choice:** add Prometheus metrics module
([#309](#309))
([cbc9d8d](cbc9d8d))
* **forkchoice:** implement the forkchoice module
([#246](#246))
([7c62a9b](7c62a9b))
* getSyncCommitteesWitness
([#367](#367))
([ef77649](ef77649))
* implement `loadState` API and binding
([#165](#165))
([f903519](f903519)),
closes [#159](#159)
* **metrics:** metrics bindings
([#455](#455))
([dd41999](dd41999))
* migrate blst,pubkeys to use zapi js dsl
([#331](#331))
([fcd26ca](fcd26ca))
* **pubkeys:** add getPubkeyBytes binding
([#555](#555))
([4ca51cf](4ca51cf))
* publish ARM64 musl bindings
([#482](#482))
([ac764c9](ac764c9))
* **shuffle:** add swap-or-not shuffling module and binding
([#559](#559))
([c2db37c](c2db37c))
* split nextValue fn
([#464](#464))
([b47faeb](b47faeb))
* support getLatestWeakSubjectivityCheckpointEpoch
([#366](#366))
([dcf3883](dcf3883))
* update fulu deposit processing
([#442](#442))
([064335c](064335c))


### Bug Fixes

* avoid set ([#484](#484))
([2e25d97](2e25d97))
* better generation of rand scalar
([#388](#388))
([74dce77](74dce77))
* **bindings:** accept `dontTransferCache` in processSlots for backward
compatibility
([#460](#460))
([65df5af](65df5af))
* **bindings:** check signature infinity by default
([#509](#509))
([2f5f281](2f5f281))
* **bindings:** clean up failed async BLS work
([#527](#527))
([1111b00](1111b00))
* **bindings:** free metrics writer on scrape failure
([#529](#529))
([4c8d94a](4c8d94a))
* **bindings:** harden random aggregate scalars
([#528](#528))
([8e89a63](8e89a63))
* **bindings:** log level for missing fields
([#435](#435))
([08faf41](08faf41))
* **bindings:** misordering of print for cpu count
([#381](#381))
([752a972](752a972))
* **bindings:** populate epoch participation for test fixtures
([#436](#436))
([8dbdd2e](8dbdd2e))
* **bindings:** refcount Pool to fix teardown panic
([#352](#352))
([23b2f68](23b2f68))
* **bindings:** roll back partial N-API initialization
([#491](#491))
([31c5ebb](31c5ebb))
* **bindings:** size BLS thread pool by cgroup-aware CPU count
([#386](#386))
([3ae9522](3ae9522))
* **bindings:** validate class types before unwrap
([#514](#514))
([2fd2ad5](2fd2ad5))
* **bindings:** validate secret key hex length
([#517](#517))
([136e415](136e415))
* **bls:** align PublicKey.uncompress validation with
Signature.uncompress
([#508](#508))
([5a8dbe9](5a8dbe9))
* **bls:** bound randomized aggregation inputs
([#548](#548))
([779d0bf](779d0bf)),
closes [#542](#542)
* **bls:** clean up partial thread pool initialization
([#490](#490))
([d55e598](d55e598))
* **bls:** convert pippenger scratch bytes to element counts
([#513](#513))
([a12ca92](a12ca92))
* **bls:** enforce 32-byte signing roots
([#545](#545))
([72fd308](72fd308))
* **bls:** make batch cardinality structural
([#547](#547))
([a06d8b2](a06d8b2))
* **bls:** preserve aggregate outputs on failure
([#521](#521))
([e0b6dd1](e0b6dd1))
* **bls:** reject empty keygen salts
([#524](#524))
([d2a9c86](d2a9c86))
* **bls:** reject unknown BLST error codes
([#525](#525))
([9e4a6ad](9e4a6ad))
* **bls:** size pairing buffers for 32-bit targets
([#531](#531))
([dc64a27](dc64a27))
* **blst:** default signature infinity check to true if not provided
([#387](#387))
([021cdcb](021cdcb))
* **build:** remove `zig-out` from `files`
([#360](#360))
([c52af09](c52af09))
* **ci:** fix caching spec test version
([#439](#439))
([96885a1](96885a1))
* dangling state pointer in loadOtherState
([#450](#450))
([81cbd5f](81cbd5f))
* **epoch_cache:** compute missing `next_proposers`
([#447](#447))
([0088a29](0088a29))
* **epoch_cache:** populate decision roots in afterProcessEpoch
([#453](#453))
([4b70a5e](4b70a5e))
* export asyncAggregateWithRandomness through napi binding
([#371](#371))
([1d04c2b](1d04c2b))
* harden memory safety across PMT, SSZ tree views, and state transition
([#377](#377))
([d6f5897](d6f5897))
* improve atomic ordering in ThreadPool and NAPI init
([#310](#310))
([4b0a1cc](4b0a1cc))
* interface compatbility with NativeBeaconStateView
([#445](#445))
([89e13d1](89e13d1))
* missing deinits in loadOtherState
([#459](#459))
([094d278](094d278))
* missing state commits
([#454](#454))
([a432b55](a432b55))
* no-op when syncPubkeys run on a pk cache with shrinking validator set
([#432](#432))
([ed05a99](ed05a99))
* param order in BeaconBlockBody
([#348](#348))
([d8b9c06](d8b9c06))
* pendingConsolidations bindings
([#449](#449))
([b9c497e](b9c497e))
* **pmt,ssz:** harden chunked-leaf and zero-copy tree-view memory safety
([#400](#400))
([de50c53](de50c53))
* populate cache balances during rewards/penalties processing
([#474](#474))
([5bf23dc](5bf23dc))
* re-expose sizes
([#369](#369))
([64b81f3](64b81f3))
* remove `slashValidator` gating on active status
([#448](#448))
([d319a0d](d319a0d))
* **ssz:** drop redundant default-init pass in fixed-list decode
([#468](#468))
([0c757be](0c757be))
* **ssz:** publish child cache entries after lookup
([#565](#565))
([21e78c9](21e78c9))
* state transition binding exports
([#456](#456))
([895982c](895982c))
* **state-transition:** group-check signature sets
([#515](#515))
([42774e9](42774e9)),
closes [#502](#502)
* **state-transition:** isolate epoch step cache mutations
([#535](#535))
([a83741a](a83741a))
* **state-transition:** repair Pool.init call broken by
[#346](https://github.com/ChainSafe/lodestar-z/issues/346)×[#367](https://github.com/ChainSafe/lodestar-z/issues/367)
merge skew ([#394](#394))
([b42944f](b42944f))
* various fixes around config
([#433](#433))
([c4f082c](c4f082c))


### Performance Improvements

* **bindings:** drop TS BLS comparison benches and report benchmarks on
PRs ([#552](#552))
([c909c6f](c909c6f))
* **bls:** add cache-aware signature verifier
([#562](#562))
([063857e](063857e))
* **bls:** bypass worker queue for small batches
([#553](#553))
([3f8a6df](3f8a6df))
* **epoch:** replace AutoHashMap with array lookup in reward/penalty
caches ([#286](#286))
([e4e181b](e4e181b)),
closes [#243](#243)
* **pmt:** chunked-leaf packing for basic lists and container_struct
([#346](#346))
([ba156c4](ba156c4))


### Code Refactoring

* allocate `AsyncAggRandData` in one obj
([#384](#384))
([459750f](459750f))
* **bindings/pubkeys:** simplify allocation strategy for aggregate
([#518](#518))
([b82750f](b82750f))
* **bindings:** rename blst Lifecycle to State
([#516](#516))
([0a9c179](0a9c179))
* **bindings:** use zapi js.io() instead of local io module
([#469](#469))
([2b34cc0](2b34cc0))
* **bindings:** wake only required number of workers
([#383](#383))
([1db57f1](1db57f1))
* **bls:** allocations around VMAS
([#395](#395))
([dfda58c](dfda58c))
* **bls:** clean up bls
([#398](#398))
([e0f3b9b](e0f3b9b))
* **bls:** remove need for tracking results for
verifyMultipleAggregateSignatures
([#389](#389))
([6fe5c3f](6fe5c3f))
* **bls:** remove single-threaded fallback
([#390](#390))
([e057713](e057713))
* **clock:** single public Clock; internalize SlotClock
([#463](#463))
([fbab1fa](fbab1fa))
* make XXXDecisionRoot fns return `js.String`
([#342](#342))
([aef4420](aef4420))
* move shuffle into swap_or_not_shuffle module
([#558](#558))
([e56efb2](e56efb2))
* **pubkeys:** centralize the process-wide cache
([#522](#522))
([dc9669d](dc9669d))


### Miscellaneous Chores

* avoid slow tests in AGENTS.md
([#546](#546))
([c60f2a9](c60f2a9))
* bump zapi to include musl build
([#485](#485))
([0b488cc](0b488cc))
* **ci:** pin github actions with sha hashes
([#507](#507))
([167b8f5](167b8f5))
* deprecate unused blst APIs
([#575](#575))
([7b547fa](7b547fa))
* **deps:** bump zapi v2.1.0 -&gt; v2.2.0
([#376](#376))
([0c240d8](0c240d8))
* **deps:** bump zbuild
([#403](#403))
([e2545de](e2545de))
* **deps:** compile blst with ReleaseFast
([#391](#391))
([753a896](753a896))
* **deps:** update zapi to 3.1.0
([#483](#483))
([f3e5827](f3e5827))
* **deps:** use zapi v2.1.0
([#372](#372))
([88f403a](88f403a))
* disable gemini auto code review
([#382](#382))
([63e42a4](63e42a4)),
closes [#380](#380)
* **docs:** add comments section in AGENTS.md
([#566](#566))
([0c09750](0c09750))
* move state clones out of benchmark run functions
([#324](#324))
([e4035de](e4035de))
* prepare 1.0.0 release
([#576](#576))
([20b657b](20b657b))
* release v0.1.2-rc.3
([#370](#370))
([e4fc551](e4fc551))
* **release:** 0.1.2-rc.2
([#365](#365))
([7046128](7046128))
* **release:** v0.1.2-rc.10
([#477](#477))
([9a4fad5](9a4fad5))
* **release:** v0.1.2-rc.4
([#373](#373))
([09468f1](09468f1))
* **release:** v0.1.2-rc.5
([#374](#374))
([f344efa](f344efa))
* **release:** v0.1.2-rc.6
([#375](#375))
([bdf5b67](bdf5b67))
* **release:** v0.1.2-rc.8
([#401](#401))
([06f91c2](06f91c2))
* **release:** v0.1.2-rc.9
([#404](#404))
([6024800](6024800))
* remove merge transition code
([#359](#359))
([09b175d](09b175d))
* remove stale epoch cache TODOs
([#534](#534))
([27a547a](27a547a))
* rename era shortHistoricalRoot to shortEraRoot
([#473](#473))
([c75a4d3](c75a4d3))
* **scripts:** build bindings with preset
([#434](#434))
([a1b5ef7](a1b5ef7))
* silence debug log when used in release builds
([#486](#486))
([c5377d7](c5377d7))
* support dev workflow
([#364](#364))
([fcb9a78](fcb9a78))
* update gloas types to align with the latest specs
([#431](#431))
([1f065b5](1f065b5))
* update spec test version to v1.7.0-alpha.11
([#451](#451))
([5875660](5875660))
* update spec-test-version: v1.6.0-beta.2 -&gt; v1.7.0-alpha.10
([#441](#441))
([f932b1c](f932b1c))
* update zapi to 4.0.0
([#571](#571))
([de8e3fd](de8e3fd))


### Documentation

* document security threat model
([#557](#557))
([e678b87](e678b87))
* more comprehensive AGENTS.md
([#520](#520))
([c74b386](c74b386))
* **pkix:** document load provenance requirement
([#556](#556))
([37e0aa2](37e0aa2))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants