From 396ec574789fabde91039dcfeb2d4352f1bda9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:01:33 -0300 Subject: [PATCH 01/10] test: implement test runner --- Cargo.lock | 6 + crates/blockchain/state_transition/Cargo.toml | 15 + .../tests/signature_spectests.rs | 111 ++++ .../state_transition/tests/signature_types.rs | 527 ++++++++++++++++++ 4 files changed, 659 insertions(+) create mode 100644 crates/blockchain/state_transition/tests/signature_spectests.rs create mode 100644 crates/blockchain/state_transition/tests/signature_types.rs diff --git a/Cargo.lock b/Cargo.lock index 972696c..eaad591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1886,11 +1886,17 @@ name = "ethlambda-state-transition" version = "0.1.0" dependencies = [ "datatest-stable 0.3.3", + "ethereum_ssz", + "ethereum_ssz_derive", + "ethlambda-blockchain", "ethlambda-types", "hex", + "leansig", "serde", "serde_json", + "ssz_types", "thiserror 2.0.17", + "tree_hash", ] [[package]] diff --git a/crates/blockchain/state_transition/Cargo.toml b/crates/blockchain/state_transition/Cargo.toml index be1e343..76f2c24 100644 --- a/crates/blockchain/state_transition/Cargo.toml +++ b/crates/blockchain/state_transition/Cargo.toml @@ -22,7 +22,22 @@ hex.workspace = true datatest-stable = "0.3.3" +# For signature tests - uses Store::on_block with actual signature verification +# Note: Do NOT enable skip-signature-verification feature here +ethlambda-blockchain.workspace = true + +leansig.workspace = true +tree_hash = "0.12.0" +ethereum_ssz = "0.10.0" +ethereum_ssz_derive = "0.10.0" +ssz_types = "0.14.0" + [[test]] name = "stf_spectests" path = "tests/stf_spectests.rs" harness = false + +[[test]] +name = "signature_spectests" +path = "tests/signature_spectests.rs" +harness = false diff --git a/crates/blockchain/state_transition/tests/signature_spectests.rs b/crates/blockchain/state_transition/tests/signature_spectests.rs new file mode 100644 index 0000000..14b494b --- /dev/null +++ b/crates/blockchain/state_transition/tests/signature_spectests.rs @@ -0,0 +1,111 @@ +use std::path::Path; + +use ethlambda_blockchain::{store::Store, SECONDS_PER_SLOT}; +use ethlambda_types::{ + block::{Block, SignedBlockWithAttestation}, + primitives::TreeHash, + state::State, +}; + +mod signature_types; +use signature_types::VerifySignaturesTestVector; + +const SUPPORTED_FIXTURE_FORMAT: &str = "verify_signatures_test"; + +fn run(path: &Path) -> datatest_stable::Result<()> { + let tests = VerifySignaturesTestVector::from_file(path)?; + + for (name, test) in tests.tests { + if test.info.fixture_format != SUPPORTED_FIXTURE_FORMAT { + return Err(format!( + "Unsupported fixture format: {} (expected {})", + test.info.fixture_format, SUPPORTED_FIXTURE_FORMAT + ) + .into()); + } + + println!("Running test: {}", name); + + // Step 1: Populate the pre-state with the test fixture + let anchor_state: State = test.anchor_state.into(); + + // Create anchor block from the state's latest block header + let anchor_block = Block { + slot: anchor_state.latest_block_header.slot, + proposer_index: anchor_state.latest_block_header.proposer_index, + parent_root: anchor_state.latest_block_header.parent_root, + state_root: anchor_state.tree_hash_root(), + body: Default::default(), + }; + + // Initialize the store with the anchor state and block + let genesis_time = anchor_state.config.genesis_time; + let mut store = Store::get_forkchoice_store(anchor_state, anchor_block); + + // Step 2: Run the state transition function with the block fixture + let signed_block: SignedBlockWithAttestation = test.signed_block_with_attestation.into(); + + // Debug: print details for specific test + if name.contains("test_proposer_signature[") && !name.contains("attester") { + let proposer_att_data = &signed_block.message.proposer_attestation.data; + let message_hash = proposer_att_data.tree_hash_root(); + println!("[test_proposer_signature] AttestationData tree hash: 0x{}", hex::encode(message_hash)); + println!( + "[test_proposer_signature] Slot (epoch): {}", + signed_block.message.proposer_attestation.data.slot + ); + println!( + "[test_proposer_signature] Proposer validator_id: {}", + signed_block.message.proposer_attestation.validator_id + ); + + // Print signature bytes (first 64 bytes) + let sig_bytes: &[u8] = signed_block.signature.proposer_signature.as_ref(); + println!( + "[test_proposer_signature] Signature bytes (first 64): {}", + hex::encode(&sig_bytes[..64.min(sig_bytes.len())]) + ); + println!("[test_proposer_signature] Signature total length: {}", sig_bytes.len()); + } + + // Advance time to the block's slot + let block_time = signed_block.message.block.slot * SECONDS_PER_SLOT + genesis_time; + store.on_tick(block_time, true); + + // Process the block (this includes signature verification) + let result = store.on_block(signed_block); + + // Step 3: Check that it succeeded or failed as expected + match (result.is_ok(), test.expect_exception.as_ref()) { + (true, None) => { + // Expected success, got success + } + (true, Some(expected_err)) => { + return Err(format!( + "Test '{}' failed: expected exception '{}' but got success", + name, expected_err + ) + .into()); + } + (false, None) => { + return Err(format!( + "Test '{}' failed: expected success but got failure: {:?}", + name, + result.err() + ) + .into()); + } + (false, Some(_)) => { + // Expected failure, got failure + } + } + } + + Ok(()) +} + +datatest_stable::harness!({ + test = run, + root = "../../../../ethlambda/leanSpec/fixtures/consensus/verify_signatures", + pattern = r".*\.json" +}); diff --git a/crates/blockchain/state_transition/tests/signature_types.rs b/crates/blockchain/state_transition/tests/signature_types.rs new file mode 100644 index 0000000..7303439 --- /dev/null +++ b/crates/blockchain/state_transition/tests/signature_types.rs @@ -0,0 +1,527 @@ +use ethlambda_types::attestation::{ + AggregatedAttestation as EthAggregatedAttestation, + AggregationBits as EthAggregationBits, Attestation as EthAttestation, + AttestationData as EthAttestationData, XmssSignature, +}; +use ethlambda_types::block::{ + AggregatedAttestations, AttestationSignatures, Block as EthBlock, + BlockBody as EthBlockBody, BlockSignatures, BlockWithAttestation, + NaiveAggregatedSignature, SignedBlockWithAttestation, +}; +use ethlambda_types::primitives::{BitList, Encode, H256, VariableList}; +use ethlambda_types::state::{Checkpoint as EthCheckpoint, State, ValidatorPubkeyBytes}; +use serde::Deserialize; +use ssz_derive::{Decode as SszDecode, Encode as SszEncode}; +use ssz_types::typenum::{U28, U32}; +use ssz_types::FixedVector; +use std::collections::HashMap; +use std::path::Path; + +// ============================================================================ +// SSZ Types matching leansig's GeneralizedXMSSSignature structure +// ============================================================================ + +/// A single hash digest (8 field elements = 32 bytes) +pub type HashDigest = FixedVector; + +/// Randomness (7 field elements = 28 bytes) +pub type Rho = FixedVector; + +/// SSZ-compatible HashTreeOpening matching leansig's structure +#[derive(Clone, SszEncode, SszDecode)] +pub struct SszHashTreeOpening { + pub co_path: Vec, +} + +/// SSZ-compatible XMSS Signature matching leansig's GeneralizedXMSSSignature +#[derive(Clone, SszEncode, SszDecode)] +pub struct SszXmssSignature { + pub path: SszHashTreeOpening, + pub rho: Rho, + pub hashes: Vec, +} + +/// Root struct for verify signatures test vectors +#[derive(Debug, Clone, Deserialize)] +pub struct VerifySignaturesTestVector { + #[serde(flatten)] + pub tests: HashMap, +} + +impl VerifySignaturesTestVector { + /// Load a verify signatures test vector from a JSON file + pub fn from_file>(path: P) -> Result> { + let content = std::fs::read_to_string(path)?; + let test_vector = serde_json::from_str(&content)?; + Ok(test_vector) + } +} + +/// A single verify signatures test case +#[derive(Debug, Clone, Deserialize)] +pub struct VerifySignaturesTest { + #[allow(dead_code)] + pub network: String, + #[serde(rename = "leanEnv")] + #[allow(dead_code)] + pub lean_env: String, + #[serde(rename = "anchorState")] + pub anchor_state: TestState, + #[serde(rename = "signedBlockWithAttestation")] + pub signed_block_with_attestation: TestSignedBlockWithAttestation, + #[serde(rename = "expectException")] + pub expect_exception: Option, + #[serde(rename = "_info")] + #[allow(dead_code)] + pub info: TestInfo, +} + +/// Pre-state of the beacon chain for signature tests +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct TestState { + pub config: Config, + pub slot: u64, + #[serde(rename = "latestBlockHeader")] + pub latest_block_header: BlockHeader, + #[serde(rename = "latestJustified")] + pub latest_justified: Checkpoint, + #[serde(rename = "latestFinalized")] + pub latest_finalized: Checkpoint, + #[serde(rename = "historicalBlockHashes")] + pub historical_block_hashes: Container, + #[serde(rename = "justifiedSlots")] + pub justified_slots: Container, + pub validators: Container, + #[serde(rename = "justificationsRoots")] + pub justifications_roots: Container, + #[serde(rename = "justificationsValidators")] + pub justifications_validators: Container, +} + +impl From for State { + fn from(value: TestState) -> Self { + let historical_block_hashes = + VariableList::new(value.historical_block_hashes.data).unwrap(); + let validators = + VariableList::new(value.validators.data.into_iter().map(Into::into).collect()).unwrap(); + let justifications_roots = VariableList::new(value.justifications_roots.data).unwrap(); + + State { + config: value.config.into(), + slot: value.slot, + latest_block_header: value.latest_block_header.into(), + latest_justified: value.latest_justified.into(), + latest_finalized: value.latest_finalized.into(), + historical_block_hashes, + justified_slots: BitList::with_capacity(0).unwrap(), + validators, + justifications_roots, + justifications_validators: BitList::with_capacity(0).unwrap(), + } + } +} + +/// Configuration for the beacon chain +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + #[serde(rename = "genesisTime")] + pub genesis_time: u64, +} + +impl From for ethlambda_types::state::ChainConfig { + fn from(value: Config) -> Self { + ethlambda_types::state::ChainConfig { + genesis_time: value.genesis_time, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Checkpoint { + pub root: H256, + pub slot: u64, +} + +impl From for EthCheckpoint { + fn from(value: Checkpoint) -> Self { + Self { + root: value.root, + slot: value.slot, + } + } +} + +/// Block header representing the latest block +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct BlockHeader { + pub slot: u64, + #[serde(rename = "proposerIndex")] + pub proposer_index: u64, + #[serde(rename = "parentRoot")] + pub parent_root: H256, + #[serde(rename = "stateRoot")] + pub state_root: H256, + #[serde(rename = "bodyRoot")] + pub body_root: H256, +} + +impl From for ethlambda_types::block::BlockHeader { + fn from(value: BlockHeader) -> Self { + 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 information +#[derive(Debug, Clone, Deserialize)] +pub struct Validator { + pub index: u64, + #[serde(deserialize_with = "deser_pubkey_hex")] + pub pubkey: ValidatorPubkeyBytes, +} + +impl From for ethlambda_types::state::Validator { + fn from(value: Validator) -> Self { + Self { + index: value.index, + pubkey: value.pubkey, + } + } +} + +/// Generic container for arrays +#[derive(Debug, Clone, Deserialize)] +pub struct Container { + pub data: Vec, +} + +/// Signed block with attestation and signature +#[derive(Debug, Clone, Deserialize)] +pub struct TestSignedBlockWithAttestation { + pub message: TestBlockWithAttestation, + pub signature: TestSignatureBundle, +} + +impl From for SignedBlockWithAttestation { + fn from(value: TestSignedBlockWithAttestation) -> Self { + let message = BlockWithAttestation { + block: value.message.block.into(), + proposer_attestation: value.message.proposer_attestation.into(), + }; + + let proposer_signature = value.signature.proposer_signature.to_xmss_signature(); + + // For now, attestation signatures use placeholder proofData (for future SNARK aggregation). + // We create empty NaiveAggregatedSignature entries to match the attestation count. + // The actual signature verification for attestations is not yet implemented. + let attestation_signatures: AttestationSignatures = value + .signature + .attestation_signatures + .data + .into_iter() + .map(|_att_sig| { + // Create empty signature list for each attestation + // Real implementation would parse proofData or individual signatures + let empty: NaiveAggregatedSignature = Vec::new().try_into().unwrap(); + empty + }) + .collect::>() + .try_into() + .expect("too many attestation signatures"); + + SignedBlockWithAttestation { + message, + signature: BlockSignatures { + proposer_signature, + attestation_signatures, + }, + } + } +} + +/// Block with proposer attestation (the message that gets signed) +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct TestBlockWithAttestation { + pub block: Block, + #[serde(rename = "proposerAttestation")] + pub proposer_attestation: ProposerAttestation, +} + +/// A block to be processed +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct Block { + pub slot: u64, + #[serde(rename = "proposerIndex")] + pub proposer_index: u64, + #[serde(rename = "parentRoot")] + pub parent_root: H256, + #[serde(rename = "stateRoot")] + pub state_root: H256, + pub body: BlockBody, +} + +impl From for EthBlock { + fn from(value: Block) -> Self { + Self { + slot: value.slot, + proposer_index: value.proposer_index, + parent_root: value.parent_root, + state_root: value.state_root, + body: value.body.into(), + } + } +} + +/// Block body containing attestations +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct BlockBody { + pub attestations: Container, +} + +impl From for EthBlockBody { + fn from(value: BlockBody) -> Self { + let attestations: AggregatedAttestations = value + .attestations + .data + .into_iter() + .map(Into::into) + .collect::>() + .try_into() + .expect("too many attestations"); + Self { attestations } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct AggregatedAttestation { + #[serde(rename = "aggregationBits")] + pub aggregation_bits: AggregationBits, + pub data: AttestationData, +} + +impl From for EthAggregatedAttestation { + fn from(value: AggregatedAttestation) -> Self { + Self { + aggregation_bits: value.aggregation_bits.into(), + data: value.data.into(), + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct AggregationBits { + pub data: Vec, +} + +impl From for EthAggregationBits { + fn from(value: AggregationBits) -> Self { + let mut bits = EthAggregationBits::with_capacity(value.data.len()).unwrap(); + for (i, &b) in value.data.iter().enumerate() { + bits.set(i, b).unwrap(); + } + bits + } +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct AttestationData { + pub slot: u64, + pub head: Checkpoint, + pub target: Checkpoint, + pub source: Checkpoint, +} + +impl From for EthAttestationData { + fn from(value: AttestationData) -> Self { + Self { + slot: value.slot, + head: value.head.into(), + target: value.target.into(), + source: value.source.into(), + } + } +} + +/// Proposer attestation structure +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct ProposerAttestation { + #[serde(rename = "validatorId")] + pub validator_id: u64, + pub data: AttestationData, +} + +impl From for EthAttestation { + fn from(value: ProposerAttestation) -> Self { + Self { + validator_id: value.validator_id, + data: value.data.into(), + } + } +} + +/// Bundle of signatures for block and attestations +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct TestSignatureBundle { + #[serde(rename = "proposerSignature")] + pub proposer_signature: ProposerSignature, + #[serde(rename = "attestationSignatures")] + pub attestation_signatures: Container, +} + +/// XMSS signature structure as it appears in JSON +#[derive(Debug, Clone, Deserialize)] +pub struct ProposerSignature { + pub path: SignaturePath, + pub rho: RhoData, + pub hashes: HashesData, +} + +impl ProposerSignature { + /// Convert to XmssSignature (FixedVector of bytes). + /// + /// Constructs an SSZ-encoded signature matching leansig's GeneralizedXMSSSignature format. + pub fn to_xmss_signature(&self) -> XmssSignature { + // Build SSZ types from JSON data + let ssz_sig = self.to_ssz_signature(); + + // Encode to SSZ bytes + let bytes = ssz_sig.as_ssz_bytes(); + + // Pad to exactly SignatureSize bytes (3112) + let sig_size = 3112; + let mut padded = bytes; + padded.resize(sig_size, 0); + + XmssSignature::new(padded).expect("signature size mismatch") + } + + /// Convert to SSZ signature type + fn to_ssz_signature(&self) -> SszXmssSignature { + // Convert path siblings to HashDigest (Vec of 32 bytes each) + let co_path: Vec = self + .path + .siblings + .data + .iter() + .map(|sibling| { + let bytes: Vec = sibling + .data + .iter() + .flat_map(|&val| val.to_le_bytes()) + .collect(); + HashDigest::new(bytes).expect("Invalid sibling length") + }) + .collect(); + + // Convert rho (7 field elements = 28 bytes) + let rho_bytes: Vec = self + .rho + .data + .iter() + .flat_map(|&val| val.to_le_bytes()) + .collect(); + let rho = Rho::new(rho_bytes).expect("Invalid rho length"); + + // Convert hashes to HashDigest + let hashes: Vec = self + .hashes + .data + .iter() + .map(|hash| { + let bytes: Vec = hash + .data + .iter() + .flat_map(|&val| val.to_le_bytes()) + .collect(); + HashDigest::new(bytes).expect("Invalid hash length") + }) + .collect(); + + SszXmssSignature { + path: SszHashTreeOpening { co_path }, + rho, + hashes, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SignaturePath { + pub siblings: Container, +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct HashElement { + pub data: [u32; 8], +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RhoData { + pub data: [u32; 7], +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HashesData { + pub data: Vec, +} + +/// Attestation signature from a validator +/// Note: proofData is for future SNARK aggregation, currently just placeholder +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct AttestationSignature { + pub participants: AggregationBits, + #[serde(rename = "proofData")] + pub proof_data: ProofData, +} + +/// Placeholder for future SNARK proof data +#[derive(Debug, Clone, Deserialize)] +pub struct ProofData { + pub data: String, +} + +/// Test metadata and information +#[derive(Debug, Clone, Deserialize)] +#[allow(dead_code)] +pub struct TestInfo { + pub hash: String, + pub comment: String, + #[serde(rename = "testId")] + pub test_id: String, + pub description: String, + #[serde(rename = "fixtureFormat")] + pub fixture_format: String, +} + +// Helpers + +pub fn deser_pubkey_hex<'de, D>(d: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::de::Error; + + let value = String::deserialize(d)?; + let pubkey: ValidatorPubkeyBytes = hex::decode(value.strip_prefix("0x").unwrap_or(&value)) + .map_err(|_| D::Error::custom("ValidatorPubkey value is not valid hex"))? + .try_into() + .map_err(|_| D::Error::custom("ValidatorPubkey length != 52"))?; + Ok(pubkey) +} From 59689578d4a738016971d44db34722878d6cb280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:52:19 -0300 Subject: [PATCH 02/10] fix: use attestation slot instead of target's --- crates/blockchain/src/store.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 273cb0c..e757484 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -303,7 +303,8 @@ impl Store { #[cfg(not(feature = "skip-signature-verification"))] { use ethlambda_types::signature::ValidatorSignature; - let epoch = target.slot.try_into().expect("slot exceeds u32"); + // Use attestation.data.slot as epoch (matching what Zeam and ethlambda use for signing) + let epoch: u32 = attestation.data.slot.try_into().expect("slot exceeds u32"); let signature = ValidatorSignature::from_bytes(&signed_attestation.signature) .map_err(|_| StoreError::SignatureDecodingFailed)?; if !validator_pubkey.is_valid(epoch, &message, &signature) { From d108b8edbd2653d34718f7574659909bd5be7cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:39:03 -0300 Subject: [PATCH 03/10] refactor: move test runner to ethlambda-blockchain --- Cargo.lock | 11 +++++------ crates/blockchain/Cargo.toml | 11 +++++++++++ crates/blockchain/state_transition/Cargo.toml | 15 --------------- .../tests/signature_spectests.rs | 7 ++++++- .../tests/signature_types.rs | 8 +++++++- 5 files changed, 29 insertions(+), 23 deletions(-) rename crates/blockchain/{state_transition => }/tests/signature_spectests.rs (85%) rename crates/blockchain/{state_transition => }/tests/signature_types.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index eaad591..ccfeb54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1821,18 +1821,23 @@ name = "ethlambda-blockchain" version = "0.1.0" dependencies = [ "datatest-stable 0.3.3", + "ethereum_ssz", + "ethereum_ssz_derive", "ethlambda-fork-choice", "ethlambda-state-transition", "ethlambda-storage", "ethlambda-types", "hex", + "leansig", "prometheus", "serde", "serde_json", "spawned-concurrency", + "ssz_types", "thiserror 2.0.17", "tokio", "tracing", + "tree_hash", ] [[package]] @@ -1886,17 +1891,11 @@ name = "ethlambda-state-transition" version = "0.1.0" dependencies = [ "datatest-stable 0.3.3", - "ethereum_ssz", - "ethereum_ssz_derive", - "ethlambda-blockchain", "ethlambda-types", "hex", - "leansig", "serde", "serde_json", - "ssz_types", "thiserror 2.0.17", - "tree_hash", ] [[package]] diff --git a/crates/blockchain/Cargo.toml b/crates/blockchain/Cargo.toml index 85c6c46..589abf9 100644 --- a/crates/blockchain/Cargo.toml +++ b/crates/blockchain/Cargo.toml @@ -27,15 +27,26 @@ thiserror.workspace = true tracing.workspace = true prometheus.workspace = true +hex.workspace = true [dev-dependencies] serde = { workspace = true } serde_json = { workspace = true } hex = { workspace = true } datatest-stable = "0.3.3" +leansig.workspace = true +tree_hash = "0.12.0" +ethereum_ssz = "0.10.0" +ethereum_ssz_derive = "0.10.0" +ssz_types = "0.14.0" [[test]] name = "forkchoice_spectests" path = "tests/forkchoice_spectests.rs" harness = false required-features = ["skip-signature-verification"] + +[[test]] +name = "signature_spectests" +path = "tests/signature_spectests.rs" +harness = false diff --git a/crates/blockchain/state_transition/Cargo.toml b/crates/blockchain/state_transition/Cargo.toml index 76f2c24..be1e343 100644 --- a/crates/blockchain/state_transition/Cargo.toml +++ b/crates/blockchain/state_transition/Cargo.toml @@ -22,22 +22,7 @@ hex.workspace = true datatest-stable = "0.3.3" -# For signature tests - uses Store::on_block with actual signature verification -# Note: Do NOT enable skip-signature-verification feature here -ethlambda-blockchain.workspace = true - -leansig.workspace = true -tree_hash = "0.12.0" -ethereum_ssz = "0.10.0" -ethereum_ssz_derive = "0.10.0" -ssz_types = "0.14.0" - [[test]] name = "stf_spectests" path = "tests/stf_spectests.rs" harness = false - -[[test]] -name = "signature_spectests" -path = "tests/signature_spectests.rs" -harness = false diff --git a/crates/blockchain/state_transition/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs similarity index 85% rename from crates/blockchain/state_transition/tests/signature_spectests.rs rename to crates/blockchain/tests/signature_spectests.rs index 14b494b..6f87b4e 100644 --- a/crates/blockchain/state_transition/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -58,6 +58,11 @@ fn run(path: &Path) -> datatest_stable::Result<()> { "[test_proposer_signature] Proposer validator_id: {}", signed_block.message.proposer_attestation.validator_id ); + println!("[test_proposer_signature] AttestationData fields:"); + println!("[test_proposer_signature] slot: {}", proposer_att_data.slot); + println!("[test_proposer_signature] head: root=0x{}, slot={}", hex::encode(proposer_att_data.head.root), proposer_att_data.head.slot); + println!("[test_proposer_signature] target: root=0x{}, slot={}", hex::encode(proposer_att_data.target.root), proposer_att_data.target.slot); + println!("[test_proposer_signature] source: root=0x{}, slot={}", hex::encode(proposer_att_data.source.root), proposer_att_data.source.slot); // Print signature bytes (first 64 bytes) let sig_bytes: &[u8] = signed_block.signature.proposer_signature.as_ref(); @@ -106,6 +111,6 @@ fn run(path: &Path) -> datatest_stable::Result<()> { datatest_stable::harness!({ test = run, - root = "../../../../ethlambda/leanSpec/fixtures/consensus/verify_signatures", + root = "../../../ethlambda/leanSpec/fixtures/consensus/verify_signatures", pattern = r".*\.json" }); diff --git a/crates/blockchain/state_transition/tests/signature_types.rs b/crates/blockchain/tests/signature_types.rs similarity index 97% rename from crates/blockchain/state_transition/tests/signature_types.rs rename to crates/blockchain/tests/signature_types.rs index 7303439..bbb4cb7 100644 --- a/crates/blockchain/state_transition/tests/signature_types.rs +++ b/crates/blockchain/tests/signature_types.rs @@ -402,11 +402,17 @@ impl ProposerSignature { // Encode to SSZ bytes let bytes = ssz_sig.as_ssz_bytes(); + println!("[DEBUG to_xmss_signature] co_path length: {}", self.path.siblings.data.len()); + println!("[DEBUG to_xmss_signature] hashes length: {}", self.hashes.data.len()); + println!("[DEBUG to_xmss_signature] SSZ encoded size before padding: {}", bytes.len()); + // Pad to exactly SignatureSize bytes (3112) let sig_size = 3112; - let mut padded = bytes; + let mut padded = bytes.clone(); padded.resize(sig_size, 0); + println!("[DEBUG to_xmss_signature] After padding: {} bytes", padded.len()); + XmssSignature::new(padded).expect("signature size mismatch") } From dbde51b913c1476ef1e6672f017b4239ac57e2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:44:34 -0300 Subject: [PATCH 04/10] chore: add target to regenerate fixtures --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 7734da0..c046f74 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,9 @@ leanSpec: git clone https://github.com/leanEthereum/leanSpec.git --single-branch cd leanSpec && git checkout $(LEAN_SPEC_COMMIT_HASH) +leanSpec/fixtures: leanSpec + cd leanSpec && uv run fill --fork devnet -o fixtures + # lean-quickstart: # git clone https://github.com/blockblaz/lean-quickstart.git --depth 1 --single-branch From eb6c049626513163314ba7e7afe0c26b31066d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 18 Jan 2026 15:38:17 -0300 Subject: [PATCH 05/10] fix: use --scheme=prod when generating fixtures --- Makefile | 2 +- crates/blockchain/src/store.rs | 4 +++ .../blockchain/tests/signature_spectests.rs | 28 ------------------- crates/blockchain/tests/signature_types.rs | 6 ---- 4 files changed, 5 insertions(+), 35 deletions(-) diff --git a/Makefile b/Makefile index c046f74..0802def 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ leanSpec: cd leanSpec && git checkout $(LEAN_SPEC_COMMIT_HASH) leanSpec/fixtures: leanSpec - cd leanSpec && uv run fill --fork devnet -o fixtures + cd leanSpec && uv run fill --fork devnet --scheme=prod -o fixtures # lean-quickstart: # git clone https://github.com/blockblaz/lean-quickstart.git --depth 1 --single-branch diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index e757484..b4b6438 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -950,9 +950,11 @@ fn verify_signatures( } let proposer_attestation = &signed_block.message.proposer_attestation; + let proposer_signature = ValidatorSignature::from_bytes(&signed_block.signature.proposer_signature) .map_err(|_| StoreError::ProposerSignatureDecodingFailed)?; + let proposer = validators .get(block.proposer_index as usize) .ok_or(StoreError::InvalidValidatorIndex)?; @@ -960,12 +962,14 @@ fn verify_signatures( let proposer_pubkey = proposer .get_pubkey() .map_err(|_| StoreError::PubkeyDecodingFailed(proposer.index))?; + let epoch = proposer_attestation .data .slot .try_into() .expect("slot exceeds u32"); let message = proposer_attestation.data.tree_hash_root(); + if !proposer_pubkey.is_valid(epoch, &message, &proposer_signature) { return Err(StoreError::ProposerSignatureVerificationFailed); } diff --git a/crates/blockchain/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs index 6f87b4e..7cf91b9 100644 --- a/crates/blockchain/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -45,34 +45,6 @@ fn run(path: &Path) -> datatest_stable::Result<()> { // Step 2: Run the state transition function with the block fixture let signed_block: SignedBlockWithAttestation = test.signed_block_with_attestation.into(); - // Debug: print details for specific test - if name.contains("test_proposer_signature[") && !name.contains("attester") { - let proposer_att_data = &signed_block.message.proposer_attestation.data; - let message_hash = proposer_att_data.tree_hash_root(); - println!("[test_proposer_signature] AttestationData tree hash: 0x{}", hex::encode(message_hash)); - println!( - "[test_proposer_signature] Slot (epoch): {}", - signed_block.message.proposer_attestation.data.slot - ); - println!( - "[test_proposer_signature] Proposer validator_id: {}", - signed_block.message.proposer_attestation.validator_id - ); - println!("[test_proposer_signature] AttestationData fields:"); - println!("[test_proposer_signature] slot: {}", proposer_att_data.slot); - println!("[test_proposer_signature] head: root=0x{}, slot={}", hex::encode(proposer_att_data.head.root), proposer_att_data.head.slot); - println!("[test_proposer_signature] target: root=0x{}, slot={}", hex::encode(proposer_att_data.target.root), proposer_att_data.target.slot); - println!("[test_proposer_signature] source: root=0x{}, slot={}", hex::encode(proposer_att_data.source.root), proposer_att_data.source.slot); - - // Print signature bytes (first 64 bytes) - let sig_bytes: &[u8] = signed_block.signature.proposer_signature.as_ref(); - println!( - "[test_proposer_signature] Signature bytes (first 64): {}", - hex::encode(&sig_bytes[..64.min(sig_bytes.len())]) - ); - println!("[test_proposer_signature] Signature total length: {}", sig_bytes.len()); - } - // Advance time to the block's slot let block_time = signed_block.message.block.slot * SECONDS_PER_SLOT + genesis_time; store.on_tick(block_time, true); diff --git a/crates/blockchain/tests/signature_types.rs b/crates/blockchain/tests/signature_types.rs index bbb4cb7..f1f35be 100644 --- a/crates/blockchain/tests/signature_types.rs +++ b/crates/blockchain/tests/signature_types.rs @@ -402,17 +402,11 @@ impl ProposerSignature { // Encode to SSZ bytes let bytes = ssz_sig.as_ssz_bytes(); - println!("[DEBUG to_xmss_signature] co_path length: {}", self.path.siblings.data.len()); - println!("[DEBUG to_xmss_signature] hashes length: {}", self.hashes.data.len()); - println!("[DEBUG to_xmss_signature] SSZ encoded size before padding: {}", bytes.len()); - // Pad to exactly SignatureSize bytes (3112) let sig_size = 3112; let mut padded = bytes.clone(); padded.resize(sig_size, 0); - println!("[DEBUG to_xmss_signature] After padding: {} bytes", padded.len()); - XmssSignature::new(padded).expect("signature size mismatch") } From 8dad05c05463d1d3fe9d9123e9e1987bd19865da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:24:20 -0300 Subject: [PATCH 06/10] chore: cargo fmt --- crates/blockchain/tests/signature_spectests.rs | 2 +- crates/blockchain/tests/signature_types.rs | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/blockchain/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs index 7cf91b9..0318d35 100644 --- a/crates/blockchain/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -1,6 +1,6 @@ use std::path::Path; -use ethlambda_blockchain::{store::Store, SECONDS_PER_SLOT}; +use ethlambda_blockchain::{SECONDS_PER_SLOT, store::Store}; use ethlambda_types::{ block::{Block, SignedBlockWithAttestation}, primitives::TreeHash, diff --git a/crates/blockchain/tests/signature_types.rs b/crates/blockchain/tests/signature_types.rs index f1f35be..5def57a 100644 --- a/crates/blockchain/tests/signature_types.rs +++ b/crates/blockchain/tests/signature_types.rs @@ -1,19 +1,17 @@ use ethlambda_types::attestation::{ - AggregatedAttestation as EthAggregatedAttestation, - AggregationBits as EthAggregationBits, Attestation as EthAttestation, - AttestationData as EthAttestationData, XmssSignature, + AggregatedAttestation as EthAggregatedAttestation, AggregationBits as EthAggregationBits, + Attestation as EthAttestation, AttestationData as EthAttestationData, XmssSignature, }; use ethlambda_types::block::{ - AggregatedAttestations, AttestationSignatures, Block as EthBlock, - BlockBody as EthBlockBody, BlockSignatures, BlockWithAttestation, - NaiveAggregatedSignature, SignedBlockWithAttestation, + AggregatedAttestations, AttestationSignatures, Block as EthBlock, BlockBody as EthBlockBody, + BlockSignatures, BlockWithAttestation, NaiveAggregatedSignature, SignedBlockWithAttestation, }; use ethlambda_types::primitives::{BitList, Encode, H256, VariableList}; use ethlambda_types::state::{Checkpoint as EthCheckpoint, State, ValidatorPubkeyBytes}; use serde::Deserialize; use ssz_derive::{Decode as SszDecode, Encode as SszEncode}; -use ssz_types::typenum::{U28, U32}; use ssz_types::FixedVector; +use ssz_types::typenum::{U28, U32}; use std::collections::HashMap; use std::path::Path; From 9568da80c27cd72ad080030b09db3df7be3056de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:44:25 -0300 Subject: [PATCH 07/10] fix: use prod scheme --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2df54e..9c9c0cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: - name: Generate test fixtures working-directory: ./leanSpec - run: uv run fill --clean --fork=devnet + run: uv run fill --clean --fork=devnet --scheme=prod - name: Setup Rust uses: dtolnay/rust-toolchain@master From 12c6820288617e883e3e0e70c58ab77b22c4ad6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:37:09 -0300 Subject: [PATCH 08/10] ci: download test fixtures instead of generating them --- .github/workflows/ci.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c9c0cf..6580cc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,19 +47,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Checkout leanSpec - uses: actions/checkout@v4 + - name: Download test fixtures + uses: actions/download-artifact@v5 with: repository: leanEthereum/leanSpec - ref: bf0f606a75095cf1853529bc770516b1464d9716 - path: ./leanSpec - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Generate test fixtures - working-directory: ./leanSpec - run: uv run fill --clean --fork=devnet --scheme=prod + name: fixtures-prod-scheme + path: ./leanSpec/fixtures + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Rust uses: dtolnay/rust-toolchain@master From 0d451d809aa2dca7b830ed130bda885b1c35253f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:42:34 -0300 Subject: [PATCH 09/10] ci: use GH CLI --- .github/workflows/ci.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6580cc6..13d385f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,12 +48,11 @@ jobs: - uses: actions/checkout@v4 - name: Download test fixtures - uses: actions/download-artifact@v5 - with: - repository: leanEthereum/leanSpec - name: fixtures-prod-scheme - path: ./leanSpec/fixtures - github-token: ${{ secrets.GITHUB_TOKEN }} + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p leanSpec/fixtures + gh run download --repo leanEthereum/leanSpec --name fixtures-prod-scheme --dir leanSpec/fixtures - name: Setup Rust uses: dtolnay/rust-toolchain@master From eb5f9eff32450b284ed52c5f4dadc1c06788e1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:17:44 -0300 Subject: [PATCH 10/10] refactor: rename private key -> secret key --- bin/ethlambda/src/main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 1043547..f07b574 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -187,22 +187,22 @@ fn read_validator_keys( for validator in validator_vec { let validator_index = validator.index; - // Resolve the private key file path relative to the validators config directory - let privkey_path = if validator.privkey_file.is_absolute() { + // Resolve the secret key file path relative to the validators config directory + let secret_key_path = if validator.privkey_file.is_absolute() { validator.privkey_file.clone() } else { validator_keys_dir.join(&validator.privkey_file) }; - info!(node_id=%node_id, index=validator_index, privkey_file=?privkey_path, "Loading validator private key"); + info!(node_id=%node_id, index=validator_index, secret_key_file=?secret_key_path, "Loading validator secret key"); - // Read the hex-encoded private key file - let privkey_bytes = - std::fs::read(&privkey_path).expect("Failed to read validator secret key file"); + // Read the hex-encoded secret key file + let secret_key_bytes = + std::fs::read(&secret_key_path).expect("Failed to read validator secret key file"); - // Parse the private key - let secret_key = ValidatorSecretKey::from_bytes(&privkey_bytes).unwrap_or_else(|err| { - error!(node_id=%node_id, index=validator_index, privkey_file=?privkey_path, ?err, "Failed to parse validator secret key"); + // Parse the secret key + let secret_key = ValidatorSecretKey::from_bytes(&secret_key_bytes).unwrap_or_else(|err| { + error!(node_id=%node_id, index=validator_index, secret_key_file=?secret_key_path, ?err, "Failed to parse validator secret key"); std::process::exit(1); }); @@ -212,7 +212,7 @@ fn read_validator_keys( info!( node_id = %node_id, count = validator_keys.len(), - "Loaded validator private keys" + "Loaded validator secret keys" ); validator_keys