From 8b113c98cebb0572769def1b559e2dd1b1ffaff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:19:37 -0300 Subject: [PATCH 1/2] test(spec): assert full post-state root against fixture postStateRoot 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. --- .../state_transition/tests/stf_spectests.rs | 14 ++++++++++++++ crates/blockchain/state_transition/tests/types.rs | 7 +++---- crates/net/rpc/src/test_driver.rs | 8 +++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/blockchain/state_transition/tests/stf_spectests.rs b/crates/blockchain/state_transition/tests/stf_spectests.rs index 8a92fd4e..458cc2c0 100644 --- a/crates/blockchain/state_transition/tests/stf_spectests.rs +++ b/crates/blockchain/state_transition/tests/stf_spectests.rs @@ -53,6 +53,20 @@ fn run(path: &Path) -> datatest_stable::Result<()> { match (result, test.post) { (Ok(_), Some(expected_post)) => { compare_post_states(&post_state, &expected_post, &block_registry)?; + // Hardening: the full post-state hash_tree_root must equal the + // fixture's `postStateRoot`. The per-field `post` checks only + // pin the fields the fixture chose to enumerate; this catches + // any state field they omit. + if let Some(expected_root) = test.post_state_root { + let actual_root = post_state.hash_tree_root(); + if actual_root != expected_root { + return Err(format!( + "Test '{name}' post-state root mismatch: \ + expected {expected_root:?}, got {actual_root:?}" + ) + .into()); + } + } } (Ok(_), None) => { return Err( diff --git a/crates/blockchain/state_transition/tests/types.rs b/crates/blockchain/state_transition/tests/types.rs index 0e766a69..262785b0 100644 --- a/crates/blockchain/state_transition/tests/types.rs +++ b/crates/blockchain/state_transition/tests/types.rs @@ -33,11 +33,10 @@ pub struct StateTransitionTest { pub pre: TestState, pub blocks: Vec, pub post: Option, - /// Expected post-state `hash_tree_root`, present alongside `post`. Captured - /// only so `deny_unknown_fields` accepts it; the per-field `post` checks - /// already pin the post-state. + /// Expected post-state `hash_tree_root`, present alongside `post`. Asserted + /// against the full post-state root after the per-field `post` checks, so + /// any state field those checks don't enumerate is still pinned. #[serde(rename = "postStateRoot")] - #[allow(dead_code)] pub post_state_root: Option, /// Expected rejection reason for negative cases. Captured only so /// `deny_unknown_fields` accepts it; failure is asserted via a missing diff --git a/crates/net/rpc/src/test_driver.rs b/crates/net/rpc/src/test_driver.rs index 913370b4..74867604 100644 --- a/crates/net/rpc/src/test_driver.rs +++ b/crates/net/rpc/src/test_driver.rs @@ -45,7 +45,7 @@ use ethlambda_types::{ }, block::{Block, ByteList512KiB, SingleMessageAggregate}, checkpoint::Checkpoint, - primitives::H256, + primitives::{H256, HashTreeRoot as _}, state::{State, anchor_pair_is_consistent}, }; use serde::{Deserialize, Serialize}; @@ -169,6 +169,11 @@ struct StateTransitionPost { latest_block_header_slot: u64, latest_block_header_state_root: H256, historical_block_hashes_count: usize, + /// Full post-state `hash_tree_root`, mirroring the fixture's `postStateRoot`. + /// `latest_block_header_state_root` above is only the state's recorded + /// header field; this is the SSZ root of the entire post-state, so the hive + /// simulator can assert the whole state, not just the enumerated summary. + post_state_root: H256, } #[derive(Debug, Serialize)] @@ -455,6 +460,7 @@ fn post_summary(state: &State) -> StateTransitionPost { latest_block_header_slot: state.latest_block_header.slot, latest_block_header_state_root: state.latest_block_header.state_root, historical_block_hashes_count: state.historical_block_hashes.len(), + post_state_root: state.hash_tree_root(), } } From 3749a962ecbe09aeda8f30e4a9eed9f7e06fd9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:50:19 -0300 Subject: [PATCH 2/2] test(spec): drop inert post_state_root from hive test driver The Hive lean simulator (simulators/lean/src/scenarios/spec_assets.rs, StateTransitionPost) has no postStateRoot field and does not use deny_unknown_fields, so exposing post_state_root in the state_transition/run response was inert: nothing consumes it. Remove it. The functional whole-state check lives in the offline STF runner. --- crates/net/rpc/src/test_driver.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/net/rpc/src/test_driver.rs b/crates/net/rpc/src/test_driver.rs index 74867604..913370b4 100644 --- a/crates/net/rpc/src/test_driver.rs +++ b/crates/net/rpc/src/test_driver.rs @@ -45,7 +45,7 @@ use ethlambda_types::{ }, block::{Block, ByteList512KiB, SingleMessageAggregate}, checkpoint::Checkpoint, - primitives::{H256, HashTreeRoot as _}, + primitives::H256, state::{State, anchor_pair_is_consistent}, }; use serde::{Deserialize, Serialize}; @@ -169,11 +169,6 @@ struct StateTransitionPost { latest_block_header_slot: u64, latest_block_header_state_root: H256, historical_block_hashes_count: usize, - /// Full post-state `hash_tree_root`, mirroring the fixture's `postStateRoot`. - /// `latest_block_header_state_root` above is only the state's recorded - /// header field; this is the SSZ root of the entire post-state, so the hive - /// simulator can assert the whole state, not just the enumerated summary. - post_state_root: H256, } #[derive(Debug, Serialize)] @@ -460,7 +455,6 @@ fn post_summary(state: &State) -> StateTransitionPost { latest_block_header_slot: state.latest_block_header.slot, latest_block_header_state_root: state.latest_block_header.state_root, historical_block_hashes_count: state.historical_block_hashes.len(), - post_state_root: state.hash_tree_root(), } }