Skip to content

test(spec): assert full post-state root against fixture postStateRoot#512

Draft
MegaRedHand wants to merge 4 commits into
mainfrom
test/leanspec-post-state-root-check
Draft

test(spec): assert full post-state root against fixture postStateRoot#512
MegaRedHand wants to merge 4 commits into
mainfrom
test/leanspec-post-state-root-check

Conversation

@MegaRedHand

Copy link
Copy Markdown
Collaborator

What

Turns the STF fixtures' postStateRoot field into an actual assertion instead of a parse-only placeholder.

Why

The STF spec-test runner captured postStateRoot only to satisfy deny_unknown_fields. The per-field post checks pin only the fields a fixture chooses to enumerate, leaving any other state field unverified. Comparing the full post-state hash_tree_root against the fixture's postStateRoot pins the entire state on every successful transition.

Changes

File Change
state_transition/tests/stf_spectests.rs On a successful STF, assert post_state.hash_tree_root() == postStateRoot.
state_transition/tests/types.rs Drop #[allow(dead_code)] on post_state_root; document its new role.
net/rpc/src/test_driver.rs Expose the full post-state root in the Hive state_transition/run response.

Hive test driver

The driver doesn't assert; it reports a post-state summary and the simulator compares. It previously reported latest_block_header.state_root (a state field), never the SSZ root of the whole post-state, so the simulator had no way to check postStateRoot. The response now includes postStateRoot = state.hash_tree_root(). Additive and forward-compatible (Go's default JSON decoding ignores unknown fields).

Testing

  • cargo test -p ethlambda-state-transition --release --test stf_spectests -> 71 passed (59 fixtures exercise the new check).
  • Verified the check is live by inverting the comparison (dozens of fixtures failed with matching expected/got roots), then reverted.
  • cargo fmt --all, cargo clippy -p ethlambda-rpc -p ethlambda-state-transition --all-targets clean.

Based on test/leanspec-fixture-check-hardening; targets that branch.

…ranch (leanSpec #1179)

leanSpec PR #1179 (commit 9033157c7) added a HEAD_NOT_DESCENDANT_OF_FINALIZED
rejection rule to attestation validation. Fork choice only ever descends from
the latest finalized block, so a vote whose head sits on a branch orphaned by
finalization carries no fork-choice weight. Without this rule a vote that
finalization pruning has already dropped can be re-admitted when the same
aggregate is re-gossiped, instead of being rejected.

ethlambda's shared attestation validation (validate_attestation_data, used by
both on_gossip_attestation and on_gossip_aggregated_attestation_core) ran
availability, topology, consistency, ancestry, and timing checks but never
verified the head descends from the finalized block. Add that check mirroring
the spec: after the target-ancestor-of-head check, reuse the existing
checkpoint_is_ancestor helper against store.latest_finalized(). This is sound
because the store re-derives the finalized checkpoint from the head on each
update, so it is always an ancestor of the head.

Fixes the failing fork-choice spec test
test_re_gossip_of_pruned_orphaned_vote_is_rejected.
…(leanSpec #1189)

The spec-test fixture types silently dropped any JSON field they did not
model, so when leanSpec adds a fixture check the client keeps passing while
verifying nothing new. That masked drift is what let PR #1189's
`canonicalEquivocationHeadAmong` (and a dozen other fields already shipped)
land in the 07-11 fixtures without ethlambda asserting any of them.

Add `#[serde(deny_unknown_fields)]` to every fixture-facing struct so future
schema drift fails loudly at parse time instead of passing unnoticed, and
model every field the current fixtures carry (previously-dropped ones include
`_info.keySetDigest`, top-level `proofSetting`/`rejectionReason`/`postStateRoot`,
per-step `storeSnapshot`/`rejectionReason`, `attestationChecks[].sourceRootLabel`,
and the whole batch of new `StoreChecks` fields). The fixture types are a
production dependency of the RPC Hive test-driver, so this also hardens the
binary's parsing of simulator-delivered JSON; the driver returns a store
snapshot and applies no `StoreChecks` itself, so no driver logic changes.

Assert the checks that map onto the offline runner's store view:

- `canonicalEquivocationHeadAmong` (leanSpec #1189): the head must sit on the
  fork whose targeting attestation in the accepted pool carries the largest
  `hash_tree_root`. Scheme-independent, mirroring `lexicographicHeadAmong`.
- `latestKnownAggregatedTargetSlots`, `attestationTargetRootLabel`,
  `attestationChecks[].sourceRootLabel`, `labelsInStore`, `reorgDepth`,
  `blockAttestations`, `blockAttestationCount`.
- Seed the block registry with the anchor under the label "genesis", matching
  leanSpec's harness, so label-based checks resolve.

`latestNewAggregatedTargetSlots`, `attestationSignatureTargetSlots`,
`newPoolProofParticipants`, `storeSnapshot` and `filledBlockRootLabel` are
parsed but not asserted (each has a TODO): the offline runner advances ticks
without the aggregator role and folds block-borne votes straight into the
known pool, so the pending/raw-signature pools and built-block identity do not
track leanSpec's harness. The accepted (known) pool, which the runner does
populate, is asserted.

Stacked on #510 (the HEAD_NOT_DESCENDANT_OF_FINALIZED fix); retarget to main
once that merges.
The STF spec-test runner captured `postStateRoot` only to satisfy
deny_unknown_fields; the per-field `post` checks left any state field
they don't enumerate unpinned. Assert the full post-state
hash_tree_root against the fixture's postStateRoot so the whole state is
verified on every successful transition.

Mirror the same data in the Hive test driver: expose the full post-state
root in the state_transition/run response so the simulator can perform
the equivalent whole-state check. It previously reported only
latest_block_header.state_root (a state field), never the SSZ root of
the entire post-state.
Base automatically changed from test/leanspec-fixture-check-hardening to main July 14, 2026 19:52
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.

1 participant