Skip to content

Commit 994eda5

Browse files
authored
test: add new SSZ spec-tests runner (#282)
This PR adds a test runner for the new SSZ spec-test format. It also adds some new post-state checks to the STF spec-test runner.
1 parent 775a061 commit 994eda5

10 files changed

Lines changed: 494 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ docker-build: ## 🐳 Build the Docker image
2424
-t ghcr.io/lambdaclass/ethlambda:$(DOCKER_TAG) .
2525
@echo
2626

27-
LEAN_SPEC_COMMIT_HASH:=45e87fd8a56ac3849ae25906e96960cc116f8d81
27+
# 2026-04-14
28+
LEAN_SPEC_COMMIT_HASH:=76d4792ecd9d5bbcab60bfb022b72b590946b511
2829

2930
leanSpec:
3031
git clone https://github.com/leanEthereum/leanSpec.git --single-branch

crates/blockchain/state_transition/tests/stf_spectests.rs

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
use std::collections::HashMap;
12
use std::path::Path;
23

34
use ethlambda_state_transition::state_transition;
4-
use ethlambda_types::{block::Block, state::State};
5+
use ethlambda_types::{
6+
block::Block,
7+
primitives::{H256, HashTreeRoot as _},
8+
state::State,
9+
};
510

611
use crate::types::PostState;
712

@@ -24,8 +29,13 @@ fn run(path: &Path) -> datatest_stable::Result<()> {
2429
let mut pre_state: State = test.pre.into();
2530
let mut result = Ok(());
2631

27-
for block in test.blocks {
32+
// Build a block registry mapping "block_N" labels to hash tree roots.
33+
// Labels are 1-indexed: "block_1" is the first block in the array.
34+
let mut block_registry: HashMap<String, H256> = HashMap::new();
35+
for (i, block) in test.blocks.into_iter().enumerate() {
2836
let block: Block = block.into();
37+
let label = format!("block_{}", i + 1);
38+
block_registry.insert(label, block.hash_tree_root());
2939
result = state_transition(&mut pre_state, &block);
3040
if result.is_err() {
3141
break;
@@ -34,7 +44,7 @@ fn run(path: &Path) -> datatest_stable::Result<()> {
3444
let post_state = pre_state;
3545
match (result, test.post) {
3646
(Ok(_), Some(expected_post)) => {
37-
compare_post_states(&post_state, &expected_post)?;
47+
compare_post_states(&post_state, &expected_post, &block_registry)?;
3848
}
3949
(Ok(_), None) => {
4050
return Err(
@@ -55,9 +65,24 @@ fn run(path: &Path) -> datatest_stable::Result<()> {
5565
Ok(())
5666
}
5767

68+
fn resolve_label(
69+
label: &str,
70+
block_registry: &HashMap<String, H256>,
71+
) -> datatest_stable::Result<H256> {
72+
block_registry.get(label).copied().ok_or_else(|| {
73+
format!(
74+
"label '{}' not found in block registry. Available: {:?}",
75+
label,
76+
block_registry.keys().collect::<Vec<_>>()
77+
)
78+
.into()
79+
})
80+
}
81+
5882
fn compare_post_states(
5983
post_state: &State,
6084
expected_post: &PostState,
85+
block_registry: &HashMap<String, H256>,
6186
) -> datatest_stable::Result<()> {
6287
let PostState {
6388
config_genesis_time,
@@ -77,6 +102,11 @@ fn compare_post_states(
77102
justifications_roots,
78103
justifications_validators,
79104
validator_count,
105+
latest_justified_root_label,
106+
latest_finalized_root_label,
107+
justifications_roots_labels,
108+
justifications_roots_count,
109+
justifications_validators_count,
80110
} = expected_post;
81111
if let Some(config_genesis_time) = config_genesis_time
82112
&& post_state.config.genesis_time != *config_genesis_time
@@ -237,6 +267,57 @@ fn compare_post_states(
237267
.into());
238268
}
239269
}
270+
if let Some(label) = latest_justified_root_label {
271+
let expected = resolve_label(label, block_registry)?;
272+
if post_state.latest_justified.root != expected {
273+
return Err(format!(
274+
"latest_justified.root mismatch (via label '{label}'): expected {expected:?}, got {:?}",
275+
post_state.latest_justified.root
276+
)
277+
.into());
278+
}
279+
}
280+
if let Some(label) = latest_finalized_root_label {
281+
let expected = resolve_label(label, block_registry)?;
282+
if post_state.latest_finalized.root != expected {
283+
return Err(format!(
284+
"latest_finalized.root mismatch (via label '{label}'): expected {expected:?}, got {:?}",
285+
post_state.latest_finalized.root
286+
)
287+
.into());
288+
}
289+
}
290+
if let Some(labels) = justifications_roots_labels {
291+
let expected_roots: Vec<H256> = labels
292+
.iter()
293+
.map(|label| resolve_label(label, block_registry))
294+
.collect::<datatest_stable::Result<Vec<_>>>()?;
295+
let post_roots: Vec<_> = post_state.justifications_roots.iter().copied().collect();
296+
if post_roots != expected_roots {
297+
return Err(format!(
298+
"justifications_roots mismatch (via labels {labels:?}): expected {expected_roots:?}, got {post_roots:?}",
299+
)
300+
.into());
301+
}
302+
}
303+
if let Some(expected_count) = justifications_roots_count {
304+
let count = post_state.justifications_roots.len() as u64;
305+
if count != *expected_count {
306+
return Err(format!(
307+
"justifications_roots count mismatch: expected {expected_count}, got {count}",
308+
)
309+
.into());
310+
}
311+
}
312+
if let Some(expected_count) = justifications_validators_count {
313+
let count = post_state.justifications_validators.len() as u64;
314+
if count != *expected_count {
315+
return Err(format!(
316+
"justifications_validators count mismatch: expected {expected_count}, got {count}",
317+
)
318+
.into());
319+
}
320+
}
240321
Ok(())
241322
}
242323

crates/blockchain/state_transition/tests/types.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,18 @@ pub struct PostState {
8181

8282
#[serde(rename = "validatorCount")]
8383
pub validator_count: Option<u64>,
84+
85+
// Label-based root checks: "block_N" labels resolved to hash_tree_root of the Nth block.
86+
#[serde(rename = "latestJustifiedRootLabel")]
87+
pub latest_justified_root_label: Option<String>,
88+
#[serde(rename = "latestFinalizedRootLabel")]
89+
pub latest_finalized_root_label: Option<String>,
90+
#[serde(rename = "justificationsRootsLabels")]
91+
pub justifications_roots_labels: Option<Vec<String>>,
92+
93+
// Count checks for variable-length collections.
94+
#[serde(rename = "justificationsRootsCount")]
95+
pub justifications_roots_count: Option<u64>,
96+
#[serde(rename = "justificationsValidatorsCount")]
97+
pub justifications_validators_count: Option<u64>,
8498
}

crates/common/test-fixtures/src/lib.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ pub struct TestState {
131131
#[serde(rename = "historicalBlockHashes")]
132132
pub historical_block_hashes: Container<H256>,
133133
#[serde(rename = "justifiedSlots")]
134-
pub justified_slots: Container<u64>,
134+
pub justified_slots: Container<bool>,
135135
pub validators: Container<Validator>,
136136
#[serde(rename = "justificationsRoots")]
137137
pub justifications_roots: Container<H256>,
@@ -154,17 +154,27 @@ impl From<TestState> for State {
154154
.unwrap();
155155
let justifications_roots = SszList::try_from(value.justifications_roots.data).unwrap();
156156

157+
let mut justified_slots = JustifiedSlots::new();
158+
for &b in &value.justified_slots.data {
159+
justified_slots.push(b).unwrap();
160+
}
161+
162+
let mut justifications_validators = JustificationValidators::new();
163+
for &b in &value.justifications_validators.data {
164+
justifications_validators.push(b).unwrap();
165+
}
166+
157167
State {
158168
config: value.config.into(),
159169
slot: value.slot,
160170
latest_block_header: value.latest_block_header.into(),
161171
latest_justified: value.latest_justified.into(),
162172
latest_finalized: value.latest_finalized.into(),
163173
historical_block_hashes,
164-
justified_slots: JustifiedSlots::new(),
174+
justified_slots,
165175
validators,
166176
justifications_roots,
167-
justifications_validators: JustificationValidators::new(),
177+
justifications_validators,
168178
}
169179
}
170180
}

crates/common/types/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,11 @@ libssz-types.workspace = true
2525
serde_json.workspace = true
2626
serde_yaml_ng.workspace = true
2727
rand.workspace = true
28+
ethlambda-test-fixtures.workspace = true
29+
30+
datatest-stable = "0.3.3"
31+
32+
[[test]]
33+
name = "ssz_spectests"
34+
path = "tests/ssz_spectests.rs"
35+
harness = false

crates/common/types/src/attestation.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub struct AttestationData {
3535
}
3636

3737
/// Validator attestation bundled with its signature.
38-
#[derive(Debug, Clone, SszEncode, SszDecode)]
38+
#[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)]
3939
pub struct SignedAttestation {
4040
/// The index of the validator making the attestation.
4141
pub validator_id: u64,
@@ -79,7 +79,7 @@ pub fn validator_indices(bits: &AggregationBits) -> impl Iterator<Item = u64> +
7979
}
8080

8181
/// Aggregated attestation with its signature proof, used for gossip on the aggregation topic.
82-
#[derive(Debug, Clone, SszEncode, SszDecode)]
82+
#[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)]
8383
pub struct SignedAggregatedAttestation {
8484
pub data: AttestationData,
8585
pub proof: AggregatedSignatureProof,

crates/common/types/src/block.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::{
1212
use primitives::HashTreeRoot as _;
1313

1414
/// Envelope carrying a block and its aggregated signatures.
15-
#[derive(Clone, SszEncode, SszDecode)]
15+
#[derive(Clone, SszEncode, SszDecode, HashTreeRoot)]
1616
pub struct SignedBlock {
1717
/// The block being signed.
1818
pub message: Block,
@@ -35,7 +35,7 @@ impl core::fmt::Debug for SignedBlock {
3535
}
3636

3737
/// Signature payload for the block.
38-
#[derive(Clone, SszEncode, SszDecode)]
38+
#[derive(Clone, SszEncode, SszDecode, HashTreeRoot)]
3939
pub struct BlockSignatures {
4040
/// Attestation signatures for the aggregated attestations in the block body.
4141
///
@@ -69,7 +69,7 @@ pub type AttestationSignatures = SszList<AggregatedSignatureProof, 4096>;
6969
/// The proof can verify that all participants signed the same message in the
7070
/// same epoch, using a single verification operation instead of checking
7171
/// each signature individually.
72-
#[derive(Debug, Clone, SszEncode, SszDecode)]
72+
#[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)]
7373
pub struct AggregatedSignatureProof {
7474
/// Bitfield indicating which validators' signatures are included.
7575
pub participants: AggregationBits,

0 commit comments

Comments
 (0)