Skip to content

Commit c424116

Browse files
committed
test: add "ssz" spec-test runner
1 parent b41971b commit c424116

File tree

6 files changed

+686
-5
lines changed

6 files changed

+686
-5
lines changed

Cargo.lock

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

crates/common/types/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ libssz-types.workspace = true
2525
serde_json.workspace = true
2626
serde_yaml_ng.workspace = true
2727
rand.workspace = true
28+
29+
datatest-stable = "0.3.3"
30+
31+
[[test]]
32+
name = "ssz_spectests"
33+
path = "tests/ssz_spectests.rs"
34+
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,
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use std::path::Path;
2+
3+
use ethlambda_types::primitives::HashTreeRoot;
4+
5+
mod ssz_types;
6+
use ssz_types::{SszTestCase, SszTestVector, decode_hex, decode_hex_h256};
7+
8+
const SUPPORTED_FIXTURE_FORMAT: &str = "ssz";
9+
10+
fn run(path: &Path) -> datatest_stable::Result<()> {
11+
let tests = SszTestVector::from_file(path)?;
12+
13+
for (name, test) in tests.tests {
14+
if test.info.fixture_format != SUPPORTED_FIXTURE_FORMAT {
15+
return Err(format!(
16+
"Unsupported fixture format: {} (expected {})",
17+
test.info.fixture_format, SUPPORTED_FIXTURE_FORMAT
18+
)
19+
.into());
20+
}
21+
22+
println!("Running SSZ test: {name}");
23+
run_ssz_test(&test)?;
24+
}
25+
Ok(())
26+
}
27+
28+
fn run_ssz_test(test: &SszTestCase) -> datatest_stable::Result<()> {
29+
match test.type_name.as_str() {
30+
// Consensus containers
31+
"Config" => run_typed_test::<ssz_types::Config, ethlambda_types::state::ChainConfig>(test),
32+
"Checkpoint" => {
33+
run_typed_test::<ssz_types::Checkpoint, ethlambda_types::checkpoint::Checkpoint>(test)
34+
}
35+
"BlockHeader" => {
36+
run_typed_test::<ssz_types::BlockHeader, ethlambda_types::block::BlockHeader>(test)
37+
}
38+
"Validator" => {
39+
run_typed_test::<ssz_types::Validator, ethlambda_types::state::Validator>(test)
40+
}
41+
"AttestationData" => run_typed_test::<
42+
ssz_types::AttestationData,
43+
ethlambda_types::attestation::AttestationData,
44+
>(test),
45+
"Attestation" => run_typed_test::<
46+
ssz_types::Attestation,
47+
ethlambda_types::attestation::Attestation,
48+
>(test),
49+
"AggregatedAttestation" => run_typed_test::<
50+
ssz_types::AggregatedAttestation,
51+
ethlambda_types::attestation::AggregatedAttestation,
52+
>(test),
53+
"BlockBody" => {
54+
run_typed_test::<ssz_types::BlockBody, ethlambda_types::block::BlockBody>(test)
55+
}
56+
"Block" => run_typed_test::<ssz_types::Block, ethlambda_types::block::Block>(test),
57+
"State" => run_typed_test::<ssz_types::TestState, ethlambda_types::state::State>(test),
58+
"SignedAttestation" => run_typed_test::<
59+
ssz_types::SignedAttestation,
60+
ethlambda_types::attestation::SignedAttestation,
61+
>(test),
62+
"SignedBlock" => {
63+
run_typed_test::<ssz_types::SignedBlock, ethlambda_types::block::SignedBlock>(test)
64+
}
65+
"BlockSignatures" => run_typed_test::<
66+
ssz_types::BlockSignatures,
67+
ethlambda_types::block::BlockSignatures,
68+
>(test),
69+
"AggregatedSignatureProof" => run_typed_test::<
70+
ssz_types::AggregatedSignatureProof,
71+
ethlambda_types::block::AggregatedSignatureProof,
72+
>(test),
73+
"SignedAggregatedAttestation" => run_typed_test::<
74+
ssz_types::SignedAggregatedAttestation,
75+
ethlambda_types::attestation::SignedAggregatedAttestation,
76+
>(test),
77+
78+
// Unsupported types: skip with a message
79+
other => {
80+
println!(" Skipping unsupported type: {other}");
81+
Ok(())
82+
}
83+
}
84+
}
85+
86+
/// Run an SSZ test for a given fixture type `F` that converts into domain type `D`.
87+
///
88+
/// Tests:
89+
/// 1. JSON value deserializes into fixture type and converts to domain type
90+
/// 2. SSZ encoding matches expected serialized bytes
91+
/// 3. SSZ decoding from expected bytes re-encodes identically (round-trip)
92+
/// 4. Hash tree root matches expected root
93+
fn run_typed_test<F, D>(test: &SszTestCase) -> datatest_stable::Result<()>
94+
where
95+
F: serde::de::DeserializeOwned + Into<D>,
96+
D: libssz::SszEncode + libssz::SszDecode + HashTreeRoot,
97+
{
98+
let expected_bytes = decode_hex(&test.serialized)
99+
.map_err(|e| format!("Failed to decode serialized hex: {e}"))?;
100+
let expected_root =
101+
decode_hex_h256(&test.root).map_err(|e| format!("Failed to decode root hex: {e}"))?;
102+
103+
// Step 1: Deserialize JSON value into fixture type, then convert to domain type
104+
let fixture_value: F = serde_json::from_value(test.value.clone())
105+
.map_err(|e| format!("Failed to deserialize value: {e}"))?;
106+
let domain_value: D = fixture_value.into();
107+
108+
// Step 2: SSZ encode and compare with expected serialized bytes
109+
let encoded = <D as libssz::SszEncode>::to_ssz(&domain_value);
110+
if encoded != expected_bytes {
111+
return Err(format!(
112+
"SSZ encoding mismatch for {}:\n expected: 0x{}\n got: 0x{}",
113+
test.type_name,
114+
hex::encode(&expected_bytes),
115+
hex::encode(&encoded),
116+
)
117+
.into());
118+
}
119+
120+
// Step 3: SSZ decode from expected bytes and re-encode (round-trip)
121+
let decoded = D::from_ssz_bytes(&expected_bytes)
122+
.map_err(|e| format!("SSZ decode failed for {}: {e:?}", test.type_name))?;
123+
let re_encoded = <D as libssz::SszEncode>::to_ssz(&decoded);
124+
if re_encoded != expected_bytes {
125+
return Err(format!(
126+
"SSZ round-trip mismatch for {}:\n expected: 0x{}\n got: 0x{}",
127+
test.type_name,
128+
hex::encode(&expected_bytes),
129+
hex::encode(&re_encoded),
130+
)
131+
.into());
132+
}
133+
134+
// Step 4: Verify hash tree root
135+
let computed_root = HashTreeRoot::hash_tree_root(&domain_value);
136+
if computed_root != expected_root {
137+
return Err(format!(
138+
"Hash tree root mismatch for {}:\n expected: {expected_root}\n got: {computed_root}",
139+
test.type_name,
140+
)
141+
.into());
142+
}
143+
144+
Ok(())
145+
}
146+
147+
datatest_stable::harness!({
148+
test = run,
149+
root = "../../../leanSpec/fixtures/consensus/ssz",
150+
pattern = r".*\.json"
151+
});

0 commit comments

Comments
 (0)