From 6ef62848be250e599bcce70c69caca2c6e25b816 Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 10 Feb 2026 17:58:03 +0700 Subject: [PATCH 01/20] add ssz test vectors --- Cargo.lock | 1 + testing/lean-spec-tests/Cargo.toml | 1 + testing/lean-spec-tests/src/lib.rs | 1 + testing/lean-spec-tests/src/ssz_test.rs | 196 ++++++++ testing/lean-spec-tests/src/types/mod.rs | 1 + testing/lean-spec-tests/src/types/ssz_test.rs | 474 ++++++++++++++++++ testing/lean-spec-tests/tests/tests.rs | 65 +++ 7 files changed, 739 insertions(+) create mode 100644 testing/lean-spec-tests/src/ssz_test.rs create mode 100644 testing/lean-spec-tests/src/types/ssz_test.rs diff --git a/Cargo.lock b/Cargo.lock index dee117167..68be17ab2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3660,6 +3660,7 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "anyhow", + "ethereum_ssz", "ream-consensus-lean", "ream-consensus-misc", "ream-fork-choice-lean", diff --git a/testing/lean-spec-tests/Cargo.toml b/testing/lean-spec-tests/Cargo.toml index c4af0b3f8..293b0f1fd 100644 --- a/testing/lean-spec-tests/Cargo.toml +++ b/testing/lean-spec-tests/Cargo.toml @@ -18,6 +18,7 @@ devnet3 = ["ream-consensus-lean/devnet3", "ream-fork-choice-lean/devnet3"] [dependencies] alloy-primitives.workspace = true anyhow.workspace = true +ssz = { package = "ethereum_ssz", version = "0.10" } serde.workspace = true serde_json.workspace = true ssz_types.workspace = true diff --git a/testing/lean-spec-tests/src/lib.rs b/testing/lean-spec-tests/src/lib.rs index e69c81c43..90520f21b 100644 --- a/testing/lean-spec-tests/src/lib.rs +++ b/testing/lean-spec-tests/src/lib.rs @@ -1,3 +1,4 @@ pub mod fork_choice; +pub mod ssz_test; pub mod state_transition; pub mod types; diff --git a/testing/lean-spec-tests/src/ssz_test.rs b/testing/lean-spec-tests/src/ssz_test.rs new file mode 100644 index 000000000..c95420a71 --- /dev/null +++ b/testing/lean-spec-tests/src/ssz_test.rs @@ -0,0 +1,196 @@ +use std::path::Path; + +use alloy_primitives::{B256, hex}; +use anyhow::{anyhow, bail}; +use ream_consensus_lean::{ + attestation::{AggregatedAttestation, AggregatedAttestations, AttestationData, SignedAttestation}, + block::{Block, BlockBody, BlockHeader, BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, + checkpoint::Checkpoint, + config::Config, + state::LeanState, + validator::Validator, +}; +use ssz::Encode; +use tracing::{debug, info, warn}; +use tree_hash::TreeHash; + +use crate::types::{ + TestFixture, + ssz_test::{ + SSZTest, AggregatedAttestationJSON, AttestationDataJSON, AttestationJSON, + BlockBodyJSON, BlockHeaderJSON, BlockJSON, BlockSignaturesJSON, BlockWithAttestationJSON, + CheckpointJSON, ConfigJSON, SignedAttestationJSON, SignedBlockWithAttestationJSON, + StateJSON, ValidatorJSON, + }, +}; + +/// Load an SSZ test fixture from a JSON file +pub fn load_ssz_test(path: impl AsRef) -> anyhow::Result> { + let content = std::fs::read_to_string(path.as_ref()).map_err(|err| { + anyhow!( + "Failed to read test file {:?}: {err}", + path.as_ref().display() + ) + })?; + + let fixture: TestFixture = serde_json::from_str(&content).map_err(|err| { + anyhow!( + "Failed to parse test file {:?}: {err}", + path.as_ref().display() + ) + })?; + + Ok(fixture) +} + +/// Run a single SSZ test case +pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { + info!("Running SSZ test: {test_name}"); + debug!(" Network: {}", test.network); + debug!(" Type: {}", test.type_name); + + // Parse expected values + let expected_serialized = parse_hex_bytes(&test.serialized)?; + let expected_root = test.root; + + // Run the test based on type - using intermediate JSON types and converting to ream types + match test.type_name.as_str() { + "Checkpoint" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "AttestationData" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "AggregatedAttestation" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "Attestation" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "BlockBody" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "BlockHeader" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "Block" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "Config" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "Validator" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + "State" => { + run_test::(&test.value, &expected_serialized, expected_root) + } + // Types without proper TreeHash implementation - only test SSZ serialization + "SignedAttestation" => { + run_test_ssz_only::(&test.value, &expected_serialized) + } + "BlockSignatures" => { + run_test_ssz_only::(&test.value, &expected_serialized) + } + "BlockWithAttestation" => { + run_test_ssz_only::(&test.value, &expected_serialized) + } + "SignedBlockWithAttestation" => { + run_test_ssz_only::(&test.value, &expected_serialized) + } + _ => { + warn!("Unknown type: {}, skipping", test.type_name); + Ok(()) + } + } +} + +/// Run a test by deserializing JSON into intermediate type, converting to ream type, +/// then verifying SSZ serialization and tree hash root. +fn run_test( + value: &serde_json::Value, + expected_serialized: &[u8], + expected_root: B256, +) -> anyhow::Result<()> +where + J: serde::de::DeserializeOwned, + T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode + TreeHash, +{ + // Deserialize into intermediate JSON type + let json_value: J = serde_json::from_value(value.clone()).map_err(|err| { + anyhow!("Failed to deserialize JSON value: {err}") + })?; + + // Convert to ream type + let typed_value: T = (&json_value).try_into()?; + + // SSZ serialize + let serialized = typed_value.as_ssz_bytes(); + if serialized != expected_serialized { + bail!( + "SSZ serialization mismatch:\n expected: 0x{}\n got: 0x{}", + hex::encode(expected_serialized), + hex::encode(&serialized) + ); + } + + // Compute tree hash root + let root = typed_value.tree_hash_root(); + if root != expected_root { + bail!( + "Tree hash root mismatch:\n expected: {expected_root}\n got: {root}" + ); + } + + Ok(()) +} + +/// Run a test for types without TreeHash - only verify SSZ serialization. +fn run_test_ssz_only( + value: &serde_json::Value, + expected_serialized: &[u8], +) -> anyhow::Result<()> +where + J: serde::de::DeserializeOwned, + T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode, +{ + // Deserialize into intermediate JSON type + let json_value: J = serde_json::from_value(value.clone()).map_err(|err| { + anyhow!("Failed to deserialize JSON value: {err}") + })?; + + // Convert to ream type + let typed_value: T = (&json_value).try_into()?; + + // SSZ serialize + let serialized = typed_value.as_ssz_bytes(); + if serialized != expected_serialized { + bail!( + "SSZ serialization mismatch:\n expected: 0x{}\n got: 0x{}", + hex::encode(expected_serialized), + hex::encode(&serialized) + ); + } + + Ok(()) +} + +/// Parse a hex string (with 0x prefix) into bytes +fn parse_hex_bytes(hex_str: &str) -> anyhow::Result> { + let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); + hex::decode(hex_str).map_err(|err| anyhow!("Failed to parse hex: {err}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_hex_bytes() { + let bytes = parse_hex_bytes("0xdeadbeef").unwrap(); + assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]); + + let bytes = parse_hex_bytes("deadbeef").unwrap(); + assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]); + } +} diff --git a/testing/lean-spec-tests/src/types/mod.rs b/testing/lean-spec-tests/src/types/mod.rs index 5303bb64e..e764ce594 100644 --- a/testing/lean-spec-tests/src/types/mod.rs +++ b/testing/lean-spec-tests/src/types/mod.rs @@ -1,4 +1,5 @@ pub mod fork_choice; +pub mod ssz_test; pub mod state_transition; use std::collections::HashMap; diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs new file mode 100644 index 000000000..49465efe3 --- /dev/null +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -0,0 +1,474 @@ +//! SSZ test types with proper serde attributes for leanSpec JSON format. +//! +//! These intermediate types handle the camelCase JSON format from leanSpec fixtures +//! and provide conversions to the actual ream-consensus-lean types. + +use alloy_primitives::B256; +use anyhow::anyhow; +use ream_consensus_lean::{ + attestation::{ + AggregatedAttestation as ReamAggregatedAttestation, + AggregatedAttestations as ReamAggregatedAttestations, + AggregatedSignatureProof as ReamAggregatedSignatureProof, + AttestationData as ReamAttestationData, + SignedAttestation as ReamSignedAttestation, + }, + block::{ + Block as ReamBlock, BlockBody as ReamBlockBody, BlockHeader as ReamBlockHeader, + BlockSignatures as ReamBlockSignatures, BlockWithAttestation as ReamBlockWithAttestation, + SignedBlockWithAttestation as ReamSignedBlockWithAttestation, + }, + checkpoint::Checkpoint as ReamCheckpoint, + config::Config as ReamConfig, + state::LeanState as ReamState, + validator::Validator as ReamValidator, +}; +use ream_post_quantum_crypto::leansig::{public_key::PublicKey, signature::Signature}; +use serde::Deserialize; +use ssz::Decode; +use ssz_types::{BitList, VariableList, typenum::{U4096, U1048576}}; + +/// SSZ test case from leanSpec fixtures +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SSZTest { + pub network: String, + pub lean_env: String, + pub type_name: String, + pub value: serde_json::Value, + pub serialized: String, + pub root: B256, +} + +// ============================================================================ +// Intermediate types for JSON deserialization (camelCase) +// ============================================================================ + +/// Config with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ConfigJSON { + pub genesis_time: u64, +} + +impl TryFrom<&ConfigJSON> for ReamConfig { + type Error = anyhow::Error; + + fn try_from(config: &ConfigJSON) -> anyhow::Result { + Ok(ReamConfig { + genesis_time: config.genesis_time, + }) + } +} + +/// Checkpoint - already snake_case in JSON, but define for consistency +#[derive(Debug, Deserialize, Clone)] +pub struct CheckpointJSON { + pub root: B256, + pub slot: u64, +} + +impl TryFrom<&CheckpointJSON> for ReamCheckpoint { + type Error = anyhow::Error; + + fn try_from(cp: &CheckpointJSON) -> anyhow::Result { + Ok(ReamCheckpoint { + root: cp.root, + slot: cp.slot, + }) + } +} + +/// AttestationData - already snake_case in JSON +#[derive(Debug, Deserialize, Clone)] +pub struct AttestationDataJSON { + pub slot: u64, + pub head: CheckpointJSON, + pub target: CheckpointJSON, + pub source: CheckpointJSON, +} + +impl TryFrom<&AttestationDataJSON> for ReamAttestationData { + type Error = anyhow::Error; + + fn try_from(data: &AttestationDataJSON) -> anyhow::Result { + Ok(ReamAttestationData { + slot: data.slot, + head: (&data.head).try_into()?, + target: (&data.target).try_into()?, + source: (&data.source).try_into()?, + }) + } +} + +/// BlockHeader with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BlockHeaderJSON { + pub slot: u64, + pub proposer_index: u64, + pub parent_root: B256, + pub state_root: B256, + pub body_root: B256, +} + +impl TryFrom<&BlockHeaderJSON> for ReamBlockHeader { + type Error = anyhow::Error; + + fn try_from(header: &BlockHeaderJSON) -> anyhow::Result { + Ok(ReamBlockHeader { + slot: header.slot, + proposer_index: header.proposer_index, + parent_root: header.parent_root, + state_root: header.state_root, + body_root: header.body_root, + }) + } +} + +/// Validator with camelCase fields +#[derive(Debug, Deserialize, Clone)] +pub struct ValidatorJSON { + pub pubkey: String, + pub index: u64, +} + +impl TryFrom<&ValidatorJSON> for ReamValidator { + type Error = anyhow::Error; + + fn try_from(validator: &ValidatorJSON) -> anyhow::Result { + let pubkey_hex = validator.pubkey.trim_start_matches("0x"); + let pubkey_bytes = alloy_primitives::hex::decode(pubkey_hex) + .map_err(|err| anyhow!("Failed to decode validator pubkey hex: {err}"))?; + + if pubkey_bytes.len() != 52 { + return Err(anyhow!( + "Expected 52-byte pubkey, got {} bytes", + pubkey_bytes.len() + )); + } + + Ok(ReamValidator { + public_key: PublicKey::from(&pubkey_bytes[..]), + index: validator.index, + }) + } +} + +/// Wrapper for data lists in JSON format +#[derive(Debug, Deserialize, Clone)] +pub struct DataListJSON { + pub data: Vec, +} + +/// AggregationBits wrapper +#[derive(Debug, Deserialize, Clone)] +pub struct AggregationBitsJSON { + pub data: Vec, +} + +/// AggregatedAttestation with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AggregatedAttestationJSON { + pub aggregation_bits: AggregationBitsJSON, + pub data: AttestationDataJSON, +} + +impl TryFrom<&AggregatedAttestationJSON> for ReamAggregatedAttestation { + type Error = anyhow::Error; + + fn try_from(att: &AggregatedAttestationJSON) -> anyhow::Result { + let bool_data = &att.aggregation_bits.data; + let mut aggregation_bits = BitList::::with_capacity(bool_data.len()) + .map_err(|err| anyhow!("Failed to create BitList: {err:?}"))?; + + for (i, &bit) in bool_data.iter().enumerate() { + aggregation_bits + .set(i, bit) + .map_err(|err| anyhow!("Failed to set bit at index {i}: {err:?}"))?; + } + + Ok(ReamAggregatedAttestation { + aggregation_bits, + message: (&att.data).try_into()?, + }) + } +} + +/// Attestation (individual) with camelCase fields - maps to AggregatedAttestations in Rust +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AttestationJSON { + pub validator_id: u64, + pub data: AttestationDataJSON, +} + +impl TryFrom<&AttestationJSON> for ReamAggregatedAttestations { + type Error = anyhow::Error; + + fn try_from(att: &AttestationJSON) -> anyhow::Result { + Ok(ReamAggregatedAttestations { + validator_id: att.validator_id, + data: (&att.data).try_into()?, + }) + } +} + +/// BlockBody with attestations in {data: [...]} format +#[derive(Debug, Deserialize, Clone)] +pub struct BlockBodyJSON { + pub attestations: DataListJSON, +} + +impl TryFrom<&BlockBodyJSON> for ReamBlockBody { + type Error = anyhow::Error; + + fn try_from(body: &BlockBodyJSON) -> anyhow::Result { + let mut attestations = Vec::new(); + for att in &body.attestations.data { + attestations.push(ReamAggregatedAttestation::try_from(att)?); + } + + Ok(ReamBlockBody { + attestations: VariableList::try_from(attestations) + .map_err(|err| anyhow!("Failed to create attestations VariableList: {err}"))?, + }) + } +} + +/// Block with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BlockJSON { + pub slot: u64, + pub proposer_index: u64, + pub parent_root: B256, + pub state_root: B256, + pub body: BlockBodyJSON, +} + +impl TryFrom<&BlockJSON> for ReamBlock { + type Error = anyhow::Error; + + fn try_from(block: &BlockJSON) -> anyhow::Result { + Ok(ReamBlock { + slot: block.slot, + proposer_index: block.proposer_index, + parent_root: block.parent_root, + state_root: block.state_root, + body: ReamBlockBody::try_from(&block.body)?, + }) + } +} + +/// State with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StateJSON { + pub config: ConfigJSON, + pub slot: u64, + pub latest_block_header: BlockHeaderJSON, + pub latest_justified: CheckpointJSON, + pub latest_finalized: CheckpointJSON, + pub historical_block_hashes: DataListJSON, + pub justified_slots: DataListJSON, + pub validators: DataListJSON, + pub justifications_roots: DataListJSON, + pub justifications_validators: DataListJSON, +} + +impl TryFrom<&StateJSON> for ReamState { + type Error = anyhow::Error; + + fn try_from(state: &StateJSON) -> anyhow::Result { + use ssz_types::typenum::{U262144, U1073741824}; + + // Convert validators + let validators: Vec = state + .validators + .data + .iter() + .map(ReamValidator::try_from) + .collect::, _>>()?; + + // Convert justified_slots to BitList + let justified_slots_len = state.justified_slots.data.len(); + let mut justified_slots = BitList::::with_capacity(justified_slots_len) + .map_err(|err| anyhow!("Failed to create justified_slots BitList: {err:?}"))?; + for (i, &bit) in state.justified_slots.data.iter().enumerate() { + justified_slots + .set(i, bit) + .map_err(|err| anyhow!("Failed to set justified_slots bit at {i}: {err:?}"))?; + } + + // Convert justifications_validators to BitList + let justifications_len = state.justifications_validators.data.len(); + let mut justifications_validators = + BitList::::with_capacity(justifications_len) + .map_err(|err| anyhow!("Failed to create justifications_validators BitList: {err:?}"))?; + for (i, &bit) in state.justifications_validators.data.iter().enumerate() { + justifications_validators + .set(i, bit) + .map_err(|err| anyhow!("Failed to set justifications_validators bit at {i}: {err:?}"))?; + } + + Ok(ReamState { + config: (&state.config).try_into()?, + slot: state.slot, + latest_block_header: (&state.latest_block_header).try_into()?, + latest_justified: (&state.latest_justified).try_into()?, + latest_finalized: (&state.latest_finalized).try_into()?, + historical_block_hashes: VariableList::try_from(state.historical_block_hashes.data.clone()) + .map_err(|err| anyhow!("Failed to create historical_block_hashes: {err}"))?, + justified_slots, + validators: VariableList::try_from(validators) + .map_err(|err| anyhow!("Failed to create validators: {err}"))?, + justifications_roots: VariableList::try_from(state.justifications_roots.data.clone()) + .map_err(|err| anyhow!("Failed to create justifications_roots: {err}"))?, + justifications_validators, + }) + } +} + +// ============================================================================ +// Signature-related types +// ============================================================================ + +/// SignedAttestation with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SignedAttestationJSON { + pub validator_id: u64, + pub message: AttestationDataJSON, + pub signature: String, +} + +impl TryFrom<&SignedAttestationJSON> for ReamSignedAttestation { + type Error = anyhow::Error; + + fn try_from(att: &SignedAttestationJSON) -> anyhow::Result { + let sig_hex = att.signature.trim_start_matches("0x"); + let sig_bytes = alloy_primitives::hex::decode(sig_hex) + .map_err(|err| anyhow!("Failed to decode signature hex: {err}"))?; + + let signature = Signature::from_ssz_bytes(&sig_bytes) + .map_err(|err| anyhow!("Failed to decode signature from SSZ: {err:?}"))?; + + Ok(ReamSignedAttestation { + validator_id: att.validator_id, + message: (&att.message).try_into()?, + signature, + }) + } +} + +/// Wrapper for proof_data in JSON format (nested { data: "0x..." }) +#[derive(Debug, Deserialize, Clone)] +pub struct ProofDataJSON { + pub data: String, +} + +/// AggregatedSignatureProof with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AggregatedSignatureProofJSON { + pub participants: AggregationBitsJSON, + pub proof_data: ProofDataJSON, +} + +impl TryFrom<&AggregatedSignatureProofJSON> for ReamAggregatedSignatureProof { + type Error = anyhow::Error; + + fn try_from(proof: &AggregatedSignatureProofJSON) -> anyhow::Result { + let bool_data = &proof.participants.data; + let mut participants = BitList::::with_capacity(bool_data.len()) + .map_err(|err| anyhow!("Failed to create BitList: {err:?}"))?; + + for (i, &bit) in bool_data.iter().enumerate() { + participants + .set(i, bit) + .map_err(|err| anyhow!("Failed to set bit at index {i}: {err:?}"))?; + } + + let proof_hex = proof.proof_data.data.trim_start_matches("0x"); + let proof_bytes = alloy_primitives::hex::decode(proof_hex) + .map_err(|err| anyhow!("Failed to decode proof_data hex: {err}"))?; + + Ok(ReamAggregatedSignatureProof { + participants, + proof_data: VariableList::::try_from(proof_bytes) + .map_err(|err| anyhow!("Failed to create proof_data: {err}"))?, + }) + } +} + +/// BlockSignatures with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BlockSignaturesJSON { + pub attestation_signatures: DataListJSON, + pub proposer_signature: String, +} + +impl TryFrom<&BlockSignaturesJSON> for ReamBlockSignatures { + type Error = anyhow::Error; + + fn try_from(sigs: &BlockSignaturesJSON) -> anyhow::Result { + let mut attestation_signatures = Vec::new(); + for proof in &sigs.attestation_signatures.data { + attestation_signatures.push(ReamAggregatedSignatureProof::try_from(proof)?); + } + + let sig_hex = sigs.proposer_signature.trim_start_matches("0x"); + let sig_bytes = alloy_primitives::hex::decode(sig_hex) + .map_err(|err| anyhow!("Failed to decode proposer_signature hex: {err}"))?; + + let proposer_signature = Signature::from_ssz_bytes(&sig_bytes) + .map_err(|err| anyhow!("Failed to decode proposer_signature from SSZ: {err:?}"))?; + + Ok(ReamBlockSignatures { + attestation_signatures: VariableList::try_from(attestation_signatures) + .map_err(|err| anyhow!("Failed to create attestation_signatures: {err}"))?, + proposer_signature, + }) + } +} + +/// BlockWithAttestation with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BlockWithAttestationJSON { + pub block: BlockJSON, + pub proposer_attestation: AttestationJSON, +} + +impl TryFrom<&BlockWithAttestationJSON> for ReamBlockWithAttestation { + type Error = anyhow::Error; + + fn try_from(bwa: &BlockWithAttestationJSON) -> anyhow::Result { + Ok(ReamBlockWithAttestation { + block: (&bwa.block).try_into()?, + proposer_attestation: (&bwa.proposer_attestation).try_into()?, + }) + } +} + +/// SignedBlockWithAttestation with camelCase fields +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SignedBlockWithAttestationJSON { + pub message: BlockWithAttestationJSON, + pub signature: BlockSignaturesJSON, +} + +impl TryFrom<&SignedBlockWithAttestationJSON> for ReamSignedBlockWithAttestation { + type Error = anyhow::Error; + + fn try_from(sbwa: &SignedBlockWithAttestationJSON) -> anyhow::Result { + Ok(ReamSignedBlockWithAttestation { + message: (&sbwa.message).try_into()?, + signature: (&sbwa.signature).try_into()?, + }) + } +} diff --git a/testing/lean-spec-tests/tests/tests.rs b/testing/lean-spec-tests/tests/tests.rs index 0172a501d..206e5e1c6 100644 --- a/testing/lean-spec-tests/tests/tests.rs +++ b/testing/lean-spec-tests/tests/tests.rs @@ -2,6 +2,7 @@ use std::{env, fs, path::PathBuf}; #[cfg(feature = "devnet2")] use lean_spec_tests::fork_choice::{load_fork_choice_test, run_fork_choice_test}; +use lean_spec_tests::ssz_test::{load_ssz_test, run_ssz_test}; use lean_spec_tests::state_transition::{load_state_transition_test, run_state_transition_test}; use tracing::{debug, error, info, warn}; use tracing_subscriber::EnvFilter; @@ -156,3 +157,67 @@ fn test_all_state_transition_fixtures() { assert_eq!(failed, 0, "Some state transition tests failed"); } + +#[test] +fn test_all_ssz_fixtures() { + // Initialize tracing subscriber for test output + let env_filter = match env::var(EnvFilter::DEFAULT_ENV) { + Ok(filter) => EnvFilter::builder().parse_lossy(filter), + Err(_) => EnvFilter::new("info"), + }; + let _ = tracing_subscriber::fmt() + .with_env_filter(env_filter) + .try_init(); + + let fixtures = find_json_files("fixtures/consensus/ssz/devnet"); + + if fixtures.is_empty() { + info!( + "No SSZ fixtures found. Skipping tests. Run 'make test' in lean-spec-tests to download fixtures." + ); + return; + } + + info!("Found {} SSZ test fixtures", fixtures.len()); + + let mut total_tests = 0; + let mut passed = 0; + let mut failed = 0; + let mut skipped = 0; + + for fixture_path in fixtures { + debug!("\n=== Loading fixture: {:?} ===", fixture_path.file_name()); + + match load_ssz_test(&fixture_path) { + Ok(fixture) => { + for (test_name, test) in &fixture { + total_tests += 1; + info!("Starting test: {}", test_name); + match run_ssz_test(test_name, test) { + Ok(_) => { + // Check if the test was skipped (logs contain "Skipping") + passed += 1; + info!("PASSED: {}", test_name); + } + Err(err) => { + failed += 1; + error!("FAILED: {test_name} - {err:?}"); + } + } + } + } + Err(err) => { + error!("Failed to load fixture {fixture_path:?}: {err:?}"); + failed += 1; + } + } + } + + info!("\n=== SSZ Test Summary ==="); + info!("Total tests: {total_tests}"); + info!("Passed: {passed}"); + info!("Skipped: {skipped}"); + info!("Failed: {failed}"); + + assert_eq!(failed, 0, "Some SSZ tests failed"); +} From 5a5df5bcd8aa7187b7cce97311c7270ab1e0aa86 Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 10 Feb 2026 17:58:53 +0700 Subject: [PATCH 02/20] change Signature.inner to hold leansig's signature type directly instead of fixed bytes --- crates/common/fork_choice/lean/src/store.rs | 8 +- .../src/lean_multisig/aggregate.rs | 5 +- .../post_quantum/src/leansig/private_key.rs | 2 +- .../post_quantum/src/leansig/signature.rs | 121 +++++++++++++----- 4 files changed, 95 insertions(+), 41 deletions(-) diff --git a/crates/common/fork_choice/lean/src/store.rs b/crates/common/fork_choice/lean/src/store.rs index 288efec9e..9f19e0042 100644 --- a/crates/common/fork_choice/lean/src/store.rs +++ b/crates/common/fork_choice/lean/src/store.rs @@ -1275,7 +1275,7 @@ impl Store { proposer_attestation.validator_id, &proposer_attestation.data, ), - signed_block_with_attestation.signature.proposer_signature, + signed_block_with_attestation.signature.proposer_signature.clone(), )?; #[cfg(feature = "devnet3")] @@ -1293,7 +1293,7 @@ impl Store { proposer_attestation.validator_id, &proposer_attestation.data, ), - signed_block_with_attestation.signature.proposer_signature, + signed_block_with_attestation.signature.proposer_signature.clone(), )?; } } @@ -1304,7 +1304,7 @@ impl Store { SignedAttestation { validator_id: proposer_attestation.validator_id, message: proposer_attestation.data.clone(), - signature: signed_block_with_attestation.signature.proposer_signature, + signature: signed_block_with_attestation.signature.proposer_signature.clone(), }, false, ) @@ -1624,7 +1624,7 @@ impl Store { ) -> anyhow::Result<()> { let validator_id = signed_attestation.validator_id; let attestation_data = &signed_attestation.message; - let signature = signed_attestation.signature; + let signature = signed_attestation.signature.clone(); self.validate_attestation(&signed_attestation).await?; diff --git a/crates/crypto/post_quantum/src/lean_multisig/aggregate.rs b/crates/crypto/post_quantum/src/lean_multisig/aggregate.rs index 61b36c243..356f000e7 100644 --- a/crates/crypto/post_quantum/src/lean_multisig/aggregate.rs +++ b/crates/crypto/post_quantum/src/lean_multisig/aggregate.rs @@ -40,9 +40,8 @@ pub fn aggregate_signatures( .map_err(|err| anyhow!("Failed to convert public keys: {err}"))?, &signatures .iter() - .map(|signature| signature.as_lean_sig()) - .collect::, _>>() - .map_err(|err| anyhow!("Failed to convert signatures: {err}"))?, + .map(|signature| signature.as_lean_sig().clone()) + .collect::>(), message, epoch, ) diff --git a/crates/crypto/post_quantum/src/leansig/private_key.rs b/crates/crypto/post_quantum/src/leansig/private_key.rs index 5444b3734..eefa31c83 100644 --- a/crates/crypto/post_quantum/src/leansig/private_key.rs +++ b/crates/crypto/post_quantum/src/leansig/private_key.rs @@ -76,7 +76,7 @@ impl PrivateKey { let signature = ::sign(&self.inner, epoch, message) .map_err(LeanSigError::SigningFailed)?; - Signature::from_lean_sig(signature) + Ok(Signature::from_lean_sig(signature)) } pub fn from_bytes(bytes: &[u8]) -> Result { diff --git a/crates/crypto/post_quantum/src/leansig/signature.rs b/crates/crypto/post_quantum/src/leansig/signature.rs index daa3857a2..407812dec 100644 --- a/crates/crypto/post_quantum/src/leansig/signature.rs +++ b/crates/crypto/post_quantum/src/leansig/signature.rs @@ -1,37 +1,96 @@ -use alloy_primitives::FixedBytes; -use anyhow::anyhow; -use leansig::{MESSAGE_LENGTH, serialization::Serializable, signature::SignatureScheme}; +use leansig::{MESSAGE_LENGTH, signature::SignatureScheme}; use serde::{Deserialize, Serialize}; -use ssz_derive::{Decode, Encode}; -use tree_hash_derive::TreeHash; +use ssz::{Decode, DecodeError, Encode}; +use tree_hash::TreeHash; -use crate::leansig::{LeanSigScheme, errors::LeanSigError, public_key::PublicKey}; +use crate::leansig::{LeanSigScheme, public_key::PublicKey}; -const SIGNATURE_SIZE: usize = 3112; +/// The inner leansig signature type with built-in SSZ support. +pub type LeanSigSignature = ::Signature; -type LeanSigSignature = ::Signature; - -/// Wrapper around a fixed-size serialized hash-based signature. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, Copy)] +/// Wrapper around leansig's signature type. +/// Uses leansig's built-in SSZ encoding for interoperability with other clients. +#[derive(Clone, Serialize, Deserialize)] pub struct Signature { - pub inner: FixedBytes, + pub inner: LeanSigSignature, +} + +impl std::fmt::Debug for Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Signature") + .field("inner", &"") + .finish() + } } -impl From<&[u8]> for Signature { - fn from(value: &[u8]) -> Self { - Self { - inner: FixedBytes::from_slice(value), - } +impl PartialEq for Signature { + fn eq(&self, other: &Self) -> bool { + // Compare by SSZ encoding since LeanSigSignature doesn't implement PartialEq + self.inner.as_ssz_bytes() == other.inner.as_ssz_bytes() + } +} + +impl Eq for Signature {} + +impl Encode for Signature { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + self.inner.ssz_bytes_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.inner.ssz_append(buf) + } +} + +impl Decode for Signature { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + Ok(Self { + inner: LeanSigSignature::from_ssz_bytes(bytes)?, + }) + } +} + +impl TreeHash for Signature { + fn tree_hash_type() -> tree_hash::TreeHashType { + // Signatures are variable-length containers + tree_hash::TreeHashType::Container + } + + fn tree_hash_packed_encoding(&self) -> tree_hash::PackedEncoding { + unreachable!("Signature is not a basic type") + } + + fn tree_hash_packing_factor() -> usize { + unreachable!("Signature is not a basic type") + } + + fn tree_hash_root(&self) -> tree_hash::Hash256 { + // Hash the SSZ encoding as the tree hash root + // This matches how variable-length containers are hashed + let bytes = self.inner.as_ssz_bytes(); + tree_hash::merkle_root(&bytes, 0) } } impl Signature { - pub fn new(inner: FixedBytes) -> Self { + pub fn new(inner: LeanSigSignature) -> Self { Self { inner } } + /// Create a blank/placeholder signature. + /// + /// Note: This generates a real mock signature under the hood which is expensive. + /// Only use in contexts where the signature won't be validated. pub fn blank() -> Self { - Self::new(Default::default()) + Self::mock() } /// Create a mock signature for testing purposes. @@ -48,15 +107,12 @@ impl Signature { .expect("Mock signature generation failed") } - pub fn from_lean_sig(signature: LeanSigSignature) -> Result { - Ok(Self { - inner: FixedBytes::try_from(signature.to_bytes().as_slice())?, - }) + pub fn from_lean_sig(signature: LeanSigSignature) -> Self { + Self { inner: signature } } - pub fn as_lean_sig(&self) -> anyhow::Result { - LeanSigSignature::from_bytes(self.inner.as_slice()) - .map_err(|err| anyhow!("Failed to decode LeanSigSignature from SSZ: {err:?}")) + pub fn as_lean_sig(&self) -> &LeanSigSignature { + &self.inner } pub fn verify( @@ -69,7 +125,7 @@ impl Signature { &public_key.as_lean_sig()?, epoch, message, - &self.as_lean_sig()?, + &self.inner, )) } } @@ -77,6 +133,7 @@ impl Signature { #[cfg(test)] mod tests { use rand::rng; + use ssz::{Decode, Encode}; use crate::leansig::{private_key::PrivateKey, signature::Signature}; @@ -100,13 +157,11 @@ mod tests { assert!(result.is_ok(), "Signing should succeed"); let signature = result.unwrap(); - // convert to leansig signature - let hash_sig_signature = signature.as_lean_sig().unwrap(); - - // convert back to signature - let signature_returned = Signature::from_lean_sig(hash_sig_signature).unwrap(); + // SSZ roundtrip test + let ssz_bytes = signature.as_ssz_bytes(); + let signature_decoded = Signature::from_ssz_bytes(&ssz_bytes).unwrap(); // verify roundtrip - assert_eq!(signature, signature_returned); + assert_eq!(signature, signature_decoded); } } From 230e188dda6c8f8832368211d291d832a0ca9133 Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 10 Feb 2026 18:50:22 +0700 Subject: [PATCH 03/20] fix ssz tests --- crates/common/fork_choice/lean/src/store.rs | 7 ++++--- testing/lean-spec-tests/tests/tests.rs | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/common/fork_choice/lean/src/store.rs b/crates/common/fork_choice/lean/src/store.rs index 9f19e0042..f54ca6461 100644 --- a/crates/common/fork_choice/lean/src/store.rs +++ b/crates/common/fork_choice/lean/src/store.rs @@ -50,6 +50,7 @@ use ream_storage::{ }, }; use ream_sync::rwlock::{Reader, Writer}; +use ssz::{Decode, Encode}; use ssz_types::{BitList, VariableList, typenum::U4096}; use tokio::sync::Mutex; use tree_hash::TreeHash; @@ -1323,8 +1324,7 @@ impl Store { let signature_bytes = signed_block_with_attestation .signature .proposer_signature - .inner - .to_vec(); + .as_ssz_bytes(); let proof_data = signature_bytes.try_into().map_err(|err| { anyhow!("Failed to convert proposer signature to VariableList {err:?}") @@ -1505,7 +1505,8 @@ impl Store { }) .collect::>>()?; - let sig = Signature::from(proof.proof_data.as_ref()); + let sig = Signature::from_ssz_bytes(proof.proof_data.as_ref()) + .map_err(|err| anyhow!("Failed to decode signature: {err:?}"))?; for pubkey in &public_keys { let is_valid = sig.verify(pubkey, attestation_slot as u32, &data_root.0)?; diff --git a/testing/lean-spec-tests/tests/tests.rs b/testing/lean-spec-tests/tests/tests.rs index 206e5e1c6..384d23ceb 100644 --- a/testing/lean-spec-tests/tests/tests.rs +++ b/testing/lean-spec-tests/tests/tests.rs @@ -169,7 +169,10 @@ fn test_all_ssz_fixtures() { .with_env_filter(env_filter) .try_init(); - let fixtures = find_json_files("fixtures/consensus/ssz/devnet"); + #[cfg(feature = "devnet2")] + let fixtures = find_json_files("fixtures/devnet2/ssz/devnet"); + #[cfg(feature = "devnet3")] + let fixtures = find_json_files("fixtures/devnet3/ssz/devnet"); if fixtures.is_empty() { info!( From 68efe495b0f03c38f8cfd82428653c95b13592b9 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 14:58:54 +0700 Subject: [PATCH 04/20] fix linting --- crates/common/fork_choice/lean/src/store.rs | 15 ++- testing/lean-spec-tests/src/ssz_test.rs | 93 +++++++++++-------- testing/lean-spec-tests/src/types/ssz_test.rs | 29 +++--- 3 files changed, 83 insertions(+), 54 deletions(-) diff --git a/crates/common/fork_choice/lean/src/store.rs b/crates/common/fork_choice/lean/src/store.rs index f54ca6461..1676ffc9a 100644 --- a/crates/common/fork_choice/lean/src/store.rs +++ b/crates/common/fork_choice/lean/src/store.rs @@ -1276,7 +1276,10 @@ impl Store { proposer_attestation.validator_id, &proposer_attestation.data, ), - signed_block_with_attestation.signature.proposer_signature.clone(), + signed_block_with_attestation + .signature + .proposer_signature + .clone(), )?; #[cfg(feature = "devnet3")] @@ -1294,7 +1297,10 @@ impl Store { proposer_attestation.validator_id, &proposer_attestation.data, ), - signed_block_with_attestation.signature.proposer_signature.clone(), + signed_block_with_attestation + .signature + .proposer_signature + .clone(), )?; } } @@ -1305,7 +1311,10 @@ impl Store { SignedAttestation { validator_id: proposer_attestation.validator_id, message: proposer_attestation.data.clone(), - signature: signed_block_with_attestation.signature.proposer_signature.clone(), + signature: signed_block_with_attestation + .signature + .proposer_signature + .clone(), }, false, ) diff --git a/testing/lean-spec-tests/src/ssz_test.rs b/testing/lean-spec-tests/src/ssz_test.rs index c95420a71..271bb47f3 100644 --- a/testing/lean-spec-tests/src/ssz_test.rs +++ b/testing/lean-spec-tests/src/ssz_test.rs @@ -3,8 +3,13 @@ use std::path::Path; use alloy_primitives::{B256, hex}; use anyhow::{anyhow, bail}; use ream_consensus_lean::{ - attestation::{AggregatedAttestation, AggregatedAttestations, AttestationData, SignedAttestation}, - block::{Block, BlockBody, BlockHeader, BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, + attestation::{ + AggregatedAttestation, AggregatedAttestations, AttestationData, SignedAttestation, + }, + block::{ + Block, BlockBody, BlockHeader, BlockSignatures, BlockWithAttestation, + SignedBlockWithAttestation, + }, checkpoint::Checkpoint, config::Config, state::LeanState, @@ -17,10 +22,10 @@ use tree_hash::TreeHash; use crate::types::{ TestFixture, ssz_test::{ - SSZTest, AggregatedAttestationJSON, AttestationDataJSON, AttestationJSON, - BlockBodyJSON, BlockHeaderJSON, BlockJSON, BlockSignaturesJSON, BlockWithAttestationJSON, - CheckpointJSON, ConfigJSON, SignedAttestationJSON, SignedBlockWithAttestationJSON, - StateJSON, ValidatorJSON, + AggregatedAttestationJSON, AttestationDataJSON, AttestationJSON, BlockBodyJSON, + BlockHeaderJSON, BlockJSON, BlockSignaturesJSON, BlockWithAttestationJSON, CheckpointJSON, + ConfigJSON, SSZTest, SignedAttestationJSON, SignedBlockWithAttestationJSON, StateJSON, + ValidatorJSON, }, }; @@ -58,24 +63,30 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { "Checkpoint" => { run_test::(&test.value, &expected_serialized, expected_root) } - "AttestationData" => { - run_test::(&test.value, &expected_serialized, expected_root) - } - "AggregatedAttestation" => { - run_test::(&test.value, &expected_serialized, expected_root) - } - "Attestation" => { - run_test::(&test.value, &expected_serialized, expected_root) - } + "AttestationData" => run_test::( + &test.value, + &expected_serialized, + expected_root, + ), + "AggregatedAttestation" => run_test::( + &test.value, + &expected_serialized, + expected_root, + ), + "Attestation" => run_test::( + &test.value, + &expected_serialized, + expected_root, + ), "BlockBody" => { run_test::(&test.value, &expected_serialized, expected_root) } - "BlockHeader" => { - run_test::(&test.value, &expected_serialized, expected_root) - } - "Block" => { - run_test::(&test.value, &expected_serialized, expected_root) - } + "BlockHeader" => run_test::( + &test.value, + &expected_serialized, + expected_root, + ), + "Block" => run_test::(&test.value, &expected_serialized, expected_root), "Config" => { run_test::(&test.value, &expected_serialized, expected_root) } @@ -86,18 +97,24 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { run_test::(&test.value, &expected_serialized, expected_root) } // Types without proper TreeHash implementation - only test SSZ serialization - "SignedAttestation" => { - run_test_ssz_only::(&test.value, &expected_serialized) - } - "BlockSignatures" => { - run_test_ssz_only::(&test.value, &expected_serialized) - } + "SignedAttestation" => run_test_ssz_only::( + &test.value, + &expected_serialized, + ), + "BlockSignatures" => run_test_ssz_only::( + &test.value, + &expected_serialized, + ), "BlockWithAttestation" => { - run_test_ssz_only::(&test.value, &expected_serialized) - } - "SignedBlockWithAttestation" => { - run_test_ssz_only::(&test.value, &expected_serialized) + run_test_ssz_only::( + &test.value, + &expected_serialized, + ) } + "SignedBlockWithAttestation" => run_test_ssz_only::< + SignedBlockWithAttestationJSON, + SignedBlockWithAttestation, + >(&test.value, &expected_serialized), _ => { warn!("Unknown type: {}, skipping", test.type_name); Ok(()) @@ -117,9 +134,8 @@ where T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode + TreeHash, { // Deserialize into intermediate JSON type - let json_value: J = serde_json::from_value(value.clone()).map_err(|err| { - anyhow!("Failed to deserialize JSON value: {err}") - })?; + let json_value: J = serde_json::from_value(value.clone()) + .map_err(|err| anyhow!("Failed to deserialize JSON value: {err}"))?; // Convert to ream type let typed_value: T = (&json_value).try_into()?; @@ -137,9 +153,7 @@ where // Compute tree hash root let root = typed_value.tree_hash_root(); if root != expected_root { - bail!( - "Tree hash root mismatch:\n expected: {expected_root}\n got: {root}" - ); + bail!("Tree hash root mismatch:\n expected: {expected_root}\n got: {root}"); } Ok(()) @@ -155,9 +169,8 @@ where T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode, { // Deserialize into intermediate JSON type - let json_value: J = serde_json::from_value(value.clone()).map_err(|err| { - anyhow!("Failed to deserialize JSON value: {err}") - })?; + let json_value: J = serde_json::from_value(value.clone()) + .map_err(|err| anyhow!("Failed to deserialize JSON value: {err}"))?; // Convert to ream type let typed_value: T = (&json_value).try_into()?; diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 49465efe3..8d2cfaaa3 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -10,8 +10,7 @@ use ream_consensus_lean::{ AggregatedAttestation as ReamAggregatedAttestation, AggregatedAttestations as ReamAggregatedAttestations, AggregatedSignatureProof as ReamAggregatedSignatureProof, - AttestationData as ReamAttestationData, - SignedAttestation as ReamSignedAttestation, + AttestationData as ReamAttestationData, SignedAttestation as ReamSignedAttestation, }, block::{ Block as ReamBlock, BlockBody as ReamBlockBody, BlockHeader as ReamBlockHeader, @@ -26,7 +25,10 @@ use ream_consensus_lean::{ use ream_post_quantum_crypto::leansig::{public_key::PublicKey, signature::Signature}; use serde::Deserialize; use ssz::Decode; -use ssz_types::{BitList, VariableList, typenum::{U4096, U1048576}}; +use ssz_types::{ + BitList, VariableList, + typenum::{U4096, U1048576}, +}; /// SSZ test case from leanSpec fixtures #[derive(Debug, Deserialize)] @@ -305,12 +307,13 @@ impl TryFrom<&StateJSON> for ReamState { // Convert justifications_validators to BitList let justifications_len = state.justifications_validators.data.len(); let mut justifications_validators = - BitList::::with_capacity(justifications_len) - .map_err(|err| anyhow!("Failed to create justifications_validators BitList: {err:?}"))?; + BitList::::with_capacity(justifications_len).map_err(|err| { + anyhow!("Failed to create justifications_validators BitList: {err:?}") + })?; for (i, &bit) in state.justifications_validators.data.iter().enumerate() { - justifications_validators - .set(i, bit) - .map_err(|err| anyhow!("Failed to set justifications_validators bit at {i}: {err:?}"))?; + justifications_validators.set(i, bit).map_err(|err| { + anyhow!("Failed to set justifications_validators bit at {i}: {err:?}") + })?; } Ok(ReamState { @@ -319,13 +322,17 @@ impl TryFrom<&StateJSON> for ReamState { latest_block_header: (&state.latest_block_header).try_into()?, latest_justified: (&state.latest_justified).try_into()?, latest_finalized: (&state.latest_finalized).try_into()?, - historical_block_hashes: VariableList::try_from(state.historical_block_hashes.data.clone()) - .map_err(|err| anyhow!("Failed to create historical_block_hashes: {err}"))?, + historical_block_hashes: VariableList::try_from( + state.historical_block_hashes.data.clone(), + ) + .map_err(|err| anyhow!("Failed to create historical_block_hashes: {err}"))?, justified_slots, validators: VariableList::try_from(validators) .map_err(|err| anyhow!("Failed to create validators: {err}"))?, justifications_roots: VariableList::try_from(state.justifications_roots.data.clone()) - .map_err(|err| anyhow!("Failed to create justifications_roots: {err}"))?, + .map_err(|err| { + anyhow!("Failed to create justifications_roots: {err}") + })?, justifications_validators, }) } From 32a7f8cb280cb1f8b2e1945382735ffc54366633 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 15:44:34 +0700 Subject: [PATCH 05/20] fix linting --- crates/common/fork_choice/lean/src/store.rs | 1 + testing/lean-spec-tests/Cargo.toml | 2 +- testing/lean-spec-tests/tests/tests.rs | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/common/fork_choice/lean/src/store.rs b/crates/common/fork_choice/lean/src/store.rs index 1676ffc9a..2b4128847 100644 --- a/crates/common/fork_choice/lean/src/store.rs +++ b/crates/common/fork_choice/lean/src/store.rs @@ -50,6 +50,7 @@ use ream_storage::{ }, }; use ream_sync::rwlock::{Reader, Writer}; +#[cfg(feature = "devnet3")] use ssz::{Decode, Encode}; use ssz_types::{BitList, VariableList, typenum::U4096}; use tokio::sync::Mutex; diff --git a/testing/lean-spec-tests/Cargo.toml b/testing/lean-spec-tests/Cargo.toml index 293b0f1fd..fd155f9b6 100644 --- a/testing/lean-spec-tests/Cargo.toml +++ b/testing/lean-spec-tests/Cargo.toml @@ -18,9 +18,9 @@ devnet3 = ["ream-consensus-lean/devnet3", "ream-fork-choice-lean/devnet3"] [dependencies] alloy-primitives.workspace = true anyhow.workspace = true -ssz = { package = "ethereum_ssz", version = "0.10" } serde.workspace = true serde_json.workspace = true +ssz = { package = "ethereum_ssz", version = "0.10" } ssz_types.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/testing/lean-spec-tests/tests/tests.rs b/testing/lean-spec-tests/tests/tests.rs index 384d23ceb..66207a714 100644 --- a/testing/lean-spec-tests/tests/tests.rs +++ b/testing/lean-spec-tests/tests/tests.rs @@ -2,8 +2,10 @@ use std::{env, fs, path::PathBuf}; #[cfg(feature = "devnet2")] use lean_spec_tests::fork_choice::{load_fork_choice_test, run_fork_choice_test}; -use lean_spec_tests::ssz_test::{load_ssz_test, run_ssz_test}; -use lean_spec_tests::state_transition::{load_state_transition_test, run_state_transition_test}; +use lean_spec_tests::{ + ssz_test::{load_ssz_test, run_ssz_test}, + state_transition::{load_state_transition_test, run_state_transition_test}, +}; use tracing::{debug, error, info, warn}; use tracing_subscriber::EnvFilter; From aca1153229b8d0c7bc58985afc47801b89f0d775 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 16:31:13 +0700 Subject: [PATCH 06/20] fix linting --- testing/lean-spec-tests/src/ssz_test.rs | 152 +++++++++++------------- 1 file changed, 70 insertions(+), 82 deletions(-) diff --git a/testing/lean-spec-tests/src/ssz_test.rs b/testing/lean-spec-tests/src/ssz_test.rs index 271bb47f3..45d336644 100644 --- a/testing/lean-spec-tests/src/ssz_test.rs +++ b/testing/lean-spec-tests/src/ssz_test.rs @@ -22,10 +22,9 @@ use tree_hash::TreeHash; use crate::types::{ TestFixture, ssz_test::{ - AggregatedAttestationJSON, AttestationDataJSON, AttestationJSON, BlockBodyJSON, - BlockHeaderJSON, BlockJSON, BlockSignaturesJSON, BlockWithAttestationJSON, CheckpointJSON, - ConfigJSON, SSZTest, SignedAttestationJSON, SignedBlockWithAttestationJSON, StateJSON, - ValidatorJSON, + AggregatedAttestationJSON, AttestationJSON, BlockBodyJSON, BlockHeaderJSON, BlockJSON, + BlockSignaturesJSON, BlockWithAttestationJSON, ConfigJSON, SSZTest, SignedAttestationJSON, + SignedBlockWithAttestationJSON, StateJSON, ValidatorJSON, }, }; @@ -54,67 +53,59 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { debug!(" Network: {}", test.network); debug!(" Type: {}", test.type_name); - // Parse expected values - let expected_serialized = parse_hex_bytes(&test.serialized)?; + let expected_ssz = parse_hex_bytes(&test.serialized)?; let expected_root = test.root; - // Run the test based on type - using intermediate JSON types and converting to ream types match test.type_name.as_str() { - "Checkpoint" => { - run_test::(&test.value, &expected_serialized, expected_root) + // Types that deserialize directly (snake_case in JSON) + "Checkpoint" => run_test_direct::(&test.value, &expected_ssz, expected_root), + "AttestationData" => { + run_test_direct::(&test.value, &expected_ssz, expected_root) } - "AttestationData" => run_test::( - &test.value, - &expected_serialized, - expected_root, - ), + + // Types with JSON intermediate conversion "AggregatedAttestation" => run_test::( &test.value, - &expected_serialized, + &expected_ssz, expected_root, ), "Attestation" => run_test::( &test.value, - &expected_serialized, + &expected_ssz, expected_root, ), "BlockBody" => { - run_test::(&test.value, &expected_serialized, expected_root) + run_test::(&test.value, &expected_ssz, expected_root) } - "BlockHeader" => run_test::( - &test.value, - &expected_serialized, - expected_root, - ), - "Block" => run_test::(&test.value, &expected_serialized, expected_root), - "Config" => { - run_test::(&test.value, &expected_serialized, expected_root) + "BlockHeader" => { + run_test::(&test.value, &expected_ssz, expected_root) } + "Block" => run_test::(&test.value, &expected_ssz, expected_root), + "Config" => run_test::(&test.value, &expected_ssz, expected_root), "Validator" => { - run_test::(&test.value, &expected_serialized, expected_root) + run_test::(&test.value, &expected_ssz, expected_root) } - "State" => { - run_test::(&test.value, &expected_serialized, expected_root) - } - // Types without proper TreeHash implementation - only test SSZ serialization + "State" => run_test::(&test.value, &expected_ssz, expected_root), + + // Types without TreeHash - SSZ only "SignedAttestation" => run_test_ssz_only::( &test.value, - &expected_serialized, - ), - "BlockSignatures" => run_test_ssz_only::( - &test.value, - &expected_serialized, + &expected_ssz, ), + "BlockSignatures" => { + run_test_ssz_only::(&test.value, &expected_ssz) + } "BlockWithAttestation" => { run_test_ssz_only::( &test.value, - &expected_serialized, + &expected_ssz, ) } "SignedBlockWithAttestation" => run_test_ssz_only::< SignedBlockWithAttestationJSON, SignedBlockWithAttestation, - >(&test.value, &expected_serialized), + >(&test.value, &expected_ssz), + _ => { warn!("Unknown type: {}, skipping", test.type_name); Ok(()) @@ -122,73 +113,67 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { } } -/// Run a test by deserializing JSON into intermediate type, converting to ream type, -/// then verifying SSZ serialization and tree hash root. +/// Run test with JSON intermediate type conversion fn run_test( value: &serde_json::Value, - expected_serialized: &[u8], + expected_ssz: &[u8], expected_root: B256, ) -> anyhow::Result<()> where J: serde::de::DeserializeOwned, T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode + TreeHash, { - // Deserialize into intermediate JSON type let json_value: J = serde_json::from_value(value.clone()) - .map_err(|err| anyhow!("Failed to deserialize JSON value: {err}"))?; - - // Convert to ream type + .map_err(|err| anyhow!("Failed to deserialize JSON: {err}"))?; let typed_value: T = (&json_value).try_into()?; - - // SSZ serialize - let serialized = typed_value.as_ssz_bytes(); - if serialized != expected_serialized { - bail!( - "SSZ serialization mismatch:\n expected: 0x{}\n got: 0x{}", - hex::encode(expected_serialized), - hex::encode(&serialized) - ); - } - - // Compute tree hash root - let root = typed_value.tree_hash_root(); - if root != expected_root { - bail!("Tree hash root mismatch:\n expected: {expected_root}\n got: {root}"); - } - - Ok(()) + verify_ssz(&typed_value, expected_ssz)?; + verify_root(&typed_value, expected_root) } -/// Run a test for types without TreeHash - only verify SSZ serialization. -fn run_test_ssz_only( +/// Run test for types that deserialize directly +fn run_test_direct( value: &serde_json::Value, - expected_serialized: &[u8], -) -> anyhow::Result<()> + expected_ssz: &[u8], + expected_root: B256, +) -> anyhow::Result<()> { + let typed_value: T = serde_json::from_value(value.clone()) + .map_err(|err| anyhow!("Failed to deserialize JSON: {err}"))?; + verify_ssz(&typed_value, expected_ssz)?; + verify_root(&typed_value, expected_root) +} + +/// Run test for types without TreeHash (SSZ only) +fn run_test_ssz_only(value: &serde_json::Value, expected_ssz: &[u8]) -> anyhow::Result<()> where J: serde::de::DeserializeOwned, T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode, { - // Deserialize into intermediate JSON type let json_value: J = serde_json::from_value(value.clone()) - .map_err(|err| anyhow!("Failed to deserialize JSON value: {err}"))?; - - // Convert to ream type + .map_err(|err| anyhow!("Failed to deserialize JSON: {err}"))?; let typed_value: T = (&json_value).try_into()?; + verify_ssz(&typed_value, expected_ssz) +} - // SSZ serialize - let serialized = typed_value.as_ssz_bytes(); - if serialized != expected_serialized { +fn verify_ssz(value: &T, expected: &[u8]) -> anyhow::Result<()> { + let actual = value.as_ssz_bytes(); + if actual != expected { bail!( - "SSZ serialization mismatch:\n expected: 0x{}\n got: 0x{}", - hex::encode(expected_serialized), - hex::encode(&serialized) + "SSZ mismatch:\n expected: 0x{}\n got: 0x{}", + hex::encode(expected), + hex::encode(&actual) ); } + Ok(()) +} +fn verify_root(value: &T, expected: B256) -> anyhow::Result<()> { + let actual = value.tree_hash_root(); + if actual != expected { + bail!("TreeHash mismatch:\n expected: {expected}\n got: {actual}"); + } Ok(()) } -/// Parse a hex string (with 0x prefix) into bytes fn parse_hex_bytes(hex_str: &str) -> anyhow::Result> { let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); hex::decode(hex_str).map_err(|err| anyhow!("Failed to parse hex: {err}")) @@ -200,10 +185,13 @@ mod tests { #[test] fn test_parse_hex_bytes() { - let bytes = parse_hex_bytes("0xdeadbeef").unwrap(); - assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]); - - let bytes = parse_hex_bytes("deadbeef").unwrap(); - assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]); + assert_eq!( + parse_hex_bytes("0xdeadbeef").unwrap(), + vec![0xde, 0xad, 0xbe, 0xef] + ); + assert_eq!( + parse_hex_bytes("deadbeef").unwrap(), + vec![0xde, 0xad, 0xbe, 0xef] + ); } } From 48aa070c35d785137933f5da1f69fc939790d111 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 16:31:37 +0700 Subject: [PATCH 07/20] simplify ssz_test.rs --- testing/lean-spec-tests/src/types/ssz_test.rs | 60 ++++--------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 8d2cfaaa3..653aeb742 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -63,46 +63,6 @@ impl TryFrom<&ConfigJSON> for ReamConfig { } } -/// Checkpoint - already snake_case in JSON, but define for consistency -#[derive(Debug, Deserialize, Clone)] -pub struct CheckpointJSON { - pub root: B256, - pub slot: u64, -} - -impl TryFrom<&CheckpointJSON> for ReamCheckpoint { - type Error = anyhow::Error; - - fn try_from(cp: &CheckpointJSON) -> anyhow::Result { - Ok(ReamCheckpoint { - root: cp.root, - slot: cp.slot, - }) - } -} - -/// AttestationData - already snake_case in JSON -#[derive(Debug, Deserialize, Clone)] -pub struct AttestationDataJSON { - pub slot: u64, - pub head: CheckpointJSON, - pub target: CheckpointJSON, - pub source: CheckpointJSON, -} - -impl TryFrom<&AttestationDataJSON> for ReamAttestationData { - type Error = anyhow::Error; - - fn try_from(data: &AttestationDataJSON) -> anyhow::Result { - Ok(ReamAttestationData { - slot: data.slot, - head: (&data.head).try_into()?, - target: (&data.target).try_into()?, - source: (&data.source).try_into()?, - }) - } -} - /// BlockHeader with camelCase fields #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -174,7 +134,7 @@ pub struct AggregationBitsJSON { #[serde(rename_all = "camelCase")] pub struct AggregatedAttestationJSON { pub aggregation_bits: AggregationBitsJSON, - pub data: AttestationDataJSON, + pub data: ReamAttestationData, } impl TryFrom<&AggregatedAttestationJSON> for ReamAggregatedAttestation { @@ -193,7 +153,7 @@ impl TryFrom<&AggregatedAttestationJSON> for ReamAggregatedAttestation { Ok(ReamAggregatedAttestation { aggregation_bits, - message: (&att.data).try_into()?, + message: att.data.clone(), }) } } @@ -203,7 +163,7 @@ impl TryFrom<&AggregatedAttestationJSON> for ReamAggregatedAttestation { #[serde(rename_all = "camelCase")] pub struct AttestationJSON { pub validator_id: u64, - pub data: AttestationDataJSON, + pub data: ReamAttestationData, } impl TryFrom<&AttestationJSON> for ReamAggregatedAttestations { @@ -212,7 +172,7 @@ impl TryFrom<&AttestationJSON> for ReamAggregatedAttestations { fn try_from(att: &AttestationJSON) -> anyhow::Result { Ok(ReamAggregatedAttestations { validator_id: att.validator_id, - data: (&att.data).try_into()?, + data: att.data.clone(), }) } } @@ -271,8 +231,8 @@ pub struct StateJSON { pub config: ConfigJSON, pub slot: u64, pub latest_block_header: BlockHeaderJSON, - pub latest_justified: CheckpointJSON, - pub latest_finalized: CheckpointJSON, + pub latest_justified: ReamCheckpoint, + pub latest_finalized: ReamCheckpoint, pub historical_block_hashes: DataListJSON, pub justified_slots: DataListJSON, pub validators: DataListJSON, @@ -320,8 +280,8 @@ impl TryFrom<&StateJSON> for ReamState { config: (&state.config).try_into()?, slot: state.slot, latest_block_header: (&state.latest_block_header).try_into()?, - latest_justified: (&state.latest_justified).try_into()?, - latest_finalized: (&state.latest_finalized).try_into()?, + latest_justified: state.latest_justified, + latest_finalized: state.latest_finalized, historical_block_hashes: VariableList::try_from( state.historical_block_hashes.data.clone(), ) @@ -347,7 +307,7 @@ impl TryFrom<&StateJSON> for ReamState { #[serde(rename_all = "camelCase")] pub struct SignedAttestationJSON { pub validator_id: u64, - pub message: AttestationDataJSON, + pub message: ReamAttestationData, pub signature: String, } @@ -364,7 +324,7 @@ impl TryFrom<&SignedAttestationJSON> for ReamSignedAttestation { Ok(ReamSignedAttestation { validator_id: att.validator_id, - message: (&att.message).try_into()?, + message: att.message.clone(), signature, }) } From eef7fee710aefe30dca3aa4030b1d9f901dc1898 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 16:43:28 +0700 Subject: [PATCH 08/20] remove unnecessary conversions --- testing/lean-spec-tests/src/types/ssz_test.rs | 319 ++++++++---------- testing/lean-spec-tests/tests/tests.rs | 3 - 2 files changed, 135 insertions(+), 187 deletions(-) diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 653aeb742..9f3dc34db 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -1,7 +1,6 @@ -//! SSZ test types with proper serde attributes for leanSpec JSON format. +//! Intermediate JSON types for leanSpec SSZ test fixtures. //! -//! These intermediate types handle the camelCase JSON format from leanSpec fixtures -//! and provide conversions to the actual ream-consensus-lean types. +//! These types handle camelCase JSON and convert to ream-consensus-lean types. use alloy_primitives::B256; use anyhow::anyhow; @@ -27,10 +26,37 @@ use serde::Deserialize; use ssz::Decode; use ssz_types::{ BitList, VariableList, - typenum::{U4096, U1048576}, + typenum::{U262144, U1048576, U1073741824}, }; -/// SSZ test case from leanSpec fixtures +// ============================================================================ +// Helpers +// ============================================================================ + +fn decode_hex(hex: &str) -> anyhow::Result> { + alloy_primitives::hex::decode(hex.trim_start_matches("0x")) + .map_err(|error| anyhow!("hex decode failed: {error}")) +} + +fn decode_signature(hex: &str) -> anyhow::Result { + Signature::from_ssz_bytes(&decode_hex(hex)?) + .map_err(|error| anyhow!("signature decode failed: {error:?}")) +} + +fn bools_to_bitlist(bools: &[bool]) -> anyhow::Result> { + let mut bits = BitList::::with_capacity(bools.len()) + .map_err(|error| anyhow!("BitList creation failed: {error:?}"))?; + for (index, &bit) in bools.iter().enumerate() { + bits.set(index, bit) + .map_err(|error| anyhow!("BitList set failed: {error:?}"))?; + } + Ok(bits) +} + +// ============================================================================ +// Test case structure +// ============================================================================ + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SSZTest { @@ -43,10 +69,28 @@ pub struct SSZTest { } // ============================================================================ -// Intermediate types for JSON deserialization (camelCase) +// JSON wrapper types +// ============================================================================ + +#[derive(Debug, Deserialize, Clone)] +pub struct DataListJSON { + pub data: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct AggregationBitsJSON { + pub data: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ProofDataJSON { + pub data: String, +} + +// ============================================================================ +// Config // ============================================================================ -/// Config with camelCase fields #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ConfigJSON { @@ -55,7 +99,6 @@ pub struct ConfigJSON { impl TryFrom<&ConfigJSON> for ReamConfig { type Error = anyhow::Error; - fn try_from(config: &ConfigJSON) -> anyhow::Result { Ok(ReamConfig { genesis_time: config.genesis_time, @@ -63,7 +106,10 @@ impl TryFrom<&ConfigJSON> for ReamConfig { } } -/// BlockHeader with camelCase fields +// ============================================================================ +// BlockHeader +// ============================================================================ + #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct BlockHeaderJSON { @@ -76,7 +122,6 @@ pub struct BlockHeaderJSON { impl TryFrom<&BlockHeaderJSON> for ReamBlockHeader { type Error = anyhow::Error; - fn try_from(header: &BlockHeaderJSON) -> anyhow::Result { Ok(ReamBlockHeader { slot: header.slot, @@ -88,7 +133,10 @@ impl TryFrom<&BlockHeaderJSON> for ReamBlockHeader { } } -/// Validator with camelCase fields +// ============================================================================ +// Validator +// ============================================================================ + #[derive(Debug, Deserialize, Clone)] pub struct ValidatorJSON { pub pubkey: String, @@ -97,39 +145,22 @@ pub struct ValidatorJSON { impl TryFrom<&ValidatorJSON> for ReamValidator { type Error = anyhow::Error; - fn try_from(validator: &ValidatorJSON) -> anyhow::Result { - let pubkey_hex = validator.pubkey.trim_start_matches("0x"); - let pubkey_bytes = alloy_primitives::hex::decode(pubkey_hex) - .map_err(|err| anyhow!("Failed to decode validator pubkey hex: {err}"))?; - - if pubkey_bytes.len() != 52 { - return Err(anyhow!( - "Expected 52-byte pubkey, got {} bytes", - pubkey_bytes.len() - )); + let bytes = decode_hex(&validator.pubkey)?; + if bytes.len() != 52 { + return Err(anyhow!("Expected 52-byte pubkey, got {}", bytes.len())); } - Ok(ReamValidator { - public_key: PublicKey::from(&pubkey_bytes[..]), + public_key: PublicKey::from(&bytes[..]), index: validator.index, }) } } -/// Wrapper for data lists in JSON format -#[derive(Debug, Deserialize, Clone)] -pub struct DataListJSON { - pub data: Vec, -} - -/// AggregationBits wrapper -#[derive(Debug, Deserialize, Clone)] -pub struct AggregationBitsJSON { - pub data: Vec, -} +// ============================================================================ +// Attestations +// ============================================================================ -/// AggregatedAttestation with camelCase fields #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AggregatedAttestationJSON { @@ -139,26 +170,14 @@ pub struct AggregatedAttestationJSON { impl TryFrom<&AggregatedAttestationJSON> for ReamAggregatedAttestation { type Error = anyhow::Error; - - fn try_from(att: &AggregatedAttestationJSON) -> anyhow::Result { - let bool_data = &att.aggregation_bits.data; - let mut aggregation_bits = BitList::::with_capacity(bool_data.len()) - .map_err(|err| anyhow!("Failed to create BitList: {err:?}"))?; - - for (i, &bit) in bool_data.iter().enumerate() { - aggregation_bits - .set(i, bit) - .map_err(|err| anyhow!("Failed to set bit at index {i}: {err:?}"))?; - } - + fn try_from(attestation: &AggregatedAttestationJSON) -> anyhow::Result { Ok(ReamAggregatedAttestation { - aggregation_bits, - message: att.data.clone(), + aggregation_bits: bools_to_bitlist(&attestation.aggregation_bits.data)?, + message: attestation.data.clone(), }) } } -/// Attestation (individual) with camelCase fields - maps to AggregatedAttestations in Rust #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AttestationJSON { @@ -168,16 +187,37 @@ pub struct AttestationJSON { impl TryFrom<&AttestationJSON> for ReamAggregatedAttestations { type Error = anyhow::Error; - - fn try_from(att: &AttestationJSON) -> anyhow::Result { + fn try_from(attestation: &AttestationJSON) -> anyhow::Result { Ok(ReamAggregatedAttestations { - validator_id: att.validator_id, - data: att.data.clone(), + validator_id: attestation.validator_id, + data: attestation.data.clone(), + }) + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SignedAttestationJSON { + pub validator_id: u64, + pub message: ReamAttestationData, + pub signature: String, +} + +impl TryFrom<&SignedAttestationJSON> for ReamSignedAttestation { + type Error = anyhow::Error; + fn try_from(attestation: &SignedAttestationJSON) -> anyhow::Result { + Ok(ReamSignedAttestation { + validator_id: attestation.validator_id, + message: attestation.message.clone(), + signature: decode_signature(&attestation.signature)?, }) } } -/// BlockBody with attestations in {data: [...]} format +// ============================================================================ +// Block +// ============================================================================ + #[derive(Debug, Deserialize, Clone)] pub struct BlockBodyJSON { pub attestations: DataListJSON, @@ -185,21 +225,20 @@ pub struct BlockBodyJSON { impl TryFrom<&BlockBodyJSON> for ReamBlockBody { type Error = anyhow::Error; - fn try_from(body: &BlockBodyJSON) -> anyhow::Result { - let mut attestations = Vec::new(); - for att in &body.attestations.data { - attestations.push(ReamAggregatedAttestation::try_from(att)?); - } - + let attestations: Vec<_> = body + .attestations + .data + .iter() + .map(TryInto::try_into) + .collect::>()?; Ok(ReamBlockBody { attestations: VariableList::try_from(attestations) - .map_err(|err| anyhow!("Failed to create attestations VariableList: {err}"))?, + .map_err(|error| anyhow!("{error}"))?, }) } } -/// Block with camelCase fields #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct BlockJSON { @@ -212,19 +251,21 @@ pub struct BlockJSON { impl TryFrom<&BlockJSON> for ReamBlock { type Error = anyhow::Error; - fn try_from(block: &BlockJSON) -> anyhow::Result { Ok(ReamBlock { slot: block.slot, proposer_index: block.proposer_index, parent_root: block.parent_root, state_root: block.state_root, - body: ReamBlockBody::try_from(&block.body)?, + body: (&block.body).try_into()?, }) } } -/// State with camelCase fields +// ============================================================================ +// State +// ============================================================================ + #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct StateJSON { @@ -242,40 +283,13 @@ pub struct StateJSON { impl TryFrom<&StateJSON> for ReamState { type Error = anyhow::Error; - fn try_from(state: &StateJSON) -> anyhow::Result { - use ssz_types::typenum::{U262144, U1073741824}; - - // Convert validators - let validators: Vec = state + let validators: Vec<_> = state .validators .data .iter() - .map(ReamValidator::try_from) - .collect::, _>>()?; - - // Convert justified_slots to BitList - let justified_slots_len = state.justified_slots.data.len(); - let mut justified_slots = BitList::::with_capacity(justified_slots_len) - .map_err(|err| anyhow!("Failed to create justified_slots BitList: {err:?}"))?; - for (i, &bit) in state.justified_slots.data.iter().enumerate() { - justified_slots - .set(i, bit) - .map_err(|err| anyhow!("Failed to set justified_slots bit at {i}: {err:?}"))?; - } - - // Convert justifications_validators to BitList - let justifications_len = state.justifications_validators.data.len(); - let mut justifications_validators = - BitList::::with_capacity(justifications_len).map_err(|err| { - anyhow!("Failed to create justifications_validators BitList: {err:?}") - })?; - for (i, &bit) in state.justifications_validators.data.iter().enumerate() { - justifications_validators.set(i, bit).map_err(|err| { - anyhow!("Failed to set justifications_validators bit at {i}: {err:?}") - })?; - } - + .map(TryInto::try_into) + .collect::>()?; Ok(ReamState { config: (&state.config).try_into()?, slot: state.slot, @@ -285,15 +299,14 @@ impl TryFrom<&StateJSON> for ReamState { historical_block_hashes: VariableList::try_from( state.historical_block_hashes.data.clone(), ) - .map_err(|err| anyhow!("Failed to create historical_block_hashes: {err}"))?, - justified_slots, - validators: VariableList::try_from(validators) - .map_err(|err| anyhow!("Failed to create validators: {err}"))?, + .map_err(|error| anyhow!("{error}"))?, + justified_slots: bools_to_bitlist::(&state.justified_slots.data)?, + validators: VariableList::try_from(validators).map_err(|error| anyhow!("{error}"))?, justifications_roots: VariableList::try_from(state.justifications_roots.data.clone()) - .map_err(|err| { - anyhow!("Failed to create justifications_roots: {err}") - })?, - justifications_validators, + .map_err(|error| anyhow!("{error}"))?, + justifications_validators: bools_to_bitlist::( + &state.justifications_validators.data, + )?, }) } } @@ -302,41 +315,6 @@ impl TryFrom<&StateJSON> for ReamState { // Signature-related types // ============================================================================ -/// SignedAttestation with camelCase fields -#[derive(Debug, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct SignedAttestationJSON { - pub validator_id: u64, - pub message: ReamAttestationData, - pub signature: String, -} - -impl TryFrom<&SignedAttestationJSON> for ReamSignedAttestation { - type Error = anyhow::Error; - - fn try_from(att: &SignedAttestationJSON) -> anyhow::Result { - let sig_hex = att.signature.trim_start_matches("0x"); - let sig_bytes = alloy_primitives::hex::decode(sig_hex) - .map_err(|err| anyhow!("Failed to decode signature hex: {err}"))?; - - let signature = Signature::from_ssz_bytes(&sig_bytes) - .map_err(|err| anyhow!("Failed to decode signature from SSZ: {err:?}"))?; - - Ok(ReamSignedAttestation { - validator_id: att.validator_id, - message: att.message.clone(), - signature, - }) - } -} - -/// Wrapper for proof_data in JSON format (nested { data: "0x..." }) -#[derive(Debug, Deserialize, Clone)] -pub struct ProofDataJSON { - pub data: String, -} - -/// AggregatedSignatureProof with camelCase fields #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AggregatedSignatureProofJSON { @@ -346,31 +324,15 @@ pub struct AggregatedSignatureProofJSON { impl TryFrom<&AggregatedSignatureProofJSON> for ReamAggregatedSignatureProof { type Error = anyhow::Error; - fn try_from(proof: &AggregatedSignatureProofJSON) -> anyhow::Result { - let bool_data = &proof.participants.data; - let mut participants = BitList::::with_capacity(bool_data.len()) - .map_err(|err| anyhow!("Failed to create BitList: {err:?}"))?; - - for (i, &bit) in bool_data.iter().enumerate() { - participants - .set(i, bit) - .map_err(|err| anyhow!("Failed to set bit at index {i}: {err:?}"))?; - } - - let proof_hex = proof.proof_data.data.trim_start_matches("0x"); - let proof_bytes = alloy_primitives::hex::decode(proof_hex) - .map_err(|err| anyhow!("Failed to decode proof_data hex: {err}"))?; - Ok(ReamAggregatedSignatureProof { - participants, - proof_data: VariableList::::try_from(proof_bytes) - .map_err(|err| anyhow!("Failed to create proof_data: {err}"))?, + participants: bools_to_bitlist(&proof.participants.data)?, + proof_data: VariableList::::try_from(decode_hex(&proof.proof_data.data)?) + .map_err(|error| anyhow!("{error}"))?, }) } } -/// BlockSignatures with camelCase fields #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct BlockSignaturesJSON { @@ -380,29 +342,21 @@ pub struct BlockSignaturesJSON { impl TryFrom<&BlockSignaturesJSON> for ReamBlockSignatures { type Error = anyhow::Error; - - fn try_from(sigs: &BlockSignaturesJSON) -> anyhow::Result { - let mut attestation_signatures = Vec::new(); - for proof in &sigs.attestation_signatures.data { - attestation_signatures.push(ReamAggregatedSignatureProof::try_from(proof)?); - } - - let sig_hex = sigs.proposer_signature.trim_start_matches("0x"); - let sig_bytes = alloy_primitives::hex::decode(sig_hex) - .map_err(|err| anyhow!("Failed to decode proposer_signature hex: {err}"))?; - - let proposer_signature = Signature::from_ssz_bytes(&sig_bytes) - .map_err(|err| anyhow!("Failed to decode proposer_signature from SSZ: {err:?}"))?; - + fn try_from(signatures: &BlockSignaturesJSON) -> anyhow::Result { + let attestation_signatures: Vec<_> = signatures + .attestation_signatures + .data + .iter() + .map(TryInto::try_into) + .collect::>()?; Ok(ReamBlockSignatures { attestation_signatures: VariableList::try_from(attestation_signatures) - .map_err(|err| anyhow!("Failed to create attestation_signatures: {err}"))?, - proposer_signature, + .map_err(|error| anyhow!("{error}"))?, + proposer_signature: decode_signature(&signatures.proposer_signature)?, }) } } -/// BlockWithAttestation with camelCase fields #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct BlockWithAttestationJSON { @@ -412,16 +366,14 @@ pub struct BlockWithAttestationJSON { impl TryFrom<&BlockWithAttestationJSON> for ReamBlockWithAttestation { type Error = anyhow::Error; - - fn try_from(bwa: &BlockWithAttestationJSON) -> anyhow::Result { + fn try_from(block_with_attestation: &BlockWithAttestationJSON) -> anyhow::Result { Ok(ReamBlockWithAttestation { - block: (&bwa.block).try_into()?, - proposer_attestation: (&bwa.proposer_attestation).try_into()?, + block: (&block_with_attestation.block).try_into()?, + proposer_attestation: (&block_with_attestation.proposer_attestation).try_into()?, }) } } -/// SignedBlockWithAttestation with camelCase fields #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct SignedBlockWithAttestationJSON { @@ -431,11 +383,10 @@ pub struct SignedBlockWithAttestationJSON { impl TryFrom<&SignedBlockWithAttestationJSON> for ReamSignedBlockWithAttestation { type Error = anyhow::Error; - - fn try_from(sbwa: &SignedBlockWithAttestationJSON) -> anyhow::Result { + fn try_from(signed_block: &SignedBlockWithAttestationJSON) -> anyhow::Result { Ok(ReamSignedBlockWithAttestation { - message: (&sbwa.message).try_into()?, - signature: (&sbwa.signature).try_into()?, + message: (&signed_block.message).try_into()?, + signature: (&signed_block.signature).try_into()?, }) } } diff --git a/testing/lean-spec-tests/tests/tests.rs b/testing/lean-spec-tests/tests/tests.rs index 66207a714..843ce98ec 100644 --- a/testing/lean-spec-tests/tests/tests.rs +++ b/testing/lean-spec-tests/tests/tests.rs @@ -188,7 +188,6 @@ fn test_all_ssz_fixtures() { let mut total_tests = 0; let mut passed = 0; let mut failed = 0; - let mut skipped = 0; for fixture_path in fixtures { debug!("\n=== Loading fixture: {:?} ===", fixture_path.file_name()); @@ -200,7 +199,6 @@ fn test_all_ssz_fixtures() { info!("Starting test: {}", test_name); match run_ssz_test(test_name, test) { Ok(_) => { - // Check if the test was skipped (logs contain "Skipping") passed += 1; info!("PASSED: {}", test_name); } @@ -221,7 +219,6 @@ fn test_all_ssz_fixtures() { info!("\n=== SSZ Test Summary ==="); info!("Total tests: {total_tests}"); info!("Passed: {passed}"); - info!("Skipped: {skipped}"); info!("Failed: {failed}"); assert_eq!(failed, 0, "Some SSZ tests failed"); From afc6d4f323ff60ddb95e40c7430bd5bffa20ae6e Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 16:48:57 +0700 Subject: [PATCH 09/20] fix comments --- testing/lean-spec-tests/src/types/ssz_test.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 9f3dc34db..657f4a0c4 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -1,6 +1,10 @@ //! Intermediate JSON types for leanSpec SSZ test fixtures. //! //! These types handle camelCase JSON and convert to ream-consensus-lean types. +//! +//! These intermediate conversions are needed because the test vectors define the +//! expected deserialized keys & values as JSON and in camelCase while Rust and our +//! codebase uses snake_case. use alloy_primitives::B256; use anyhow::anyhow; @@ -69,7 +73,7 @@ pub struct SSZTest { } // ============================================================================ -// JSON wrapper types +// Common JSON wrapper types // ============================================================================ #[derive(Debug, Deserialize, Clone)] @@ -82,11 +86,6 @@ pub struct AggregationBitsJSON { pub data: Vec, } -#[derive(Debug, Deserialize, Clone)] -pub struct ProofDataJSON { - pub data: String, -} - // ============================================================================ // Config // ============================================================================ @@ -315,6 +314,11 @@ impl TryFrom<&StateJSON> for ReamState { // Signature-related types // ============================================================================ +#[derive(Debug, Deserialize, Clone)] +pub struct ProofDataJSON { + pub data: String, +} + #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AggregatedSignatureProofJSON { From bba40bb501b5681a80ffe93745b6de511a559ac6 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 17:36:16 +0700 Subject: [PATCH 10/20] remove TreeHash checks from SSZ tests --- .../common/consensus/lean/src/attestation.rs | 2 +- .../post_quantum/src/leansig/signature.rs | 23 ---- testing/lean-spec-tests/src/ssz_test.rs | 102 +++++------------- testing/lean-spec-tests/src/types/ssz_test.rs | 1 - 4 files changed, 30 insertions(+), 98 deletions(-) diff --git a/crates/common/consensus/lean/src/attestation.rs b/crates/common/consensus/lean/src/attestation.rs index abb96d60d..1bab8e7f4 100644 --- a/crates/common/consensus/lean/src/attestation.rs +++ b/crates/common/consensus/lean/src/attestation.rs @@ -102,7 +102,7 @@ impl AggregatedAttestation { } /// Validator attestation bundled with its signature. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode)] pub struct SignedAttestation { pub validator_id: u64, pub message: AttestationData, diff --git a/crates/crypto/post_quantum/src/leansig/signature.rs b/crates/crypto/post_quantum/src/leansig/signature.rs index 407812dec..d319f9474 100644 --- a/crates/crypto/post_quantum/src/leansig/signature.rs +++ b/crates/crypto/post_quantum/src/leansig/signature.rs @@ -1,7 +1,6 @@ use leansig::{MESSAGE_LENGTH, signature::SignatureScheme}; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode}; -use tree_hash::TreeHash; use crate::leansig::{LeanSigScheme, public_key::PublicKey}; @@ -58,28 +57,6 @@ impl Decode for Signature { } } -impl TreeHash for Signature { - fn tree_hash_type() -> tree_hash::TreeHashType { - // Signatures are variable-length containers - tree_hash::TreeHashType::Container - } - - fn tree_hash_packed_encoding(&self) -> tree_hash::PackedEncoding { - unreachable!("Signature is not a basic type") - } - - fn tree_hash_packing_factor() -> usize { - unreachable!("Signature is not a basic type") - } - - fn tree_hash_root(&self) -> tree_hash::Hash256 { - // Hash the SSZ encoding as the tree hash root - // This matches how variable-length containers are hashed - let bytes = self.inner.as_ssz_bytes(); - tree_hash::merkle_root(&bytes, 0) - } -} - impl Signature { pub fn new(inner: LeanSigSignature) -> Self { Self { inner } diff --git a/testing/lean-spec-tests/src/ssz_test.rs b/testing/lean-spec-tests/src/ssz_test.rs index 45d336644..99cc01744 100644 --- a/testing/lean-spec-tests/src/ssz_test.rs +++ b/testing/lean-spec-tests/src/ssz_test.rs @@ -1,6 +1,6 @@ use std::path::Path; -use alloy_primitives::{B256, hex}; +use alloy_primitives::hex; use anyhow::{anyhow, bail}; use ream_consensus_lean::{ attestation::{ @@ -17,7 +17,6 @@ use ream_consensus_lean::{ }; use ssz::Encode; use tracing::{debug, info, warn}; -use tree_hash::TreeHash; use crate::types::{ TestFixture, @@ -54,57 +53,40 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { debug!(" Type: {}", test.type_name); let expected_ssz = parse_hex_bytes(&test.serialized)?; - let expected_root = test.root; match test.type_name.as_str() { // Types that deserialize directly (snake_case in JSON) - "Checkpoint" => run_test_direct::(&test.value, &expected_ssz, expected_root), - "AttestationData" => { - run_test_direct::(&test.value, &expected_ssz, expected_root) - } + "Checkpoint" => run_test_direct::(&test.value, &expected_ssz), + "AttestationData" => run_test_direct::(&test.value, &expected_ssz), // Types with JSON intermediate conversion - "AggregatedAttestation" => run_test::( - &test.value, - &expected_ssz, - expected_root, - ), - "Attestation" => run_test::( - &test.value, - &expected_ssz, - expected_root, - ), - "BlockBody" => { - run_test::(&test.value, &expected_ssz, expected_root) + "AggregatedAttestation" => { + run_test::(&test.value, &expected_ssz) } - "BlockHeader" => { - run_test::(&test.value, &expected_ssz, expected_root) + "Attestation" => { + run_test::(&test.value, &expected_ssz) } - "Block" => run_test::(&test.value, &expected_ssz, expected_root), - "Config" => run_test::(&test.value, &expected_ssz, expected_root), - "Validator" => { - run_test::(&test.value, &expected_ssz, expected_root) + "BlockBody" => run_test::(&test.value, &expected_ssz), + "BlockHeader" => run_test::(&test.value, &expected_ssz), + "Block" => run_test::(&test.value, &expected_ssz), + "Config" => run_test::(&test.value, &expected_ssz), + "Validator" => run_test::(&test.value, &expected_ssz), + "State" => run_test::(&test.value, &expected_ssz), + "SignedAttestation" => { + run_test::(&test.value, &expected_ssz) } - "State" => run_test::(&test.value, &expected_ssz, expected_root), - - // Types without TreeHash - SSZ only - "SignedAttestation" => run_test_ssz_only::( - &test.value, - &expected_ssz, - ), "BlockSignatures" => { - run_test_ssz_only::(&test.value, &expected_ssz) + run_test::(&test.value, &expected_ssz) } "BlockWithAttestation" => { - run_test_ssz_only::( + run_test::(&test.value, &expected_ssz) + } + "SignedBlockWithAttestation" => { + run_test::( &test.value, &expected_ssz, ) } - "SignedBlockWithAttestation" => run_test_ssz_only::< - SignedBlockWithAttestationJSON, - SignedBlockWithAttestation, - >(&test.value, &expected_ssz), _ => { warn!("Unknown type: {}, skipping", test.type_name); @@ -113,44 +95,26 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { } } -/// Run test with JSON intermediate type conversion -fn run_test( - value: &serde_json::Value, - expected_ssz: &[u8], - expected_root: B256, -) -> anyhow::Result<()> +/// Run SSZ test with JSON intermediate type conversion. +/// J is the JSON intermediate type, T is the target type. +fn run_test(value: &serde_json::Value, expected_ssz: &[u8]) -> anyhow::Result<()> where J: serde::de::DeserializeOwned, - T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode + TreeHash, + T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode, { let json_value: J = serde_json::from_value(value.clone()) .map_err(|err| anyhow!("Failed to deserialize JSON: {err}"))?; let typed_value: T = (&json_value).try_into()?; - verify_ssz(&typed_value, expected_ssz)?; - verify_root(&typed_value, expected_root) -} - -/// Run test for types that deserialize directly -fn run_test_direct( - value: &serde_json::Value, - expected_ssz: &[u8], - expected_root: B256, -) -> anyhow::Result<()> { - let typed_value: T = serde_json::from_value(value.clone()) - .map_err(|err| anyhow!("Failed to deserialize JSON: {err}"))?; - verify_ssz(&typed_value, expected_ssz)?; - verify_root(&typed_value, expected_root) + verify_ssz(&typed_value, expected_ssz) } -/// Run test for types without TreeHash (SSZ only) -fn run_test_ssz_only(value: &serde_json::Value, expected_ssz: &[u8]) -> anyhow::Result<()> +/// Run SSZ test for types that deserialize directly (no JSON intermediate type needed). +fn run_test_direct(value: &serde_json::Value, expected_ssz: &[u8]) -> anyhow::Result<()> where - J: serde::de::DeserializeOwned, - T: for<'a> TryFrom<&'a J, Error = anyhow::Error> + Encode, + T: serde::de::DeserializeOwned + Encode, { - let json_value: J = serde_json::from_value(value.clone()) + let typed_value: T = serde_json::from_value(value.clone()) .map_err(|err| anyhow!("Failed to deserialize JSON: {err}"))?; - let typed_value: T = (&json_value).try_into()?; verify_ssz(&typed_value, expected_ssz) } @@ -166,14 +130,6 @@ fn verify_ssz(value: &T, expected: &[u8]) -> anyhow::Result<()> { Ok(()) } -fn verify_root(value: &T, expected: B256) -> anyhow::Result<()> { - let actual = value.tree_hash_root(); - if actual != expected { - bail!("TreeHash mismatch:\n expected: {expected}\n got: {actual}"); - } - Ok(()) -} - fn parse_hex_bytes(hex_str: &str) -> anyhow::Result> { let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); hex::decode(hex_str).map_err(|err| anyhow!("Failed to parse hex: {err}")) diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 657f4a0c4..7c94124af 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -69,7 +69,6 @@ pub struct SSZTest { pub type_name: String, pub value: serde_json::Value, pub serialized: String, - pub root: B256, } // ============================================================================ From e37e5ff5a600295b25b2fc2f5df95db9022ec9c3 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 17:51:25 +0700 Subject: [PATCH 11/20] simplify ssz test setup --- testing/lean-spec-tests/src/ssz_test.rs | 46 ++++--------------- testing/lean-spec-tests/src/types/ssz_test.rs | 26 +++++++++++ 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/testing/lean-spec-tests/src/ssz_test.rs b/testing/lean-spec-tests/src/ssz_test.rs index 99cc01744..4f0c23db5 100644 --- a/testing/lean-spec-tests/src/ssz_test.rs +++ b/testing/lean-spec-tests/src/ssz_test.rs @@ -21,9 +21,10 @@ use tracing::{debug, info, warn}; use crate::types::{ TestFixture, ssz_test::{ - AggregatedAttestationJSON, AttestationJSON, BlockBodyJSON, BlockHeaderJSON, BlockJSON, - BlockSignaturesJSON, BlockWithAttestationJSON, ConfigJSON, SSZTest, SignedAttestationJSON, - SignedBlockWithAttestationJSON, StateJSON, ValidatorJSON, + AggregatedAttestationJSON, AttestationDataJSON, AttestationJSON, BlockBodyJSON, + BlockHeaderJSON, BlockJSON, BlockSignaturesJSON, BlockWithAttestationJSON, CheckpointJSON, + ConfigJSON, SSZTest, SignedAttestationJSON, SignedBlockWithAttestationJSON, StateJSON, + ValidatorJSON, }, }; @@ -55,11 +56,10 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { let expected_ssz = parse_hex_bytes(&test.serialized)?; match test.type_name.as_str() { - // Types that deserialize directly (snake_case in JSON) - "Checkpoint" => run_test_direct::(&test.value, &expected_ssz), - "AttestationData" => run_test_direct::(&test.value, &expected_ssz), - - // Types with JSON intermediate conversion + "Checkpoint" => run_test::(&test.value, &expected_ssz), + "AttestationData" => { + run_test::(&test.value, &expected_ssz) + } "AggregatedAttestation" => { run_test::(&test.value, &expected_ssz) } @@ -95,8 +95,7 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { } } -/// Run SSZ test with JSON intermediate type conversion. -/// J is the JSON intermediate type, T is the target type. +/// Run SSZ test. J is the JSON type, T is the target type. fn run_test(value: &serde_json::Value, expected_ssz: &[u8]) -> anyhow::Result<()> where J: serde::de::DeserializeOwned, @@ -108,16 +107,6 @@ where verify_ssz(&typed_value, expected_ssz) } -/// Run SSZ test for types that deserialize directly (no JSON intermediate type needed). -fn run_test_direct(value: &serde_json::Value, expected_ssz: &[u8]) -> anyhow::Result<()> -where - T: serde::de::DeserializeOwned + Encode, -{ - let typed_value: T = serde_json::from_value(value.clone()) - .map_err(|err| anyhow!("Failed to deserialize JSON: {err}"))?; - verify_ssz(&typed_value, expected_ssz) -} - fn verify_ssz(value: &T, expected: &[u8]) -> anyhow::Result<()> { let actual = value.as_ssz_bytes(); if actual != expected { @@ -134,20 +123,3 @@ fn parse_hex_bytes(hex_str: &str) -> anyhow::Result> { let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str); hex::decode(hex_str).map_err(|err| anyhow!("Failed to parse hex: {err}")) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_hex_bytes() { - assert_eq!( - parse_hex_bytes("0xdeadbeef").unwrap(), - vec![0xde, 0xad, 0xbe, 0xef] - ); - assert_eq!( - parse_hex_bytes("deadbeef").unwrap(), - vec![0xde, 0xad, 0xbe, 0xef] - ); - } -} diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 7c94124af..09227ec73 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -104,6 +104,21 @@ impl TryFrom<&ConfigJSON> for ReamConfig { } } +// ============================================================================ +// Checkpoint +// ============================================================================ + +#[derive(Debug, Deserialize, Clone)] +#[serde(transparent)] +pub struct CheckpointJSON(pub ReamCheckpoint); + +impl TryFrom<&CheckpointJSON> for ReamCheckpoint { + type Error = anyhow::Error; + fn try_from(value: &CheckpointJSON) -> anyhow::Result { + Ok(value.0.clone()) + } +} + // ============================================================================ // BlockHeader // ============================================================================ @@ -193,6 +208,17 @@ impl TryFrom<&AttestationJSON> for ReamAggregatedAttestations { } } +#[derive(Debug, Deserialize, Clone)] +#[serde(transparent)] +pub struct AttestationDataJSON(pub ReamAttestationData); + +impl TryFrom<&AttestationDataJSON> for ReamAttestationData { + type Error = anyhow::Error; + fn try_from(value: &AttestationDataJSON) -> anyhow::Result { + Ok(value.0.clone()) + } +} + #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct SignedAttestationJSON { From 5b355364bd3bc34aec15c43bb63bfdaed324ae21 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 18:56:46 +0700 Subject: [PATCH 12/20] deduplicate conversions with macros --- testing/lean-spec-tests/src/types/ssz_test.rs | 119 ++++++++---------- 1 file changed, 55 insertions(+), 64 deletions(-) diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 09227ec73..e58737782 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -71,6 +71,54 @@ pub struct SSZTest { pub serialized: String, } +// ============================================================================ +// Macros +// ============================================================================ + +/// Creates a passthrough JSON wrapper that deserializes directly to the inner type. +macro_rules! passthrough_wrapper { + ($name:ident, $inner:ty) => { + #[derive(Debug, Deserialize, Clone)] + #[serde(transparent)] + pub struct $name(pub $inner); + + impl TryFrom<&$name> for $inner { + type Error = anyhow::Error; + fn try_from(value: &$name) -> anyhow::Result { + Ok(value.0.clone()) + } + } + }; +} + +/// Creates a TryFrom impl where all fields are copied directly (for Copy types). +macro_rules! simple_conversion { + ($json:ident => $target:ty { $($field:ident),+ }) => { + impl TryFrom<&$json> for $target { + type Error = anyhow::Error; + fn try_from(value: &$json) -> anyhow::Result { + Ok(Self { + $($field: value.$field),+ + }) + } + } + }; +} + +/// Creates a TryFrom impl where all fields are converted via try_into(). +macro_rules! nested_conversion { + ($json:ident => $target:ty { $($field:ident),+ }) => { + impl TryFrom<&$json> for $target { + type Error = anyhow::Error; + fn try_from(value: &$json) -> anyhow::Result { + Ok(Self { + $($field: (&value.$field).try_into()?),+ + }) + } + } + }; +} + // ============================================================================ // Common JSON wrapper types // ============================================================================ @@ -85,6 +133,9 @@ pub struct AggregationBitsJSON { pub data: Vec, } +passthrough_wrapper!(CheckpointJSON, ReamCheckpoint); +passthrough_wrapper!(AttestationDataJSON, ReamAttestationData); + // ============================================================================ // Config // ============================================================================ @@ -95,29 +146,7 @@ pub struct ConfigJSON { pub genesis_time: u64, } -impl TryFrom<&ConfigJSON> for ReamConfig { - type Error = anyhow::Error; - fn try_from(config: &ConfigJSON) -> anyhow::Result { - Ok(ReamConfig { - genesis_time: config.genesis_time, - }) - } -} - -// ============================================================================ -// Checkpoint -// ============================================================================ - -#[derive(Debug, Deserialize, Clone)] -#[serde(transparent)] -pub struct CheckpointJSON(pub ReamCheckpoint); - -impl TryFrom<&CheckpointJSON> for ReamCheckpoint { - type Error = anyhow::Error; - fn try_from(value: &CheckpointJSON) -> anyhow::Result { - Ok(value.0.clone()) - } -} +simple_conversion!(ConfigJSON => ReamConfig { genesis_time }); // ============================================================================ // BlockHeader @@ -133,18 +162,7 @@ pub struct BlockHeaderJSON { pub body_root: B256, } -impl TryFrom<&BlockHeaderJSON> for ReamBlockHeader { - type Error = anyhow::Error; - fn try_from(header: &BlockHeaderJSON) -> anyhow::Result { - Ok(ReamBlockHeader { - slot: header.slot, - proposer_index: header.proposer_index, - parent_root: header.parent_root, - state_root: header.state_root, - body_root: header.body_root, - }) - } -} +simple_conversion!(BlockHeaderJSON => ReamBlockHeader { slot, proposer_index, parent_root, state_root, body_root }); // ============================================================================ // Validator @@ -208,17 +226,6 @@ impl TryFrom<&AttestationJSON> for ReamAggregatedAttestations { } } -#[derive(Debug, Deserialize, Clone)] -#[serde(transparent)] -pub struct AttestationDataJSON(pub ReamAttestationData); - -impl TryFrom<&AttestationDataJSON> for ReamAttestationData { - type Error = anyhow::Error; - fn try_from(value: &AttestationDataJSON) -> anyhow::Result { - Ok(value.0.clone()) - } -} - #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct SignedAttestationJSON { @@ -393,15 +400,7 @@ pub struct BlockWithAttestationJSON { pub proposer_attestation: AttestationJSON, } -impl TryFrom<&BlockWithAttestationJSON> for ReamBlockWithAttestation { - type Error = anyhow::Error; - fn try_from(block_with_attestation: &BlockWithAttestationJSON) -> anyhow::Result { - Ok(ReamBlockWithAttestation { - block: (&block_with_attestation.block).try_into()?, - proposer_attestation: (&block_with_attestation.proposer_attestation).try_into()?, - }) - } -} +nested_conversion!(BlockWithAttestationJSON => ReamBlockWithAttestation { block, proposer_attestation }); #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -410,12 +409,4 @@ pub struct SignedBlockWithAttestationJSON { pub signature: BlockSignaturesJSON, } -impl TryFrom<&SignedBlockWithAttestationJSON> for ReamSignedBlockWithAttestation { - type Error = anyhow::Error; - fn try_from(signed_block: &SignedBlockWithAttestationJSON) -> anyhow::Result { - Ok(ReamSignedBlockWithAttestation { - message: (&signed_block.message).try_into()?, - signature: (&signed_block.signature).try_into()?, - }) - } -} +nested_conversion!(SignedBlockWithAttestationJSON => ReamSignedBlockWithAttestation { message, signature }); From 539f02de8589913c023d7506bff8aefb5f64d6dd Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 20:41:26 +0700 Subject: [PATCH 13/20] fix linting --- testing/lean-spec-tests/src/ssz_test.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/testing/lean-spec-tests/src/ssz_test.rs b/testing/lean-spec-tests/src/ssz_test.rs index 4f0c23db5..43c7e5aed 100644 --- a/testing/lean-spec-tests/src/ssz_test.rs +++ b/testing/lean-spec-tests/src/ssz_test.rs @@ -81,12 +81,10 @@ pub fn run_ssz_test(test_name: &str, test: &SSZTest) -> anyhow::Result<()> { "BlockWithAttestation" => { run_test::(&test.value, &expected_ssz) } - "SignedBlockWithAttestation" => { - run_test::( - &test.value, - &expected_ssz, - ) - } + "SignedBlockWithAttestation" => run_test::< + SignedBlockWithAttestationJSON, + SignedBlockWithAttestation, + >(&test.value, &expected_ssz), _ => { warn!("Unknown type: {}, skipping", test.type_name); From 1d287077dc648fc3520518344daa3cb628f29cca Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 21:37:28 +0700 Subject: [PATCH 14/20] better passthrough_conversion naming --- testing/lean-spec-tests/src/types/ssz_test.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index e58737782..fb1ec4d96 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -76,7 +76,7 @@ pub struct SSZTest { // ============================================================================ /// Creates a passthrough JSON wrapper that deserializes directly to the inner type. -macro_rules! passthrough_wrapper { +macro_rules! passthrough_conversion { ($name:ident, $inner:ty) => { #[derive(Debug, Deserialize, Clone)] #[serde(transparent)] @@ -91,7 +91,7 @@ macro_rules! passthrough_wrapper { }; } -/// Creates a TryFrom impl where all fields are copied directly (for Copy types). +/// Creates a TryFrom impl where all fields are copied directly. macro_rules! simple_conversion { ($json:ident => $target:ty { $($field:ident),+ }) => { impl TryFrom<&$json> for $target { @@ -133,8 +133,8 @@ pub struct AggregationBitsJSON { pub data: Vec, } -passthrough_wrapper!(CheckpointJSON, ReamCheckpoint); -passthrough_wrapper!(AttestationDataJSON, ReamAttestationData); +passthrough_conversion!(CheckpointJSON, ReamCheckpoint); +passthrough_conversion!(AttestationDataJSON, ReamAttestationData); // ============================================================================ // Config From 3e554fec432d3faa53cdb20ab3eb65a07b1ee9b5 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 21:44:34 +0700 Subject: [PATCH 15/20] refactor to custom_conversion macro --- testing/lean-spec-tests/src/types/ssz_test.rs | 194 +++++++----------- 1 file changed, 77 insertions(+), 117 deletions(-) diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index fb1ec4d96..13b00552e 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -119,6 +119,21 @@ macro_rules! nested_conversion { }; } +/// Creates a TryFrom impl with custom conversion expression per field. +macro_rules! custom_conversion { + ($json:ident as $val:ident => $target:ty { $($field:ident: $conv:expr),+ $(,)? }) => { + impl TryFrom<&$json> for $target { + type Error = anyhow::Error; + #[allow(clippy::redundant_closure_call)] + fn try_from($val: &$json) -> anyhow::Result { + Ok(Self { + $($field: $conv),+ + }) + } + } + }; +} + // ============================================================================ // Common JSON wrapper types // ============================================================================ @@ -174,19 +189,16 @@ pub struct ValidatorJSON { pub index: u64, } -impl TryFrom<&ValidatorJSON> for ReamValidator { - type Error = anyhow::Error; - fn try_from(validator: &ValidatorJSON) -> anyhow::Result { - let bytes = decode_hex(&validator.pubkey)?; +custom_conversion!(ValidatorJSON as value => ReamValidator { + public_key: { + let bytes = decode_hex(&value.pubkey)?; if bytes.len() != 52 { return Err(anyhow!("Expected 52-byte pubkey, got {}", bytes.len())); } - Ok(ReamValidator { - public_key: PublicKey::from(&bytes[..]), - index: validator.index, - }) - } -} + PublicKey::from(&bytes[..]) + }, + index: value.index, +}); // ============================================================================ // Attestations @@ -199,15 +211,10 @@ pub struct AggregatedAttestationJSON { pub data: ReamAttestationData, } -impl TryFrom<&AggregatedAttestationJSON> for ReamAggregatedAttestation { - type Error = anyhow::Error; - fn try_from(attestation: &AggregatedAttestationJSON) -> anyhow::Result { - Ok(ReamAggregatedAttestation { - aggregation_bits: bools_to_bitlist(&attestation.aggregation_bits.data)?, - message: attestation.data.clone(), - }) - } -} +custom_conversion!(AggregatedAttestationJSON as value => ReamAggregatedAttestation { + aggregation_bits: bools_to_bitlist(&value.aggregation_bits.data)?, + message: value.data.clone(), +}); #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -216,15 +223,10 @@ pub struct AttestationJSON { pub data: ReamAttestationData, } -impl TryFrom<&AttestationJSON> for ReamAggregatedAttestations { - type Error = anyhow::Error; - fn try_from(attestation: &AttestationJSON) -> anyhow::Result { - Ok(ReamAggregatedAttestations { - validator_id: attestation.validator_id, - data: attestation.data.clone(), - }) - } -} +custom_conversion!(AttestationJSON as value => ReamAggregatedAttestations { + validator_id: value.validator_id, + data: value.data.clone(), +}); #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -234,16 +236,11 @@ pub struct SignedAttestationJSON { pub signature: String, } -impl TryFrom<&SignedAttestationJSON> for ReamSignedAttestation { - type Error = anyhow::Error; - fn try_from(attestation: &SignedAttestationJSON) -> anyhow::Result { - Ok(ReamSignedAttestation { - validator_id: attestation.validator_id, - message: attestation.message.clone(), - signature: decode_signature(&attestation.signature)?, - }) - } -} +custom_conversion!(SignedAttestationJSON as value => ReamSignedAttestation { + validator_id: value.validator_id, + message: value.message.clone(), + signature: decode_signature(&value.signature)?, +}); // ============================================================================ // Block @@ -254,21 +251,13 @@ pub struct BlockBodyJSON { pub attestations: DataListJSON, } -impl TryFrom<&BlockBodyJSON> for ReamBlockBody { - type Error = anyhow::Error; - fn try_from(body: &BlockBodyJSON) -> anyhow::Result { - let attestations: Vec<_> = body - .attestations - .data - .iter() +custom_conversion!(BlockBodyJSON as value => ReamBlockBody { + attestations: VariableList::try_from( + value.attestations.data.iter() .map(TryInto::try_into) - .collect::>()?; - Ok(ReamBlockBody { - attestations: VariableList::try_from(attestations) - .map_err(|error| anyhow!("{error}"))?, - }) - } -} + .collect::, _>>()? + ).map_err(|error| anyhow!("{error}"))?, +}); #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -280,18 +269,13 @@ pub struct BlockJSON { pub body: BlockBodyJSON, } -impl TryFrom<&BlockJSON> for ReamBlock { - type Error = anyhow::Error; - fn try_from(block: &BlockJSON) -> anyhow::Result { - Ok(ReamBlock { - slot: block.slot, - proposer_index: block.proposer_index, - parent_root: block.parent_root, - state_root: block.state_root, - body: (&block.body).try_into()?, - }) - } -} +custom_conversion!(BlockJSON as value => ReamBlock { + slot: value.slot, + proposer_index: value.proposer_index, + parent_root: value.parent_root, + state_root: value.state_root, + body: (&value.body).try_into()?, +}); // ============================================================================ // State @@ -312,35 +296,24 @@ pub struct StateJSON { pub justifications_validators: DataListJSON, } -impl TryFrom<&StateJSON> for ReamState { - type Error = anyhow::Error; - fn try_from(state: &StateJSON) -> anyhow::Result { - let validators: Vec<_> = state - .validators - .data - .iter() +custom_conversion!(StateJSON as value => ReamState { + config: (&value.config).try_into()?, + slot: value.slot, + latest_block_header: (&value.latest_block_header).try_into()?, + latest_justified: value.latest_justified, + latest_finalized: value.latest_finalized, + historical_block_hashes: VariableList::try_from(value.historical_block_hashes.data.clone()) + .map_err(|error| anyhow!("{error}"))?, + justified_slots: bools_to_bitlist::(&value.justified_slots.data)?, + validators: VariableList::try_from( + value.validators.data.iter() .map(TryInto::try_into) - .collect::>()?; - Ok(ReamState { - config: (&state.config).try_into()?, - slot: state.slot, - latest_block_header: (&state.latest_block_header).try_into()?, - latest_justified: state.latest_justified, - latest_finalized: state.latest_finalized, - historical_block_hashes: VariableList::try_from( - state.historical_block_hashes.data.clone(), - ) - .map_err(|error| anyhow!("{error}"))?, - justified_slots: bools_to_bitlist::(&state.justified_slots.data)?, - validators: VariableList::try_from(validators).map_err(|error| anyhow!("{error}"))?, - justifications_roots: VariableList::try_from(state.justifications_roots.data.clone()) - .map_err(|error| anyhow!("{error}"))?, - justifications_validators: bools_to_bitlist::( - &state.justifications_validators.data, - )?, - }) - } -} + .collect::, _>>()? + ).map_err(|error| anyhow!("{error}"))?, + justifications_roots: VariableList::try_from(value.justifications_roots.data.clone()) + .map_err(|error| anyhow!("{error}"))?, + justifications_validators: bools_to_bitlist::(&value.justifications_validators.data)?, +}); // ============================================================================ // Signature-related types @@ -358,16 +331,11 @@ pub struct AggregatedSignatureProofJSON { pub proof_data: ProofDataJSON, } -impl TryFrom<&AggregatedSignatureProofJSON> for ReamAggregatedSignatureProof { - type Error = anyhow::Error; - fn try_from(proof: &AggregatedSignatureProofJSON) -> anyhow::Result { - Ok(ReamAggregatedSignatureProof { - participants: bools_to_bitlist(&proof.participants.data)?, - proof_data: VariableList::::try_from(decode_hex(&proof.proof_data.data)?) - .map_err(|error| anyhow!("{error}"))?, - }) - } -} +custom_conversion!(AggregatedSignatureProofJSON as value => ReamAggregatedSignatureProof { + participants: bools_to_bitlist(&value.participants.data)?, + proof_data: VariableList::::try_from(decode_hex(&value.proof_data.data)?) + .map_err(|error| anyhow!("{error}"))?, +}); #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -376,22 +344,14 @@ pub struct BlockSignaturesJSON { pub proposer_signature: String, } -impl TryFrom<&BlockSignaturesJSON> for ReamBlockSignatures { - type Error = anyhow::Error; - fn try_from(signatures: &BlockSignaturesJSON) -> anyhow::Result { - let attestation_signatures: Vec<_> = signatures - .attestation_signatures - .data - .iter() +custom_conversion!(BlockSignaturesJSON as value => ReamBlockSignatures { + attestation_signatures: VariableList::try_from( + value.attestation_signatures.data.iter() .map(TryInto::try_into) - .collect::>()?; - Ok(ReamBlockSignatures { - attestation_signatures: VariableList::try_from(attestation_signatures) - .map_err(|error| anyhow!("{error}"))?, - proposer_signature: decode_signature(&signatures.proposer_signature)?, - }) - } -} + .collect::, _>>()? + ).map_err(|error| anyhow!("{error}"))?, + proposer_signature: decode_signature(&value.proposer_signature)?, +}); #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] From 9e95a6d03cca400d30e96339bd90a9cf57303fbb Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 21:48:00 +0700 Subject: [PATCH 16/20] lint lint lint --- testing/lean-spec-tests/src/types/ssz_test.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 13b00552e..46d6cf7a0 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -39,20 +39,20 @@ use ssz_types::{ fn decode_hex(hex: &str) -> anyhow::Result> { alloy_primitives::hex::decode(hex.trim_start_matches("0x")) - .map_err(|error| anyhow!("hex decode failed: {error}")) + .map_err(|err| anyhow!("hex decode failed: {err}")) } fn decode_signature(hex: &str) -> anyhow::Result { Signature::from_ssz_bytes(&decode_hex(hex)?) - .map_err(|error| anyhow!("signature decode failed: {error:?}")) + .map_err(|err| anyhow!("signature decode failed: {err:?}")) } fn bools_to_bitlist(bools: &[bool]) -> anyhow::Result> { let mut bits = BitList::::with_capacity(bools.len()) - .map_err(|error| anyhow!("BitList creation failed: {error:?}"))?; + .map_err(|err| anyhow!("BitList creation failed: {err:?}"))?; for (index, &bit) in bools.iter().enumerate() { bits.set(index, bit) - .map_err(|error| anyhow!("BitList set failed: {error:?}"))?; + .map_err(|err| anyhow!("BitList set failed: {err:?}"))?; } Ok(bits) } @@ -256,7 +256,7 @@ custom_conversion!(BlockBodyJSON as value => ReamBlockBody { value.attestations.data.iter() .map(TryInto::try_into) .collect::, _>>()? - ).map_err(|error| anyhow!("{error}"))?, + ).map_err(|err| anyhow!("{err}"))?, }); #[derive(Debug, Deserialize, Clone)] @@ -303,15 +303,15 @@ custom_conversion!(StateJSON as value => ReamState { latest_justified: value.latest_justified, latest_finalized: value.latest_finalized, historical_block_hashes: VariableList::try_from(value.historical_block_hashes.data.clone()) - .map_err(|error| anyhow!("{error}"))?, + .map_err(|err| anyhow!("{err}"))?, justified_slots: bools_to_bitlist::(&value.justified_slots.data)?, validators: VariableList::try_from( value.validators.data.iter() .map(TryInto::try_into) .collect::, _>>()? - ).map_err(|error| anyhow!("{error}"))?, + ).map_err(|err| anyhow!("{err}"))?, justifications_roots: VariableList::try_from(value.justifications_roots.data.clone()) - .map_err(|error| anyhow!("{error}"))?, + .map_err(|err| anyhow!("{err}"))?, justifications_validators: bools_to_bitlist::(&value.justifications_validators.data)?, }); @@ -334,7 +334,7 @@ pub struct AggregatedSignatureProofJSON { custom_conversion!(AggregatedSignatureProofJSON as value => ReamAggregatedSignatureProof { participants: bools_to_bitlist(&value.participants.data)?, proof_data: VariableList::::try_from(decode_hex(&value.proof_data.data)?) - .map_err(|error| anyhow!("{error}"))?, + .map_err(|err| anyhow!("{err}"))?, }); #[derive(Debug, Deserialize, Clone)] @@ -349,7 +349,7 @@ custom_conversion!(BlockSignaturesJSON as value => ReamBlockSignatures { value.attestation_signatures.data.iter() .map(TryInto::try_into) .collect::, _>>()? - ).map_err(|error| anyhow!("{error}"))?, + ).map_err(|err| anyhow!("{err}"))?, proposer_signature: decode_signature(&value.proposer_signature)?, }); From 22ba3883ff42a7fa9affff74bd1d30a5b4dce347 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 23:11:27 +0700 Subject: [PATCH 17/20] fix slow Signature::blank() --- crates/crypto/post_quantum/src/leansig/signature.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/crypto/post_quantum/src/leansig/signature.rs b/crates/crypto/post_quantum/src/leansig/signature.rs index d319f9474..d56e4845c 100644 --- a/crates/crypto/post_quantum/src/leansig/signature.rs +++ b/crates/crypto/post_quantum/src/leansig/signature.rs @@ -1,9 +1,14 @@ +use std::sync::LazyLock; + use leansig::{MESSAGE_LENGTH, signature::SignatureScheme}; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode}; use crate::leansig::{LeanSigScheme, public_key::PublicKey}; +/// Cached blank signature to avoid expensive key generation on every call. +static BLANK_SIGNATURE: LazyLock = LazyLock::new(Signature::mock); + /// The inner leansig signature type with built-in SSZ support. pub type LeanSigSignature = ::Signature; @@ -64,13 +69,16 @@ impl Signature { /// Create a blank/placeholder signature. /// - /// Note: This generates a real mock signature under the hood which is expensive. + /// This returns a cached signature to avoid expensive key generation on every call. /// Only use in contexts where the signature won't be validated. pub fn blank() -> Self { - Self::mock() + BLANK_SIGNATURE.clone() } /// Create a mock signature for testing purposes. + /// + /// Note: This generates a real signature which is expensive. Prefer `blank()` when + /// you just need a placeholder signature. pub fn mock() -> Self { use rand::rng; From e89b2860f30d669f890a5b6b18fcd93c5aca58c4 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 11 Feb 2026 23:38:14 +0700 Subject: [PATCH 18/20] blank signatures --- .../post_quantum/src/leansig/signature.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/crypto/post_quantum/src/leansig/signature.rs b/crates/crypto/post_quantum/src/leansig/signature.rs index d56e4845c..eb67a3a55 100644 --- a/crates/crypto/post_quantum/src/leansig/signature.rs +++ b/crates/crypto/post_quantum/src/leansig/signature.rs @@ -1,14 +1,9 @@ -use std::sync::LazyLock; - use leansig::{MESSAGE_LENGTH, signature::SignatureScheme}; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode}; use crate::leansig::{LeanSigScheme, public_key::PublicKey}; -/// Cached blank signature to avoid expensive key generation on every call. -static BLANK_SIGNATURE: LazyLock = LazyLock::new(Signature::mock); - /// The inner leansig signature type with built-in SSZ support. pub type LeanSigSignature = ::Signature; @@ -69,10 +64,18 @@ impl Signature { /// Create a blank/placeholder signature. /// - /// This returns a cached signature to avoid expensive key generation on every call. + /// This decodes from minimal valid SSZ bytes, avoiding expensive key generation. /// Only use in contexts where the signature won't be validated. pub fn blank() -> Self { - BLANK_SIGNATURE.clone() + // 40 bytes: offset_path(4) + rho(28 zeros) + offset_hashes(4) + path(4) + const BYTES: [u8; 40] = [ + 36, 0, 0, 0, // offset_path = 36 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rho (28 zeros) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 40, 0, 0, 0, // offset_hashes = 40 + 4, 0, 0, 0, // path: empty HashTreeOpening + ]; + Self::from_ssz_bytes(&BYTES).expect("blank signature bytes are valid") } /// Create a mock signature for testing purposes. From fb8094643db8b346fb6d68a311e1026780a8e818 Mon Sep 17 00:00:00 2001 From: Kolby Moroz Liebl <31669092+KolbyML@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:13:10 -0700 Subject: [PATCH 19/20] Add regression tests --- .../post_quantum/src/leansig/signature.rs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/crypto/post_quantum/src/leansig/signature.rs b/crates/crypto/post_quantum/src/leansig/signature.rs index eb67a3a55..3fd7153b1 100644 --- a/crates/crypto/post_quantum/src/leansig/signature.rs +++ b/crates/crypto/post_quantum/src/leansig/signature.rs @@ -1,8 +1,8 @@ -use leansig::{MESSAGE_LENGTH, signature::SignatureScheme}; +use leansig::{signature::SignatureScheme, MESSAGE_LENGTH}; use serde::{Deserialize, Serialize}; use ssz::{Decode, DecodeError, Encode}; -use crate::leansig::{LeanSigScheme, public_key::PublicKey}; +use crate::leansig::{public_key::PublicKey, LeanSigScheme}; /// The inner leansig signature type with built-in SSZ support. pub type LeanSigSignature = ::Signature; @@ -120,11 +120,20 @@ impl Signature { #[cfg(test)] mod tests { + use alloy_primitives::FixedBytes; + use leansig::serialization::Serializable; use rand::rng; use ssz::{Decode, Encode}; use crate::leansig::{private_key::PrivateKey, signature::Signature}; + const LEGACY_SIGNATURE_SIZE: usize = 3112; + + #[derive(ssz_derive::Encode)] + struct LegacySignature { + inner: FixedBytes, + } + #[test] fn test_serialization_roundtrip() { let mut rng = rng(); @@ -152,4 +161,25 @@ mod tests { // verify roundtrip assert_eq!(signature, signature_decoded); } + + #[test] + fn test_ssz_bytes_match_legacy_signature_wrapper() { + let mut rng = rng(); + let activation_epoch = 0; + let num_active_epochs = 10; + + let (_, private_key) = + PrivateKey::generate_key_pair(&mut rng, activation_epoch, num_active_epochs); + + let epoch = 5; + let message = [0u8; 32]; + let signature = private_key.sign(&message, epoch).unwrap(); + + let legacy_signature = LegacySignature { + inner: FixedBytes::try_from(signature.as_lean_sig().to_bytes().as_slice()) + .expect("legacy signature bytes should match fixed size"), + }; + + assert_eq!(legacy_signature.as_ssz_bytes(), signature.as_ssz_bytes()); + } } From 4013c95e59fe8cd12a856a23d20ff1fa60d0337b Mon Sep 17 00:00:00 2001 From: Kolby Moroz Liebl <31669092+KolbyML@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:02:12 -0700 Subject: [PATCH 20/20] Clean up PR --- .../post_quantum/src/leansig/signature.rs | 54 +-- testing/lean-spec-tests/src/types/ssz_test.rs | 333 +++++++++++------- 2 files changed, 212 insertions(+), 175 deletions(-) diff --git a/crates/crypto/post_quantum/src/leansig/signature.rs b/crates/crypto/post_quantum/src/leansig/signature.rs index 3fd7153b1..71f76a519 100644 --- a/crates/crypto/post_quantum/src/leansig/signature.rs +++ b/crates/crypto/post_quantum/src/leansig/signature.rs @@ -1,15 +1,25 @@ use leansig::{signature::SignatureScheme, MESSAGE_LENGTH}; use serde::{Deserialize, Serialize}; -use ssz::{Decode, DecodeError, Encode}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; use crate::leansig::{public_key::PublicKey, LeanSigScheme}; /// The inner leansig signature type with built-in SSZ support. pub type LeanSigSignature = ::Signature; +const BLANK_SIGNATURE_SSZ_BYTES: [u8; 40] = [ + 36, 0, 0, 0, // offset_path = 36 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rho (28 zeros) + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // + 40, 0, 0, 0, // offset_hashes = 40 + 4, 0, 0, 0, // path: empty HashTreeOpening +]; + /// Wrapper around leansig's signature type. /// Uses leansig's built-in SSZ encoding for interoperability with other clients. -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Encode, Decode)] +#[ssz(struct_behaviour = "transparent")] pub struct Signature { pub inner: LeanSigSignature, } @@ -31,51 +41,13 @@ impl PartialEq for Signature { impl Eq for Signature {} -impl Encode for Signature { - fn is_ssz_fixed_len() -> bool { - ::is_ssz_fixed_len() - } - - fn ssz_bytes_len(&self) -> usize { - self.inner.ssz_bytes_len() - } - - fn ssz_append(&self, buf: &mut Vec) { - self.inner.ssz_append(buf) - } -} - -impl Decode for Signature { - fn is_ssz_fixed_len() -> bool { - ::is_ssz_fixed_len() - } - - fn from_ssz_bytes(bytes: &[u8]) -> Result { - Ok(Self { - inner: LeanSigSignature::from_ssz_bytes(bytes)?, - }) - } -} - impl Signature { - pub fn new(inner: LeanSigSignature) -> Self { - Self { inner } - } - /// Create a blank/placeholder signature. /// /// This decodes from minimal valid SSZ bytes, avoiding expensive key generation. /// Only use in contexts where the signature won't be validated. pub fn blank() -> Self { - // 40 bytes: offset_path(4) + rho(28 zeros) + offset_hashes(4) + path(4) - const BYTES: [u8; 40] = [ - 36, 0, 0, 0, // offset_path = 36 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rho (28 zeros) - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // - 40, 0, 0, 0, // offset_hashes = 40 - 4, 0, 0, 0, // path: empty HashTreeOpening - ]; - Self::from_ssz_bytes(&BYTES).expect("blank signature bytes are valid") + Self::from_ssz_bytes(&BLANK_SIGNATURE_SSZ_BYTES).expect("blank signature bytes are valid") } /// Create a mock signature for testing purposes. diff --git a/testing/lean-spec-tests/src/types/ssz_test.rs b/testing/lean-spec-tests/src/types/ssz_test.rs index 46d6cf7a0..14bf14d3b 100644 --- a/testing/lean-spec-tests/src/types/ssz_test.rs +++ b/testing/lean-spec-tests/src/types/ssz_test.rs @@ -29,8 +29,8 @@ use ream_post_quantum_crypto::leansig::{public_key::PublicKey, signature::Signat use serde::Deserialize; use ssz::Decode; use ssz_types::{ + typenum::{U1048576, U1073741824, U262144}, BitList, VariableList, - typenum::{U262144, U1048576, U1073741824}, }; // ============================================================================ @@ -71,69 +71,6 @@ pub struct SSZTest { pub serialized: String, } -// ============================================================================ -// Macros -// ============================================================================ - -/// Creates a passthrough JSON wrapper that deserializes directly to the inner type. -macro_rules! passthrough_conversion { - ($name:ident, $inner:ty) => { - #[derive(Debug, Deserialize, Clone)] - #[serde(transparent)] - pub struct $name(pub $inner); - - impl TryFrom<&$name> for $inner { - type Error = anyhow::Error; - fn try_from(value: &$name) -> anyhow::Result { - Ok(value.0.clone()) - } - } - }; -} - -/// Creates a TryFrom impl where all fields are copied directly. -macro_rules! simple_conversion { - ($json:ident => $target:ty { $($field:ident),+ }) => { - impl TryFrom<&$json> for $target { - type Error = anyhow::Error; - fn try_from(value: &$json) -> anyhow::Result { - Ok(Self { - $($field: value.$field),+ - }) - } - } - }; -} - -/// Creates a TryFrom impl where all fields are converted via try_into(). -macro_rules! nested_conversion { - ($json:ident => $target:ty { $($field:ident),+ }) => { - impl TryFrom<&$json> for $target { - type Error = anyhow::Error; - fn try_from(value: &$json) -> anyhow::Result { - Ok(Self { - $($field: (&value.$field).try_into()?),+ - }) - } - } - }; -} - -/// Creates a TryFrom impl with custom conversion expression per field. -macro_rules! custom_conversion { - ($json:ident as $val:ident => $target:ty { $($field:ident: $conv:expr),+ $(,)? }) => { - impl TryFrom<&$json> for $target { - type Error = anyhow::Error; - #[allow(clippy::redundant_closure_call)] - fn try_from($val: &$json) -> anyhow::Result { - Ok(Self { - $($field: $conv),+ - }) - } - } - }; -} - // ============================================================================ // Common JSON wrapper types // ============================================================================ @@ -148,8 +85,29 @@ pub struct AggregationBitsJSON { pub data: Vec, } -passthrough_conversion!(CheckpointJSON, ReamCheckpoint); -passthrough_conversion!(AttestationDataJSON, ReamAttestationData); +#[derive(Debug, Deserialize, Clone)] +#[serde(transparent)] +pub struct CheckpointJSON(pub ReamCheckpoint); + +impl TryFrom<&CheckpointJSON> for ReamCheckpoint { + type Error = anyhow::Error; + + fn try_from(value: &CheckpointJSON) -> anyhow::Result { + Ok(value.0.clone()) + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(transparent)] +pub struct AttestationDataJSON(pub ReamAttestationData); + +impl TryFrom<&AttestationDataJSON> for ReamAttestationData { + type Error = anyhow::Error; + + fn try_from(value: &AttestationDataJSON) -> anyhow::Result { + Ok(value.0.clone()) + } +} // ============================================================================ // Config @@ -161,7 +119,15 @@ pub struct ConfigJSON { pub genesis_time: u64, } -simple_conversion!(ConfigJSON => ReamConfig { genesis_time }); +impl TryFrom<&ConfigJSON> for ReamConfig { + type Error = anyhow::Error; + + fn try_from(value: &ConfigJSON) -> anyhow::Result { + Ok(Self { + genesis_time: value.genesis_time, + }) + } +} // ============================================================================ // BlockHeader @@ -177,7 +143,19 @@ pub struct BlockHeaderJSON { pub body_root: B256, } -simple_conversion!(BlockHeaderJSON => ReamBlockHeader { slot, proposer_index, parent_root, state_root, body_root }); +impl TryFrom<&BlockHeaderJSON> for ReamBlockHeader { + type Error = anyhow::Error; + + fn try_from(value: &BlockHeaderJSON) -> anyhow::Result { + Ok(Self { + slot: value.slot, + proposer_index: value.proposer_index, + parent_root: value.parent_root, + state_root: value.state_root, + body_root: value.body_root, + }) + } +} // ============================================================================ // Validator @@ -189,16 +167,21 @@ pub struct ValidatorJSON { pub index: u64, } -custom_conversion!(ValidatorJSON as value => ReamValidator { - public_key: { +impl TryFrom<&ValidatorJSON> for ReamValidator { + type Error = anyhow::Error; + + fn try_from(value: &ValidatorJSON) -> anyhow::Result { let bytes = decode_hex(&value.pubkey)?; if bytes.len() != 52 { return Err(anyhow!("Expected 52-byte pubkey, got {}", bytes.len())); } - PublicKey::from(&bytes[..]) - }, - index: value.index, -}); + + Ok(Self { + public_key: PublicKey::from(&bytes[..]), + index: value.index, + }) + } +} // ============================================================================ // Attestations @@ -211,10 +194,16 @@ pub struct AggregatedAttestationJSON { pub data: ReamAttestationData, } -custom_conversion!(AggregatedAttestationJSON as value => ReamAggregatedAttestation { - aggregation_bits: bools_to_bitlist(&value.aggregation_bits.data)?, - message: value.data.clone(), -}); +impl TryFrom<&AggregatedAttestationJSON> for ReamAggregatedAttestation { + type Error = anyhow::Error; + + fn try_from(value: &AggregatedAttestationJSON) -> anyhow::Result { + Ok(Self { + aggregation_bits: bools_to_bitlist(&value.aggregation_bits.data)?, + message: value.data.clone(), + }) + } +} #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -223,10 +212,16 @@ pub struct AttestationJSON { pub data: ReamAttestationData, } -custom_conversion!(AttestationJSON as value => ReamAggregatedAttestations { - validator_id: value.validator_id, - data: value.data.clone(), -}); +impl TryFrom<&AttestationJSON> for ReamAggregatedAttestations { + type Error = anyhow::Error; + + fn try_from(value: &AttestationJSON) -> anyhow::Result { + Ok(Self { + validator_id: value.validator_id, + data: value.data.clone(), + }) + } +} #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -236,11 +231,17 @@ pub struct SignedAttestationJSON { pub signature: String, } -custom_conversion!(SignedAttestationJSON as value => ReamSignedAttestation { - validator_id: value.validator_id, - message: value.message.clone(), - signature: decode_signature(&value.signature)?, -}); +impl TryFrom<&SignedAttestationJSON> for ReamSignedAttestation { + type Error = anyhow::Error; + + fn try_from(value: &SignedAttestationJSON) -> anyhow::Result { + Ok(Self { + validator_id: value.validator_id, + message: value.message.clone(), + signature: decode_signature(&value.signature)?, + }) + } +} // ============================================================================ // Block @@ -251,13 +252,23 @@ pub struct BlockBodyJSON { pub attestations: DataListJSON, } -custom_conversion!(BlockBodyJSON as value => ReamBlockBody { - attestations: VariableList::try_from( - value.attestations.data.iter() - .map(TryInto::try_into) - .collect::, _>>()? - ).map_err(|err| anyhow!("{err}"))?, -}); +impl TryFrom<&BlockBodyJSON> for ReamBlockBody { + type Error = anyhow::Error; + + fn try_from(value: &BlockBodyJSON) -> anyhow::Result { + Ok(Self { + attestations: VariableList::try_from( + value + .attestations + .data + .iter() + .map(TryInto::try_into) + .collect::, _>>()?, + ) + .map_err(|err| anyhow!("{err}"))?, + }) + } +} #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -269,13 +280,19 @@ pub struct BlockJSON { pub body: BlockBodyJSON, } -custom_conversion!(BlockJSON as value => ReamBlock { - slot: value.slot, - proposer_index: value.proposer_index, - parent_root: value.parent_root, - state_root: value.state_root, - body: (&value.body).try_into()?, -}); +impl TryFrom<&BlockJSON> for ReamBlock { + type Error = anyhow::Error; + + fn try_from(value: &BlockJSON) -> anyhow::Result { + Ok(Self { + slot: value.slot, + proposer_index: value.proposer_index, + parent_root: value.parent_root, + state_root: value.state_root, + body: (&value.body).try_into()?, + }) + } +} // ============================================================================ // State @@ -296,24 +313,38 @@ pub struct StateJSON { pub justifications_validators: DataListJSON, } -custom_conversion!(StateJSON as value => ReamState { - config: (&value.config).try_into()?, - slot: value.slot, - latest_block_header: (&value.latest_block_header).try_into()?, - latest_justified: value.latest_justified, - latest_finalized: value.latest_finalized, - historical_block_hashes: VariableList::try_from(value.historical_block_hashes.data.clone()) - .map_err(|err| anyhow!("{err}"))?, - justified_slots: bools_to_bitlist::(&value.justified_slots.data)?, - validators: VariableList::try_from( - value.validators.data.iter() - .map(TryInto::try_into) - .collect::, _>>()? - ).map_err(|err| anyhow!("{err}"))?, - justifications_roots: VariableList::try_from(value.justifications_roots.data.clone()) - .map_err(|err| anyhow!("{err}"))?, - justifications_validators: bools_to_bitlist::(&value.justifications_validators.data)?, -}); +impl TryFrom<&StateJSON> for ReamState { + type Error = anyhow::Error; + + fn try_from(value: &StateJSON) -> anyhow::Result { + Ok(Self { + config: (&value.config).try_into()?, + slot: value.slot, + latest_block_header: (&value.latest_block_header).try_into()?, + latest_justified: value.latest_justified, + latest_finalized: value.latest_finalized, + historical_block_hashes: VariableList::try_from( + value.historical_block_hashes.data.clone(), + ) + .map_err(|err| anyhow!("{err}"))?, + justified_slots: bools_to_bitlist::(&value.justified_slots.data)?, + validators: VariableList::try_from( + value + .validators + .data + .iter() + .map(TryInto::try_into) + .collect::, _>>()?, + ) + .map_err(|err| anyhow!("{err}"))?, + justifications_roots: VariableList::try_from(value.justifications_roots.data.clone()) + .map_err(|err| anyhow!("{err}"))?, + justifications_validators: bools_to_bitlist::( + &value.justifications_validators.data, + )?, + }) + } +} // ============================================================================ // Signature-related types @@ -331,11 +362,17 @@ pub struct AggregatedSignatureProofJSON { pub proof_data: ProofDataJSON, } -custom_conversion!(AggregatedSignatureProofJSON as value => ReamAggregatedSignatureProof { - participants: bools_to_bitlist(&value.participants.data)?, - proof_data: VariableList::::try_from(decode_hex(&value.proof_data.data)?) - .map_err(|err| anyhow!("{err}"))?, -}); +impl TryFrom<&AggregatedSignatureProofJSON> for ReamAggregatedSignatureProof { + type Error = anyhow::Error; + + fn try_from(value: &AggregatedSignatureProofJSON) -> anyhow::Result { + Ok(Self { + participants: bools_to_bitlist(&value.participants.data)?, + proof_data: VariableList::::try_from(decode_hex(&value.proof_data.data)?) + .map_err(|err| anyhow!("{err}"))?, + }) + } +} #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -344,14 +381,24 @@ pub struct BlockSignaturesJSON { pub proposer_signature: String, } -custom_conversion!(BlockSignaturesJSON as value => ReamBlockSignatures { - attestation_signatures: VariableList::try_from( - value.attestation_signatures.data.iter() - .map(TryInto::try_into) - .collect::, _>>()? - ).map_err(|err| anyhow!("{err}"))?, - proposer_signature: decode_signature(&value.proposer_signature)?, -}); +impl TryFrom<&BlockSignaturesJSON> for ReamBlockSignatures { + type Error = anyhow::Error; + + fn try_from(value: &BlockSignaturesJSON) -> anyhow::Result { + Ok(Self { + attestation_signatures: VariableList::try_from( + value + .attestation_signatures + .data + .iter() + .map(TryInto::try_into) + .collect::, _>>()?, + ) + .map_err(|err| anyhow!("{err}"))?, + proposer_signature: decode_signature(&value.proposer_signature)?, + }) + } +} #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -360,7 +407,16 @@ pub struct BlockWithAttestationJSON { pub proposer_attestation: AttestationJSON, } -nested_conversion!(BlockWithAttestationJSON => ReamBlockWithAttestation { block, proposer_attestation }); +impl TryFrom<&BlockWithAttestationJSON> for ReamBlockWithAttestation { + type Error = anyhow::Error; + + fn try_from(value: &BlockWithAttestationJSON) -> anyhow::Result { + Ok(Self { + block: (&value.block).try_into()?, + proposer_attestation: (&value.proposer_attestation).try_into()?, + }) + } +} #[derive(Debug, Deserialize, Clone)] #[serde(rename_all = "camelCase")] @@ -369,4 +425,13 @@ pub struct SignedBlockWithAttestationJSON { pub signature: BlockSignaturesJSON, } -nested_conversion!(SignedBlockWithAttestationJSON => ReamSignedBlockWithAttestation { message, signature }); +impl TryFrom<&SignedBlockWithAttestationJSON> for ReamSignedBlockWithAttestation { + type Error = anyhow::Error; + + fn try_from(value: &SignedBlockWithAttestationJSON) -> anyhow::Result { + Ok(Self { + message: (&value.message).try_into()?, + signature: (&value.signature).try_into()?, + }) + } +}