test(spec): assert full post-state root against fixture postStateRoot#512
Draft
MegaRedHand wants to merge 4 commits into
Draft
test(spec): assert full post-state root against fixture postStateRoot#512MegaRedHand wants to merge 4 commits into
MegaRedHand wants to merge 4 commits into
Conversation
…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.
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
Turns the STF fixtures'
postStateRootfield into an actual assertion instead of a parse-only placeholder.Why
The STF spec-test runner captured
postStateRootonly to satisfydeny_unknown_fields. The per-fieldpostchecks pin only the fields a fixture chooses to enumerate, leaving any other state field unverified. Comparing the full post-statehash_tree_rootagainst the fixture'spostStateRootpins the entire state on every successful transition.Changes
state_transition/tests/stf_spectests.rspost_state.hash_tree_root() == postStateRoot.state_transition/tests/types.rs#[allow(dead_code)]onpost_state_root; document its new role.net/rpc/src/test_driver.rsstate_transition/runresponse.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 checkpostStateRoot. The response now includespostStateRoot=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).cargo fmt --all,cargo clippy -p ethlambda-rpc -p ethlambda-state-transition --all-targetsclean.Based on
test/leanspec-fixture-check-hardening; targets that branch.