feat(chain): implement fork choice over the consensus store - #32
Open
adust09 wants to merge 1 commit into
Open
Conversation
The state transition answers whether a block is valid; fork choice answers which of the valid chains is the one. This adds the store that question needs — every block above finalization, their post-states, and the votes cast over them — and the decisions that move its head. The store is mutated in place while everything else in the crate is a pure function. It is a long-lived aggregate with one writer, not a value passed between them, and it holds a State per unfinalized block; copying it per imported block would cost O(chain) per block and buy nothing. What the copy guaranteed is kept as a contract instead: an entry point returning Err leaves the store exactly as it found it, stated on each one and tested. leanSpec verifies signatures inside three of these operations. This crate has no cryptographic dependency by design, so each is split at exactly that point and the caller verifies in between. The interval-2 aggregator duty is absent for the same reason. Both seams close with verity-crypto. Conformance replays all 123 fork-choice vectors, 692 steps, against all ten fields of leanSpec's storeSnapshot rather than the shallower checks block — a client whose weights are wrong but whose head happens to land right is the failure that catches and headSlot does not. Eleven vectors are not asserted on their vote pools, in two named lists with their reasons: eight where the generator's proposer pool holds votes the block never carried, and three that turn on the aggregator duty. The JSON shapes the harnesses share move into tests/common.
adust09
force-pushed
the
feat/fork-choice-20260826-1758
branch
from
August 27, 2026 03:01
eca29a9 to
0896e5a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
verity-chainowned the state transition but not the storearchitecture.md:173says it owns. This adds fork choice: theStore, block import, the interval clock, LMD-GHOST head selection, the safe target, gossip vote admission, pruning, and attestation-target selection.Transcribed from leanSpec
src/lean_spec/spec/forks/lstar/fork_choice.py,timeline.py, andvalidator_duties.pyat0588c2d2.Three decisions worth reviewing
The store is mutated in place. Everything else in the crate is a pure function returning a fresh value. The store is not: it is a long-lived aggregate with one writer, and it holds a
Stateper unfinalized block, so copying it per imported block costsO(chain)per block for nothing. What the copy guaranteed is kept as a contract — an entry point returningErrleaves the store byte-for-byte as it found it — stated on each one and covered by unit tests.The cryptography is split out, not stubbed. leanSpec verifies signatures inside three operations. This crate has no cryptographic dependency by design, so each is cut at exactly that point and the caller verifies in between:
on_blockon_blockon_gossip_attestationvalidate_attestation_signer+record_attestation_signatureon_gossip_aggregated_attestationrecord_aggregated_payloadThe interval-2 aggregator duty is absent for the same reason. Both seams close with
verity-crypto. Signatures are held as opaque bytes: the store never interprets them, and fork choice weighs votes by who signed.Conformance asserts the whole snapshot. All 123 vectors, 692 steps, against all ten fields of
storeSnapshotrather than the shallowerchecksblock — leanSpec's own docs say why: block membership is what makes over- and under-pruning observable, and weights must agree "even where two clients agree on the head".checksis asserted too, for the labels, the attestation target, and the block bodies.What is not asserted, and why
Eleven vectors are not asserted on their vote pools. Both lists are named in the harness with the reason, and both fail the run if a listed vector stops diverging:
GENERATOR_POOL_EXCEEDS_WIRE(8) — the generator merges the votes its proposer aggregated into the store before applying the block. A replaying client sees only the block, and a body carries at mostMAX_ATTESTATIONS_DATAdistinct votes; in one vector it carries none while the proposer's pool held three. That state never reached the wire.NEEDS_AGGREGATION(3) — they turn on the interval-2 duty above.Everything else in those vectors — head, checkpoints, block membership, clock, block bodies — is still asserted. Two further steps expect
INVALID_SIGNATURE, which this crate cannot produce; only those steps are skipped, not the vectors.Also
RejectionReasongains the 20 fork-choice and gossip variants; four of leanSpec's 36 remain, from paths this workspace has not reached.tests/common, so the state-transition harness drops ~250 duplicated lines. It still passes unchanged.tar --wildcardstakes thefork_choicetree.Verification
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspace(61 unit tests), pre-commit hooks, and the full fixture job against the pinned production tarball: