From 682fb84e892d59b0b1992dbecbdf1d19a1654ed9 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 16 Feb 2026 13:20:06 -0800 Subject: [PATCH 1/9] chor: add partial sig message encoding test --- anchor/spec_tests/src/lib.rs | 3 ++ anchor/spec_tests/src/types/mod.rs | 3 ++ .../src/types/partial_sig_message_encoding.rs | 50 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 anchor/spec_tests/src/types/partial_sig_message_encoding.rs diff --git a/anchor/spec_tests/src/lib.rs b/anchor/spec_tests/src/lib.rs index 7cb431af3..c5bb71434 100644 --- a/anchor/spec_tests/src/lib.rs +++ b/anchor/spec_tests/src/lib.rs @@ -57,6 +57,9 @@ fn run_types_tests() { "beaconvote.EncodingTest" => { run_test::(&path, &contents) } + "partialsigmessage.EncodingTest" => { + run_test::(&path, &contents) + } // TODO(spec-tests): Add more test types here as they are implemented. // This arm will be replaced with panic!() once all test types are added. diff --git a/anchor/spec_tests/src/types/mod.rs b/anchor/spec_tests/src/types/mod.rs index ea828d034..e08168bcb 100644 --- a/anchor/spec_tests/src/types/mod.rs +++ b/anchor/spec_tests/src/types/mod.rs @@ -1,2 +1,5 @@ mod beacon_vote_encoding; +mod partial_sig_message_encoding; + pub use beacon_vote_encoding::*; +pub use partial_sig_message_encoding::*; diff --git a/anchor/spec_tests/src/types/partial_sig_message_encoding.rs b/anchor/spec_tests/src/types/partial_sig_message_encoding.rs new file mode 100644 index 000000000..14df4974f --- /dev/null +++ b/anchor/spec_tests/src/types/partial_sig_message_encoding.rs @@ -0,0 +1,50 @@ +use serde::Deserialize; +use ssz::{Decode, Encode}; +use tree_hash::TreeHash; +use types::Hash256; + +use crate::{ + SpecTest, + utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, +}; + +/// Mirrors Go's `EncodingTest` for `PartialSignatureMessages`. +/// +/// Validates SSZ encode/decode roundtrip and hash tree root. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct PartialSigMessageEncodingTest { + #[serde(deserialize_with = "deserialize_base64")] + data: Vec, + #[serde(deserialize_with = "deserialize_bytes_to_hash256")] + expected_root: Hash256, +} + +impl SpecTest for PartialSigMessageEncodingTest { + fn run(&self) -> Result<(), String> { + // Decode PartialSignatureMessages from SSZ bytes + let decoded = ssv_types::partial_sig::PartialSignatureMessages::from_ssz_bytes(&self.data) + .map_err(|e| format!("SSZ decode failed: {e:?}"))?; + + // Encode back and verify roundtrip + let encoded = decoded.as_ssz_bytes(); + if encoded != self.data { + return Err(format!( + "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", + encoded.len(), + self.data.len() + )); + } + + // Verify hash tree root + let root = decoded.tree_hash_root(); + if root != self.expected_root { + return Err(format!( + "Hash tree root mismatch: got {root:?}, expected {:?}", + self.expected_root + )); + } + + Ok(()) + } +} From e4bd0c38dd040703f509b7507f12d6924ffba0c5 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 16 Feb 2026 14:53:04 -0800 Subject: [PATCH 2/9] chore: add signed ssv message encoding test --- anchor/spec_tests/src/lib.rs | 3 ++ anchor/spec_tests/src/types/mod.rs | 2 ++ .../src/types/signed_ssv_msg_encoding.rs | 34 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 anchor/spec_tests/src/types/signed_ssv_msg_encoding.rs diff --git a/anchor/spec_tests/src/lib.rs b/anchor/spec_tests/src/lib.rs index c5bb71434..a699f10a9 100644 --- a/anchor/spec_tests/src/lib.rs +++ b/anchor/spec_tests/src/lib.rs @@ -60,6 +60,9 @@ fn run_types_tests() { "partialsigmessage.EncodingTest" => { run_test::(&path, &contents) } + "signedssvmsg.EncodingTest" => { + run_test::(&path, &contents) + } // TODO(spec-tests): Add more test types here as they are implemented. // This arm will be replaced with panic!() once all test types are added. diff --git a/anchor/spec_tests/src/types/mod.rs b/anchor/spec_tests/src/types/mod.rs index e08168bcb..ab12f6182 100644 --- a/anchor/spec_tests/src/types/mod.rs +++ b/anchor/spec_tests/src/types/mod.rs @@ -1,5 +1,7 @@ mod beacon_vote_encoding; mod partial_sig_message_encoding; +mod signed_ssv_msg_encoding; pub use beacon_vote_encoding::*; pub use partial_sig_message_encoding::*; +pub use signed_ssv_msg_encoding::*; diff --git a/anchor/spec_tests/src/types/signed_ssv_msg_encoding.rs b/anchor/spec_tests/src/types/signed_ssv_msg_encoding.rs new file mode 100644 index 000000000..162e7f1c6 --- /dev/null +++ b/anchor/spec_tests/src/types/signed_ssv_msg_encoding.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use ssz::{Decode, Encode}; + +use crate::{SpecTest, utils::deserializers::deserialize_base64}; + +/// Mirrors Go's `EncodingTest` for `SignedSSVMessage`. +/// +/// Validates SSZ encode/decode roundtrip (no tree hash root check). +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct SignedSSVMessageEncodingTest { + #[serde(deserialize_with = "deserialize_base64")] + data: Vec, +} + +impl SpecTest for SignedSSVMessageEncodingTest { + fn run(&self) -> Result<(), String> { + // Decode SignedSSVMessage from SSZ bytes + let decoded = ssv_types::message::SignedSSVMessage::from_ssz_bytes(&self.data) + .map_err(|e| format!("SSZ decode failed: {e:?}"))?; + + // Encode back and verify roundtrip + let encoded = decoded.as_ssz_bytes(); + if encoded != self.data { + return Err(format!( + "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", + encoded.len(), + self.data.len() + )); + } + + Ok(()) + } +} From b9deff3a5601d32d0b28f3ee1eaa0a115f69ab46 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 16 Feb 2026 15:00:18 -0800 Subject: [PATCH 3/9] chore: add ssv message encoding test --- anchor/spec_tests/src/lib.rs | 1 + anchor/spec_tests/src/types/mod.rs | 2 + .../src/types/ssv_message_encoding.rs | 50 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 anchor/spec_tests/src/types/ssv_message_encoding.rs diff --git a/anchor/spec_tests/src/lib.rs b/anchor/spec_tests/src/lib.rs index a699f10a9..0e2dab63c 100644 --- a/anchor/spec_tests/src/lib.rs +++ b/anchor/spec_tests/src/lib.rs @@ -63,6 +63,7 @@ fn run_types_tests() { "signedssvmsg.EncodingTest" => { run_test::(&path, &contents) } + "ssvmsg.EncodingTest" => run_test::(&path, &contents), // TODO(spec-tests): Add more test types here as they are implemented. // This arm will be replaced with panic!() once all test types are added. diff --git a/anchor/spec_tests/src/types/mod.rs b/anchor/spec_tests/src/types/mod.rs index ab12f6182..d0738a577 100644 --- a/anchor/spec_tests/src/types/mod.rs +++ b/anchor/spec_tests/src/types/mod.rs @@ -1,7 +1,9 @@ mod beacon_vote_encoding; mod partial_sig_message_encoding; mod signed_ssv_msg_encoding; +mod ssv_message_encoding; pub use beacon_vote_encoding::*; pub use partial_sig_message_encoding::*; pub use signed_ssv_msg_encoding::*; +pub use ssv_message_encoding::*; diff --git a/anchor/spec_tests/src/types/ssv_message_encoding.rs b/anchor/spec_tests/src/types/ssv_message_encoding.rs new file mode 100644 index 000000000..45d92e0d2 --- /dev/null +++ b/anchor/spec_tests/src/types/ssv_message_encoding.rs @@ -0,0 +1,50 @@ +use serde::Deserialize; +use ssz::{Decode, Encode}; +use tree_hash::TreeHash; +use types::Hash256; + +use crate::{ + SpecTest, + utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, +}; + +/// Mirrors Go's `EncodingTest` for `SSVMessage`. +/// +/// Validates SSZ encode/decode roundtrip and hash tree root. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct SSVMessageEncodingTest { + #[serde(deserialize_with = "deserialize_base64")] + data: Vec, + #[serde(deserialize_with = "deserialize_bytes_to_hash256")] + expected_root: Hash256, +} + +impl SpecTest for SSVMessageEncodingTest { + fn run(&self) -> Result<(), String> { + // Decode SSVMessage from SSZ bytes + let decoded = ssv_types::message::SSVMessage::from_ssz_bytes(&self.data) + .map_err(|e| format!("SSZ decode failed: {e:?}"))?; + + // Encode back and verify roundtrip + let encoded = decoded.as_ssz_bytes(); + if encoded != self.data { + return Err(format!( + "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", + encoded.len(), + self.data.len() + )); + } + + // Verify hash tree root + let root = decoded.tree_hash_root(); + if root != self.expected_root { + return Err(format!( + "Hash tree root mismatch: got {root:?}, expected {:?}", + self.expected_root + )); + } + + Ok(()) + } +} From f8515c98519fc1124c4d008598a1855c31245985 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 16 Feb 2026 15:12:16 -0800 Subject: [PATCH 4/9] chore: add agg committee encoding test --- anchor/spec_tests/src/lib.rs | 3 ++ ...gator_committee_consensus_data_encoding.rs | 52 +++++++++++++++++++ anchor/spec_tests/src/types/mod.rs | 2 + 3 files changed, 57 insertions(+) create mode 100644 anchor/spec_tests/src/types/aggregator_committee_consensus_data_encoding.rs diff --git a/anchor/spec_tests/src/lib.rs b/anchor/spec_tests/src/lib.rs index 0e2dab63c..1195fb1b9 100644 --- a/anchor/spec_tests/src/lib.rs +++ b/anchor/spec_tests/src/lib.rs @@ -54,6 +54,9 @@ fn run_types_tests() { let result = match prefix { // Encoding tests + "aggregatorcommitteeconsensusdata.EncodingTest" => { + run_test::(&path, &contents) + } "beaconvote.EncodingTest" => { run_test::(&path, &contents) } diff --git a/anchor/spec_tests/src/types/aggregator_committee_consensus_data_encoding.rs b/anchor/spec_tests/src/types/aggregator_committee_consensus_data_encoding.rs new file mode 100644 index 000000000..aaaa6c972 --- /dev/null +++ b/anchor/spec_tests/src/types/aggregator_committee_consensus_data_encoding.rs @@ -0,0 +1,52 @@ +use serde::Deserialize; +use ssz::{Decode, Encode}; +use tree_hash::TreeHash; +use types::Hash256; + +use crate::{ + SpecTest, + utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, +}; + +/// Mirrors Go's `EncodingTest` for `AggregatorCommitteeConsensusData`. +/// +/// Validates SSZ encode/decode roundtrip and hash tree root. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct AggregatorCommitteeConsensusDataEncodingTest { + #[serde(deserialize_with = "deserialize_base64")] + data: Vec, + #[serde(deserialize_with = "deserialize_bytes_to_hash256")] + expected_root: Hash256, +} + +impl SpecTest for AggregatorCommitteeConsensusDataEncodingTest { + fn run(&self) -> Result<(), String> { + // Decode AggregatorCommitteeConsensusData from SSZ bytes + let decoded = ssv_types::consensus::AggregatorCommitteeConsensusData::< + types::MainnetEthSpec, + >::from_ssz_bytes(&self.data) + .map_err(|e| format!("SSZ decode failed: {e:?}"))?; + + // Encode back and verify roundtrip + let encoded = decoded.as_ssz_bytes(); + if encoded != self.data { + return Err(format!( + "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", + encoded.len(), + self.data.len() + )); + } + + // Verify hash tree root + let root = decoded.tree_hash_root(); + if root != self.expected_root { + return Err(format!( + "Hash tree root mismatch: got {root:?}, expected {:?}", + self.expected_root + )); + } + + Ok(()) + } +} diff --git a/anchor/spec_tests/src/types/mod.rs b/anchor/spec_tests/src/types/mod.rs index d0738a577..a7ab01a35 100644 --- a/anchor/spec_tests/src/types/mod.rs +++ b/anchor/spec_tests/src/types/mod.rs @@ -1,8 +1,10 @@ +mod aggregator_committee_consensus_data_encoding; mod beacon_vote_encoding; mod partial_sig_message_encoding; mod signed_ssv_msg_encoding; mod ssv_message_encoding; +pub use aggregator_committee_consensus_data_encoding::*; pub use beacon_vote_encoding::*; pub use partial_sig_message_encoding::*; pub use signed_ssv_msg_encoding::*; From 0a3c00c4f9435f47ff4cffe459832cdd5e886c3f Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 16 Feb 2026 15:25:46 -0800 Subject: [PATCH 5/9] chore: add proposer consensus data encoding test --- anchor/spec_tests/src/lib.rs | 3 ++ anchor/spec_tests/src/types/mod.rs | 2 + .../types/proposer_consensus_data_encoding.rs | 50 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 anchor/spec_tests/src/types/proposer_consensus_data_encoding.rs diff --git a/anchor/spec_tests/src/lib.rs b/anchor/spec_tests/src/lib.rs index 1195fb1b9..c77520ed5 100644 --- a/anchor/spec_tests/src/lib.rs +++ b/anchor/spec_tests/src/lib.rs @@ -67,6 +67,9 @@ fn run_types_tests() { run_test::(&path, &contents) } "ssvmsg.EncodingTest" => run_test::(&path, &contents), + "proposerconsensusdata.EncodingTest" => { + run_test::(&path, &contents) + } // TODO(spec-tests): Add more test types here as they are implemented. // This arm will be replaced with panic!() once all test types are added. diff --git a/anchor/spec_tests/src/types/mod.rs b/anchor/spec_tests/src/types/mod.rs index a7ab01a35..f78643cb0 100644 --- a/anchor/spec_tests/src/types/mod.rs +++ b/anchor/spec_tests/src/types/mod.rs @@ -1,11 +1,13 @@ mod aggregator_committee_consensus_data_encoding; mod beacon_vote_encoding; mod partial_sig_message_encoding; +mod proposer_consensus_data_encoding; mod signed_ssv_msg_encoding; mod ssv_message_encoding; pub use aggregator_committee_consensus_data_encoding::*; pub use beacon_vote_encoding::*; pub use partial_sig_message_encoding::*; +pub use proposer_consensus_data_encoding::*; pub use signed_ssv_msg_encoding::*; pub use ssv_message_encoding::*; diff --git a/anchor/spec_tests/src/types/proposer_consensus_data_encoding.rs b/anchor/spec_tests/src/types/proposer_consensus_data_encoding.rs new file mode 100644 index 000000000..09d28a15e --- /dev/null +++ b/anchor/spec_tests/src/types/proposer_consensus_data_encoding.rs @@ -0,0 +1,50 @@ +use serde::Deserialize; +use ssz::{Decode, Encode}; +use tree_hash::TreeHash; +use types::Hash256; + +use crate::{ + SpecTest, + utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, +}; + +/// Mirrors Go's `EncodingTest` for `ProposerConsensusData`. +/// +/// Validates SSZ encode/decode roundtrip and hash tree root. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct ProposerConsensusDataEncodingTest { + #[serde(deserialize_with = "deserialize_base64")] + data: Vec, + #[serde(deserialize_with = "deserialize_bytes_to_hash256")] + expected_root: Hash256, +} + +impl SpecTest for ProposerConsensusDataEncodingTest { + fn run(&self) -> Result<(), String> { + // Decode ProposerConsensusData from SSZ bytes + let decoded = ssv_types::consensus::ProposerConsensusData::from_ssz_bytes(&self.data) + .map_err(|e| format!("SSZ decode failed: {e:?}"))?; + + // Encode back and verify roundtrip + let encoded = decoded.as_ssz_bytes(); + if encoded != self.data { + return Err(format!( + "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", + encoded.len(), + self.data.len() + )); + } + + // Verify hash tree root + let root = decoded.tree_hash_root(); + if root != self.expected_root { + return Err(format!( + "Hash tree root mismatch: got {root:?}, expected {:?}", + self.expected_root + )); + } + + Ok(()) + } +} From a4f509b7c2aafd9e6a3b6e5e9eaf6fa429d1b065 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 16 Feb 2026 15:36:24 -0800 Subject: [PATCH 6/9] chore: skip share encoding test --- anchor/spec_tests/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/anchor/spec_tests/src/lib.rs b/anchor/spec_tests/src/lib.rs index c77520ed5..54f13a1ec 100644 --- a/anchor/spec_tests/src/lib.rs +++ b/anchor/spec_tests/src/lib.rs @@ -71,6 +71,12 @@ fn run_types_tests() { run_test::(&path, &contents) } + // Anchor's `Share` is architecturally different from Go spec's `Share` + // (different fields, decomposed across multiple types). Not applicable. + "share.EncodingTest" => { + continue; + } + // TODO(spec-tests): Add more test types here as they are implemented. // This arm will be replaced with panic!() once all test types are added. _ => { From 70fda6fb4177145f9e18452571690bdf50e809fa Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 23 Feb 2026 14:58:28 -0800 Subject: [PATCH 7/9] refactor: move spec test run to new module --- anchor/spec_tests/src/lib.rs | 91 +------------- anchor/spec_tests/src/runner/mod.rs | 3 + .../spec_tests/src/runner/types_dispatcher.rs | 117 ++++++++++++++++++ 3 files changed, 123 insertions(+), 88 deletions(-) create mode 100644 anchor/spec_tests/src/runner/mod.rs create mode 100644 anchor/spec_tests/src/runner/types_dispatcher.rs diff --git a/anchor/spec_tests/src/lib.rs b/anchor/spec_tests/src/lib.rs index 54f13a1ec..e3c352df5 100644 --- a/anchor/spec_tests/src/lib.rs +++ b/anchor/spec_tests/src/lib.rs @@ -1,12 +1,10 @@ #![cfg(test)] +mod runner; mod types; mod utils; -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::path::Path; use serde::de::DeserializeOwned; @@ -23,95 +21,12 @@ fn run_test(path: &Path, contents: &str) -> Result<(), String> { test.run() } -/// Run all type spec tests from the fixture directory. -/// Dispatches each JSON file to the correct test type based on exact prefix match. -fn run_types_tests() { - let dir: PathBuf = - Path::new(env!("CARGO_MANIFEST_DIR")).join("ssv-spec/types/spectest/generate/tests"); - assert!( - dir.exists(), - "Fixture directory not found: {}", - dir.display() - ); - - let mut failures = Vec::new(); - let mut count = 0; - - for entry in fs::read_dir(&dir).expect("Failed to read fixture directory") { - let entry = entry.expect("Failed to read directory entry"); - let path = entry.path(); - - if path.extension() != Some("json".as_ref()) { - continue; - } - - let filename = path.file_name().unwrap().to_string_lossy().to_string(); - let contents = fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("Failed to read {}: {e}", path.display())); - - // Extract exact type prefix - let prefix = filename.split('_').next().unwrap_or(""); - - let result = match prefix { - // Encoding tests - "aggregatorcommitteeconsensusdata.EncodingTest" => { - run_test::(&path, &contents) - } - "beaconvote.EncodingTest" => { - run_test::(&path, &contents) - } - "partialsigmessage.EncodingTest" => { - run_test::(&path, &contents) - } - "signedssvmsg.EncodingTest" => { - run_test::(&path, &contents) - } - "ssvmsg.EncodingTest" => run_test::(&path, &contents), - "proposerconsensusdata.EncodingTest" => { - run_test::(&path, &contents) - } - - // Anchor's `Share` is architecturally different from Go spec's `Share` - // (different fields, decomposed across multiple types). Not applicable. - "share.EncodingTest" => { - continue; - } - - // TODO(spec-tests): Add more test types here as they are implemented. - // This arm will be replaced with panic!() once all test types are added. - _ => { - eprintln!("SKIP (not yet implemented): {prefix}"); - continue; - } - }; - - count += 1; - if let Err(e) = result { - failures.push(format!(" {filename}: {e}")); - } - } - - assert!( - count > 0, - "No type spec test fixtures found in {}", - dir.display() - ); - - if !failures.is_empty() { - panic!( - "\n{} of {count} type spec tests failed:\n{}", - failures.len(), - failures.join("\n") - ); - } -} - #[cfg(test)] mod spec_tests { use super::*; #[test] fn types_spec_tests() { - run_types_tests(); + runner::run_all_type_fixtures(); } } diff --git a/anchor/spec_tests/src/runner/mod.rs b/anchor/spec_tests/src/runner/mod.rs new file mode 100644 index 000000000..ab40aab6b --- /dev/null +++ b/anchor/spec_tests/src/runner/mod.rs @@ -0,0 +1,3 @@ +mod types_dispatcher; + +pub use types_dispatcher::run_all_type_fixtures; diff --git a/anchor/spec_tests/src/runner/types_dispatcher.rs b/anchor/spec_tests/src/runner/types_dispatcher.rs new file mode 100644 index 000000000..d7b02186b --- /dev/null +++ b/anchor/spec_tests/src/runner/types_dispatcher.rs @@ -0,0 +1,117 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use crate::{run_test, types}; + +/// Dispatch a single fixture file to its test type based on the exact prefix before the first `_`. +/// +/// Returns `Some(result)` if the prefix matched an implemented test type, +/// or `None` if the fixture was skipped (known-inapplicable or not-yet-implemented). +fn dispatch_fixture_by_prefix( + prefix: &str, + path: &Path, + contents: &str, + skipped_known_count: &mut usize, + skipped_unknown_count: &mut usize, +) -> Option> { + match prefix { + // Encoding tests + "aggregatorcommitteeconsensusdata.EncodingTest" => Some(run_test::< + types::AggregatorCommitteeConsensusDataEncodingTest, + >(path, contents)), + "beaconvote.EncodingTest" => { + Some(run_test::(path, contents)) + } + "partialsigmessage.EncodingTest" => Some(run_test::( + path, contents, + )), + "signedssvmsg.EncodingTest" => Some(run_test::( + path, contents, + )), + "ssvmsg.EncodingTest" => Some(run_test::(path, contents)), + "proposerconsensusdata.EncodingTest" => Some(run_test::< + types::ProposerConsensusDataEncodingTest, + >(path, contents)), + + // Anchor's `Share` is architecturally different from Go spec's `Share` + // (different fields, decomposed across multiple types). Not applicable. + "share.EncodingTest" => { + *skipped_known_count += 1; + None + } + + // TODO(spec-tests): Add more test types here as they are implemented. + // This arm will be replaced with panic!() once all test types are added. + _ => { + eprintln!("SKIP (not yet implemented): {prefix}"); + *skipped_unknown_count += 1; + None + } + } +} + +/// Run all type spec tests from the fixture directory. +/// +/// Iterates over every `.json` fixture, dispatches each to the appropriate test type, +/// and reports failures at the end. +pub fn run_all_type_fixtures() { + let dir: PathBuf = + Path::new(env!("CARGO_MANIFEST_DIR")).join("ssv-spec/types/spectest/generate/tests"); + assert!( + dir.exists(), + "Fixture directory not found: {}", + dir.display() + ); + + let mut failures = Vec::new(); + let mut executed_count = 0; + let mut skipped_known_count = 0; + let mut skipped_unknown_count = 0; + + for entry in fs::read_dir(&dir).expect("Failed to read fixture directory") { + let entry = entry.expect("Failed to read directory entry"); + let path = entry.path(); + + if path.extension() != Some("json".as_ref()) { + continue; + } + + let filename = path.file_name().unwrap().to_string_lossy().to_string(); + let contents = fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("Failed to read {}: {e}", path.display())); + + // Extract exact type prefix + let prefix = filename.split('_').next().unwrap_or(""); + + let Some(result) = dispatch_fixture_by_prefix( + prefix, + &path, + &contents, + &mut skipped_known_count, + &mut skipped_unknown_count, + ) else { + continue; + }; + + executed_count += 1; + if let Err(e) = result { + failures.push(format!(" {filename}: {e}")); + } + } + + assert!( + executed_count > 0, + "No type spec test fixtures found in {}", + dir.display() + ); + + if !failures.is_empty() { + panic!( + "\n{} of {executed_count} type spec tests failed:\n{}", + failures.len(), + failures.join("\n") + ); + } +} From 90c52445b3e7835dee316e9e07139dce99bca286 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 23 Feb 2026 15:18:21 -0800 Subject: [PATCH 8/9] refactor: make spec test encoding helpers --- ...gator_committee_consensus_data_encoding.rs | 36 ++++------------- .../src/types/beacon_vote_encoding.rs | 35 ++++------------ .../src/types/partial_sig_message_encoding.rs | 35 ++++------------ .../types/proposer_consensus_data_encoding.rs | 35 ++++------------ .../src/types/signed_ssv_msg_encoding.rs | 22 +++------- .../src/types/ssv_message_encoding.rs | 32 +++------------ .../spec_tests/src/utils/encoding_helpers.rs | 40 +++++++++++++++++++ anchor/spec_tests/src/utils/mod.rs | 3 ++ 8 files changed, 84 insertions(+), 154 deletions(-) create mode 100644 anchor/spec_tests/src/utils/encoding_helpers.rs diff --git a/anchor/spec_tests/src/types/aggregator_committee_consensus_data_encoding.rs b/anchor/spec_tests/src/types/aggregator_committee_consensus_data_encoding.rs index aaaa6c972..fddd67698 100644 --- a/anchor/spec_tests/src/types/aggregator_committee_consensus_data_encoding.rs +++ b/anchor/spec_tests/src/types/aggregator_committee_consensus_data_encoding.rs @@ -1,11 +1,12 @@ use serde::Deserialize; -use ssz::{Decode, Encode}; -use tree_hash::TreeHash; use types::Hash256; use crate::{ SpecTest, - utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + utils::{ + check_roundtrip_with_root, + deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + }, }; /// Mirrors Go's `EncodingTest` for `AggregatorCommitteeConsensusData`. @@ -22,31 +23,8 @@ pub struct AggregatorCommitteeConsensusDataEncodingTest { impl SpecTest for AggregatorCommitteeConsensusDataEncodingTest { fn run(&self) -> Result<(), String> { - // Decode AggregatorCommitteeConsensusData from SSZ bytes - let decoded = ssv_types::consensus::AggregatorCommitteeConsensusData::< - types::MainnetEthSpec, - >::from_ssz_bytes(&self.data) - .map_err(|e| format!("SSZ decode failed: {e:?}"))?; - - // Encode back and verify roundtrip - let encoded = decoded.as_ssz_bytes(); - if encoded != self.data { - return Err(format!( - "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", - encoded.len(), - self.data.len() - )); - } - - // Verify hash tree root - let root = decoded.tree_hash_root(); - if root != self.expected_root { - return Err(format!( - "Hash tree root mismatch: got {root:?}, expected {:?}", - self.expected_root - )); - } - - Ok(()) + check_roundtrip_with_root::< + ssv_types::consensus::AggregatorCommitteeConsensusData, + >(&self.data, self.expected_root) } } diff --git a/anchor/spec_tests/src/types/beacon_vote_encoding.rs b/anchor/spec_tests/src/types/beacon_vote_encoding.rs index 6c93faa5c..c9458ccbe 100644 --- a/anchor/spec_tests/src/types/beacon_vote_encoding.rs +++ b/anchor/spec_tests/src/types/beacon_vote_encoding.rs @@ -1,11 +1,12 @@ use serde::Deserialize; -use ssz::{Decode, Encode}; -use tree_hash::TreeHash; use types::Hash256; use crate::{ SpecTest, - utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + utils::{ + check_roundtrip_with_root, + deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + }, }; /// Mirrors Go's `EncodingTest` for `BeaconVote`. @@ -22,29 +23,9 @@ pub struct BeaconVoteEncodingTest { impl SpecTest for BeaconVoteEncodingTest { fn run(&self) -> Result<(), String> { - // Decode BeaconVote from SSZ bytes - let decoded = ssv_types::consensus::BeaconVote::from_ssz_bytes(&self.data) - .map_err(|e| format!("SSZ decode failed: {e:?}"))?; - - // Encode back and verify roundtrip - let encoded = decoded.as_ssz_bytes(); - if encoded != self.data { - return Err(format!( - "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", - encoded.len(), - self.data.len() - )); - } - - // Verify hash tree root - let root = decoded.tree_hash_root(); - if root != self.expected_root { - return Err(format!( - "Hash tree root mismatch: got {root:?}, expected {:?}", - self.expected_root - )); - } - - Ok(()) + check_roundtrip_with_root::( + &self.data, + self.expected_root, + ) } } diff --git a/anchor/spec_tests/src/types/partial_sig_message_encoding.rs b/anchor/spec_tests/src/types/partial_sig_message_encoding.rs index 14df4974f..5157a3aca 100644 --- a/anchor/spec_tests/src/types/partial_sig_message_encoding.rs +++ b/anchor/spec_tests/src/types/partial_sig_message_encoding.rs @@ -1,11 +1,12 @@ use serde::Deserialize; -use ssz::{Decode, Encode}; -use tree_hash::TreeHash; use types::Hash256; use crate::{ SpecTest, - utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + utils::{ + check_roundtrip_with_root, + deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + }, }; /// Mirrors Go's `EncodingTest` for `PartialSignatureMessages`. @@ -22,29 +23,9 @@ pub struct PartialSigMessageEncodingTest { impl SpecTest for PartialSigMessageEncodingTest { fn run(&self) -> Result<(), String> { - // Decode PartialSignatureMessages from SSZ bytes - let decoded = ssv_types::partial_sig::PartialSignatureMessages::from_ssz_bytes(&self.data) - .map_err(|e| format!("SSZ decode failed: {e:?}"))?; - - // Encode back and verify roundtrip - let encoded = decoded.as_ssz_bytes(); - if encoded != self.data { - return Err(format!( - "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", - encoded.len(), - self.data.len() - )); - } - - // Verify hash tree root - let root = decoded.tree_hash_root(); - if root != self.expected_root { - return Err(format!( - "Hash tree root mismatch: got {root:?}, expected {:?}", - self.expected_root - )); - } - - Ok(()) + check_roundtrip_with_root::( + &self.data, + self.expected_root, + ) } } diff --git a/anchor/spec_tests/src/types/proposer_consensus_data_encoding.rs b/anchor/spec_tests/src/types/proposer_consensus_data_encoding.rs index 09d28a15e..580792416 100644 --- a/anchor/spec_tests/src/types/proposer_consensus_data_encoding.rs +++ b/anchor/spec_tests/src/types/proposer_consensus_data_encoding.rs @@ -1,11 +1,12 @@ use serde::Deserialize; -use ssz::{Decode, Encode}; -use tree_hash::TreeHash; use types::Hash256; use crate::{ SpecTest, - utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + utils::{ + check_roundtrip_with_root, + deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + }, }; /// Mirrors Go's `EncodingTest` for `ProposerConsensusData`. @@ -22,29 +23,9 @@ pub struct ProposerConsensusDataEncodingTest { impl SpecTest for ProposerConsensusDataEncodingTest { fn run(&self) -> Result<(), String> { - // Decode ProposerConsensusData from SSZ bytes - let decoded = ssv_types::consensus::ProposerConsensusData::from_ssz_bytes(&self.data) - .map_err(|e| format!("SSZ decode failed: {e:?}"))?; - - // Encode back and verify roundtrip - let encoded = decoded.as_ssz_bytes(); - if encoded != self.data { - return Err(format!( - "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", - encoded.len(), - self.data.len() - )); - } - - // Verify hash tree root - let root = decoded.tree_hash_root(); - if root != self.expected_root { - return Err(format!( - "Hash tree root mismatch: got {root:?}, expected {:?}", - self.expected_root - )); - } - - Ok(()) + check_roundtrip_with_root::( + &self.data, + self.expected_root, + ) } } diff --git a/anchor/spec_tests/src/types/signed_ssv_msg_encoding.rs b/anchor/spec_tests/src/types/signed_ssv_msg_encoding.rs index 162e7f1c6..5f998e2c5 100644 --- a/anchor/spec_tests/src/types/signed_ssv_msg_encoding.rs +++ b/anchor/spec_tests/src/types/signed_ssv_msg_encoding.rs @@ -1,7 +1,9 @@ use serde::Deserialize; -use ssz::{Decode, Encode}; -use crate::{SpecTest, utils::deserializers::deserialize_base64}; +use crate::{ + SpecTest, + utils::{check_roundtrip, deserializers::deserialize_base64}, +}; /// Mirrors Go's `EncodingTest` for `SignedSSVMessage`. /// @@ -15,20 +17,6 @@ pub struct SignedSSVMessageEncodingTest { impl SpecTest for SignedSSVMessageEncodingTest { fn run(&self) -> Result<(), String> { - // Decode SignedSSVMessage from SSZ bytes - let decoded = ssv_types::message::SignedSSVMessage::from_ssz_bytes(&self.data) - .map_err(|e| format!("SSZ decode failed: {e:?}"))?; - - // Encode back and verify roundtrip - let encoded = decoded.as_ssz_bytes(); - if encoded != self.data { - return Err(format!( - "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", - encoded.len(), - self.data.len() - )); - } - - Ok(()) + check_roundtrip::(&self.data) } } diff --git a/anchor/spec_tests/src/types/ssv_message_encoding.rs b/anchor/spec_tests/src/types/ssv_message_encoding.rs index 45d92e0d2..73382ce83 100644 --- a/anchor/spec_tests/src/types/ssv_message_encoding.rs +++ b/anchor/spec_tests/src/types/ssv_message_encoding.rs @@ -1,11 +1,12 @@ use serde::Deserialize; -use ssz::{Decode, Encode}; -use tree_hash::TreeHash; use types::Hash256; use crate::{ SpecTest, - utils::deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + utils::{ + check_roundtrip_with_root, + deserializers::{deserialize_base64, deserialize_bytes_to_hash256}, + }, }; /// Mirrors Go's `EncodingTest` for `SSVMessage`. @@ -22,29 +23,6 @@ pub struct SSVMessageEncodingTest { impl SpecTest for SSVMessageEncodingTest { fn run(&self) -> Result<(), String> { - // Decode SSVMessage from SSZ bytes - let decoded = ssv_types::message::SSVMessage::from_ssz_bytes(&self.data) - .map_err(|e| format!("SSZ decode failed: {e:?}"))?; - - // Encode back and verify roundtrip - let encoded = decoded.as_ssz_bytes(); - if encoded != self.data { - return Err(format!( - "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", - encoded.len(), - self.data.len() - )); - } - - // Verify hash tree root - let root = decoded.tree_hash_root(); - if root != self.expected_root { - return Err(format!( - "Hash tree root mismatch: got {root:?}, expected {:?}", - self.expected_root - )); - } - - Ok(()) + check_roundtrip_with_root::(&self.data, self.expected_root) } } diff --git a/anchor/spec_tests/src/utils/encoding_helpers.rs b/anchor/spec_tests/src/utils/encoding_helpers.rs new file mode 100644 index 000000000..83abb2d69 --- /dev/null +++ b/anchor/spec_tests/src/utils/encoding_helpers.rs @@ -0,0 +1,40 @@ +use ssz::{Decode, Encode}; +use tree_hash::TreeHash; +use types::Hash256; + +/// SSZ roundtrip: decode → encode → compare bytes. +pub fn check_roundtrip(data: &[u8]) -> Result<(), String> { + let decoded = T::from_ssz_bytes(data).map_err(|e| format!("SSZ decode failed: {e:?}"))?; + let encoded = decoded.as_ssz_bytes(); + if encoded != data { + return Err(format!( + "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", + encoded.len(), + data.len() + )); + } + Ok(()) +} + +/// SSZ roundtrip + hash tree root verification. +pub fn check_roundtrip_with_root( + data: &[u8], + expected_root: Hash256, +) -> Result<(), String> { + let decoded = T::from_ssz_bytes(data).map_err(|e| format!("SSZ decode failed: {e:?}"))?; + let encoded = decoded.as_ssz_bytes(); + if encoded != data { + return Err(format!( + "SSZ roundtrip mismatch: encoded {} bytes, expected {} bytes", + encoded.len(), + data.len() + )); + } + let root = decoded.tree_hash_root(); + if root != expected_root { + return Err(format!( + "Hash tree root mismatch: got {root:?}, expected {expected_root:?}", + )); + } + Ok(()) +} diff --git a/anchor/spec_tests/src/utils/mod.rs b/anchor/spec_tests/src/utils/mod.rs index 0b2b81a4f..81d7fc19d 100644 --- a/anchor/spec_tests/src/utils/mod.rs +++ b/anchor/spec_tests/src/utils/mod.rs @@ -1 +1,4 @@ pub mod deserializers; +pub mod encoding_helpers; + +pub use encoding_helpers::{check_roundtrip, check_roundtrip_with_root}; From e6b57e4837ee6a7951b98cb1cbdc06fb9b88e8a8 Mon Sep 17 00:00:00 2001 From: shane-moore Date: Mon, 23 Feb 2026 20:04:56 -0800 Subject: [PATCH 9/9] chore: add context to share test being skipped --- anchor/spec_tests/src/runner/types_dispatcher.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/anchor/spec_tests/src/runner/types_dispatcher.rs b/anchor/spec_tests/src/runner/types_dispatcher.rs index d7b02186b..b6dc7025b 100644 --- a/anchor/spec_tests/src/runner/types_dispatcher.rs +++ b/anchor/spec_tests/src/runner/types_dispatcher.rs @@ -38,6 +38,7 @@ fn dispatch_fixture_by_prefix( // Anchor's `Share` is architecturally different from Go spec's `Share` // (different fields, decomposed across multiple types). Not applicable. "share.EncodingTest" => { + eprintln!("SKIP (known-inapplicable): {prefix}"); *skipped_known_count += 1; None } @@ -107,6 +108,10 @@ pub fn run_all_type_fixtures() { dir.display() ); + eprintln!( + "{executed_count} executed, {skipped_known_count} skipped (known-inapplicable), {skipped_unknown_count} skipped (not yet implemented)" + ); + if !failures.is_empty() { panic!( "\n{} of {executed_count} type spec tests failed:\n{}",