From 062ba694b59b0845d94bf77c207864cd8be118be Mon Sep 17 00:00:00 2001 From: Julius Mieliauskas Date: Fri, 19 Dec 2025 00:46:02 +0200 Subject: [PATCH 01/24] prepared container types for signature aggregation (devnet 2) --- lean_client/containers/Cargo.toml | 3 + lean_client/containers/src/attestation.rs | 20 ++- lean_client/containers/src/block.rs | 17 ++- lean_client/containers/src/lib.rs | 4 +- lean_client/containers/src/serde_helpers.rs | 132 ++++++++++------- lean_client/containers/src/state.rs | 155 ++++++++++++-------- lean_client/src/main.rs | 6 +- 7 files changed, 211 insertions(+), 126 deletions(-) diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index 011bb4e..b6a45c3 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" [features] xmss-verify = ["leansig"] +default = ["devnet1"] +devnet1 = [] +devnet2 = [] [lib] name = "containers" diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 6ad0f56..6a820c7 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -19,9 +19,9 @@ use typenum::U4096; /// Limit is VALIDATOR_REGISTRY_LIMIT (4096). pub type Attestations = ssz::PersistentList; -/// List of signatures corresponding to attestations in a block. -/// Limit is VALIDATOR_REGISTRY_LIMIT (4096). -pub type BlockSignatures = ssz::PersistentList; +pub type AggregatedAttestations = ssz::PersistentList; + +pub type AttestationSignatures = ssz::PersistentList; /// Bitlist representing validator participation in an attestation. /// Limit is VALIDATOR_REGISTRY_LIMIT (4096). @@ -57,15 +57,19 @@ pub struct Attestation { /// Validator attestation bundled with its signature. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct SignedAttestation { - /// The attestation message signed by the validator. + #[cfg(feature = "devnet2")] + pub validator_id: u64, + #[cfg(feature = "devnet2")] + pub message: AttestationData, + #[cfg(feature = "devnet1")] pub message: Attestation, - /// Signature aggregation produced by the leanVM (SNARKs in the future). + /// signature over attestaion message only as it would be aggregated later in attestation pub signature: Signature, } /// Aggregated attestation consisting of participation bits and message. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] -pub struct AggregatedAttestations { +pub struct AggregatedAttestation { /// Bitfield indicating which validators participated in the aggregation. pub aggregation_bits: AggregationBits, /// Combined attestation data similar to the beacon chain format. @@ -77,9 +81,9 @@ pub struct AggregatedAttestations { /// Aggregated attestation bundled with aggregated signatures. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] -pub struct SignedAggregatedAttestations { +pub struct SignedAggregatedAttestation { /// Aggregated attestation data. - pub message: AggregatedAttestations, + pub message: AggregatedAttestation, /// Aggregated attestation plus its combined signature. /// /// Stores a naive list of validator signatures that mirrors the attestation diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 9c0a1de..55b4727 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -1,9 +1,12 @@ -use crate::{Attestation, Attestations, BlockSignatures, Bytes32, Signature, Slot, State, ValidatorIndex}; +use crate::{Attestation, Attestations, Bytes32, Signature, Slot, State, ValidatorIndex}; use serde::{Deserialize, Serialize}; use ssz_derive::Ssz; #[cfg(feature = "xmss-verify")] use leansig::signature::generalized_xmss::instantiations_poseidon::lifetime_2_to_the_20::target_sum::SIGTargetSumLifetime20W2NoOff; +use ssz::PersistentList; +use typenum::U4096; +use crate::attestation::AttestationSignatures; /// The body of a block, containing payload data. /// @@ -11,6 +14,9 @@ use leansig::signature::generalized_xmss::instantiations_poseidon::lifetime_2_to /// separately in BlockSignatures to match the spec architecture. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct BlockBody { + #[cfg(feature = "devnet2")] + pub attestations: VariableList, + #[cfg(feature = "devnet1")] #[serde(with = "crate::serde_helpers")] pub attestations: Attestations, } @@ -45,6 +51,12 @@ pub struct BlockWithAttestation { pub proposer_attestation: Attestation, } +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)] +pub struct BlockSignatures { + pub attestation_signatures: AttestationSignatures, + pub proposer_signature: Signature, +} + /// Envelope carrying a block, an attestation from proposer, and aggregated signatures. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -54,7 +66,10 @@ pub struct SignedBlockWithAttestation { /// Aggregated signature payload for the block. /// /// Signatures remain in attestation order followed by the proposer signature. + #[cfg(feature = "devnet1")] #[serde(with = "crate::serde_helpers::block_signatures")] + pub signature: PersistentList, + #[cfg(feature = "devnet2")] pub signature: BlockSignatures, } diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index 511db23..c73a9f9 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -10,8 +10,8 @@ pub mod types; pub mod validator; pub use attestation::{ - AggregatedAttestations, AggregatedSignatures, AggregationBits, Attestation, AttestationData, - Attestations, BlockSignatures, Signature, SignedAggregatedAttestations, SignedAttestation, + AggregatedAttestation, AggregatedSignatures, AggregationBits, Attestation, AttestationData, + Attestations, Signature, SignedAggregatedAttestation, SignedAttestation, }; pub use block::{ Block, BlockBody, BlockHeader, BlockWithAttestation, SignedBlock, SignedBlockWithAttestation, diff --git a/lean_client/containers/src/serde_helpers.rs b/lean_client/containers/src/serde_helpers.rs index aff4d60..0568f71 100644 --- a/lean_client/containers/src/serde_helpers.rs +++ b/lean_client/containers/src/serde_helpers.rs @@ -34,26 +34,26 @@ where pub mod bitlist { use super::*; use ssz::BitList; - use typenum::Unsigned; use ssz::SszRead; - + use typenum::Unsigned; + #[derive(Deserialize)] #[serde(untagged)] enum BitListData { HexString(String), BoolArray(Vec), } - + pub fn deserialize<'de, D, N>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, N: Unsigned, { use serde::de::Error; - + // First unwrap the {"data": ...} wrapper let wrapper = DataWrapper::::deserialize(deserializer)?; - + match wrapper.data { BitListData::HexString(hex_str) => { // Handle hex string format (e.g., "0x01ff") @@ -62,10 +62,10 @@ pub mod bitlist { // Empty hex string means empty bitlist return Ok(BitList::default()); } - + let bytes = hex::decode(hex_str) .map_err(|e| D::Error::custom(format!("Invalid hex string: {}", e)))?; - + // Decode SSZ bitlist (with delimiter bit) BitList::from_ssz_unchecked(&(), &bytes) .map_err(|e| D::Error::custom(format!("Invalid SSZ bitlist: {:?}", e))) @@ -80,19 +80,20 @@ pub mod bitlist { } } } - + pub fn serialize(value: &BitList, serializer: S) -> Result where S: Serializer, N: Unsigned, { use ssz::SszWrite; - + // Serialize as hex string in {"data": "0x..."} format let mut bytes = Vec::new(); - value.write_variable(&mut bytes) + value + .write_variable(&mut bytes) .map_err(|e| serde::ser::Error::custom(format!("Failed to write SSZ: {:?}", e)))?; - + let hex_str = format!("0x{}", hex::encode(&bytes)); let wrapper = DataWrapper { data: hex_str }; wrapper.serialize(serializer) @@ -103,9 +104,9 @@ pub mod bitlist { /// Signatures in test vectors are structured with {path, rho, hashes} instead of hex bytes pub mod signature { use super::*; - use serde_json::Value; use crate::Signature; - + use serde_json::Value; + /// Structured XMSS signature format from test vectors #[derive(Deserialize)] struct XmssSignature { @@ -113,65 +114,65 @@ pub mod signature { rho: DataWrapper>, hashes: DataWrapper>>>, } - + #[derive(Deserialize)] struct XmssPath { siblings: DataWrapper>>>, } - + pub fn deserialize_single<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::Error; - + // First, try to parse as a JSON value to inspect the structure let value = Value::deserialize(deserializer)?; - + // Check if it's a hex string (normal format) if let Value::String(hex_str) = &value { let hex_str = hex_str.trim_start_matches("0x"); let bytes = hex::decode(hex_str) .map_err(|e| D::Error::custom(format!("Invalid hex string: {}", e)))?; - + return Signature::try_from(bytes.as_slice()) .map_err(|_| D::Error::custom("Invalid signature length")); } - + // Otherwise, parse as structured XMSS signature let xmss_sig: XmssSignature = serde_json::from_value(value) .map_err(|e| D::Error::custom(format!("Failed to parse XMSS signature: {}", e)))?; - + // Serialize the XMSS signature to bytes // Format: siblings (variable length) + rho (28 bytes) + hashes (variable length) let mut bytes = Vec::new(); - + // Write siblings for sibling in &xmss_sig.path.siblings.data { for val in &sibling.data { bytes.extend_from_slice(&val.to_le_bytes()); } } - + // Write rho (7 u32s = 28 bytes) for val in &xmss_sig.rho.data { bytes.extend_from_slice(&val.to_le_bytes()); } - + // Write hashes for hash in &xmss_sig.hashes.data { for val in &hash.data { bytes.extend_from_slice(&val.to_le_bytes()); } } - + // Pad or truncate to 3112 bytes bytes.resize(3112, 0); - + Signature::try_from(bytes.as_slice()) .map_err(|_| D::Error::custom("Failed to create signature")) } - + pub fn serialize(value: &Signature, serializer: S) -> Result where S: Serializer, @@ -186,10 +187,11 @@ pub mod signature { /// where each signature can be either hex string or structured XMSS format pub mod block_signatures { use super::*; - use crate::{Signature, BlockSignatures}; - use ssz::PersistentList; + use crate::Signature; use serde_json::Value; - + use ssz::PersistentList; + use typenum::U4096; + /// Structured XMSS signature format from test vectors #[derive(Deserialize, Clone)] struct XmssSignature { @@ -197,79 +199,95 @@ pub mod block_signatures { rho: DataWrapper>, hashes: DataWrapper>>>, } - + #[derive(Deserialize, Clone)] struct XmssPath { siblings: DataWrapper>>>, } - + fn parse_single_signature(value: &Value) -> Result { // Check if it's a hex string (normal format) if let Value::String(hex_str) = value { let hex_str = hex_str.trim_start_matches("0x"); - let bytes = hex::decode(hex_str) - .map_err(|e| format!("Invalid hex string: {}", e))?; - + let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid hex string: {}", e))?; + return Signature::try_from(bytes.as_slice()) .map_err(|_| "Invalid signature length".to_string()); } - + // Otherwise, parse as structured XMSS signature let xmss_sig: XmssSignature = serde_json::from_value(value.clone()) .map_err(|e| format!("Failed to parse XMSS signature: {}", e))?; - + // Serialize the XMSS signature to bytes // Format: siblings (variable length) + rho (28 bytes) + hashes (variable length) let mut bytes = Vec::new(); - + // Write siblings for sibling in &xmss_sig.path.siblings.data { for val in &sibling.data { bytes.extend_from_slice(&val.to_le_bytes()); } } - + // Write rho (7 u32s = 28 bytes) for val in &xmss_sig.rho.data { bytes.extend_from_slice(&val.to_le_bytes()); } - + // Write hashes for hash in &xmss_sig.hashes.data { for val in &hash.data { bytes.extend_from_slice(&val.to_le_bytes()); } } - + // Pad or truncate to 3112 bytes bytes.resize(3112, 0); - - Signature::try_from(bytes.as_slice()) - .map_err(|_| "Failed to create signature".to_string()) + + Signature::try_from(bytes.as_slice()).map_err(|_| "Failed to create signature".to_string()) } - pub fn deserialize<'de, D>(deserializer: D) -> Result + #[cfg(feature = "devnet1")] + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result, D::Error> where D: Deserializer<'de>, { use serde::de::Error; - + // Parse the {"data": [...]} wrapper let wrapper: DataWrapper> = DataWrapper::deserialize(deserializer)?; - + let mut signatures = PersistentList::default(); - + for (idx, sig_value) in wrapper.data.into_iter().enumerate() { let sig = parse_single_signature(&sig_value) .map_err(|e| D::Error::custom(format!("Signature {}: {}", idx, e)))?; - signatures.push(sig) + signatures + .push(sig) .map_err(|e| D::Error::custom(format!("Signature {} push failed: {:?}", idx, e)))?; } - + Ok(signatures) } - - pub fn serialize(value: &BlockSignatures, serializer: S) -> Result + + #[cfg(feature = "devnet2")] + pub fn deserialize<'de, D>(_: D) -> Result + where + D: Deserializer<'de>, + { + Err(serde::de::Error::custom( + "BlockSignatures deserialization not implemented for devnet2", + )) + } + + #[cfg(feature = "devnet1")] + pub fn serialize( + value: &PersistentList, + serializer: S, + ) -> Result where S: Serializer, { @@ -285,8 +303,18 @@ pub mod block_signatures { Err(_) => break, } } - + let wrapper = DataWrapper { data: sigs }; wrapper.serialize(serializer) } + + #[cfg(feature = "devnet2")] + pub fn serialize(value: &BlockSignatures, serializer: S) -> Result + where + S: Serializer, + { + Err(serde::de::Error::custom( + "BlockSignatures serialization not implemented for devnet2", + )) + } } diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 4eb0ffd..ac84e06 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -1,13 +1,13 @@ use crate::validator::Validator; +use crate::{block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, Attestation, Attestations, Bytes32, Checkpoint, Config, Signature, Slot, Uint64, ValidatorIndex}; use crate::{ - block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, - Attestation, Attestations, BlockSignatures, Bytes32, Checkpoint, Config, Slot, Uint64, ValidatorIndex, + HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, Validators, }; -use crate::{HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, Validators}; use serde::{Deserialize, Serialize}; -use ssz::{PersistentList as List}; +use ssz::{PersistentList as List, PersistentList}; use ssz_derive::Ssz; use std::collections::BTreeMap; +use typenum::U4096; pub const VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const JUSTIFICATION_ROOTS_LIMIT: usize = 1 << 18; // 262144 @@ -47,7 +47,10 @@ pub struct State { } impl State { - pub fn generate_genesis_with_validators(genesis_time: Uint64, validators: Vec) -> Self { + pub fn generate_genesis_with_validators( + genesis_time: Uint64, + validators: Vec, + ) -> Self { let body_for_root = BlockBody { attestations: Default::default(), }; @@ -64,7 +67,6 @@ impl State { validator_list.push(v).expect("Failed to add validator"); } - Self { config: Config { genesis_time: genesis_time.0, @@ -206,7 +208,11 @@ impl State { for (i, r) in roots.iter().enumerate() { let v = map.get(r).expect("root present"); - assert_eq!(v.len(), num_validators, "vote vector must match validator count"); + assert_eq!( + v.len(), + num_validators, + "vote vector must match validator count" + ); let base = i * num_validators; for (j, &bit) in v.iter().enumerate() { if bit { @@ -230,7 +236,11 @@ impl State { } // updated for fork choice tests - pub fn state_transition(&self, signed_block: SignedBlockWithAttestation, valid_signatures: bool) -> Result { + pub fn state_transition( + &self, + signed_block: SignedBlockWithAttestation, + valid_signatures: bool, + ) -> Result { self.state_transition_with_validation(signed_block, valid_signatures, true) } @@ -314,7 +324,7 @@ impl State { } // Create a mutable clone for hash computation - let latest_header_for_hash = self.latest_block_header.clone(); + let latest_header_for_hash = self.latest_block_header.clone(); let parent_root = hash_tree_root(&latest_header_for_hash); if block.parent_root != parent_root { return Err(String::from("Block parent root mismatch")); @@ -554,6 +564,7 @@ impl State { /// # Returns /// /// Tuple of (Block, post-State, collected attestations, signatures) + #[cfg(feature = "devnet1")] pub fn build_block( &self, slot: Slot, @@ -562,10 +573,10 @@ impl State { initial_attestations: Option>, available_signed_attestations: Option<&[SignedBlockWithAttestation]>, known_block_roots: Option<&std::collections::HashSet>, - ) -> Result<(Block, Self, Vec, BlockSignatures), String> { + ) -> Result<(Block, Self, Vec, PersistentList), String> { // Initialize empty attestation set for iterative collection let mut attestations = initial_attestations.unwrap_or_default(); - let mut signatures = BlockSignatures::default(); + let mut signatures = PersistentList::default(); // Advance state to target slot // Note: parent_root comes from fork choice and is already validated. @@ -581,7 +592,9 @@ impl State { // Create candidate block with current attestation set let mut attestations_list = Attestations::default(); for att in &attestations { - attestations_list.push(att.clone()).map_err(|e| format!("Failed to push attestation: {:?}", e))?; + attestations_list + .push(att.clone()) + .map_err(|e| format!("Failed to push attestation: {:?}", e))?; } let candidate_block = Block { @@ -666,10 +679,25 @@ impl State { // Add new attestations and continue iteration attestations.extend(new_attestations); for sig in new_signatures { - signatures.push(sig).map_err(|e| format!("Failed to push signature: {:?}", e))?; + signatures + .push(sig) + .map_err(|e| format!("Failed to push signature: {:?}", e))?; } } } + + #[cfg(feature = "devnet2")] + pub fn build_block( + &self, + _slot: Slot, + _proposer_index: ValidatorIndex, + _parent_root: Bytes32, + _initial_attestations: Option>, + _available_signed_attestations: Option<&[SignedBlockWithAttestation]>, + _known_block_roots: Option<&std::collections::HashSet>, + ) -> Result<(Block, Self, Vec, BlockSignatures), String> { + Err("build_block is not implemented for devnet2".to_string()) + } } #[cfg(test)] @@ -726,14 +754,15 @@ mod tests { } #[test] + #[cfg(feature = "devnet1")] fn test_build_block() { // Create genesis state with validators let genesis_state = State::generate_genesis(Uint64(0), Uint64(4)); - + // Compute expected parent root after slot processing let pre_state = genesis_state.process_slots(Slot(1)).unwrap(); let expected_parent_root = hash_tree_root(&pre_state.latest_block_header); - + // Test 1: Build a simple block without attestations let result = genesis_state.build_block( Slot(1), @@ -743,27 +772,34 @@ mod tests { None, None, ); - + assert!(result.is_ok(), "Building simple block should succeed"); let (block, post_state, attestations, signatures) = result.unwrap(); - + // Verify block properties assert_eq!(block.slot, Slot(1)); assert_eq!(block.proposer_index, ValidatorIndex(1)); assert_eq!(block.parent_root, expected_parent_root); - assert_ne!(block.state_root, Bytes32(ssz::H256::zero()), "State root should be computed"); - + assert_ne!( + block.state_root, + Bytes32(ssz::H256::zero()), + "State root should be computed" + ); + // Verify attestations and signatures are empty assert_eq!(attestations.len(), 0); // Check signatures by trying to get first element assert!(signatures.get(0).is_err(), "Signatures should be empty"); - + // Verify post-state has advanced assert_eq!(post_state.slot, Slot(1)); // Note: The post-state's latest_block_header.state_root is zero because it will be // filled in during the next slot processing - assert_eq!(block.parent_root, expected_parent_root, "Parent root should match"); - + assert_eq!( + block.parent_root, expected_parent_root, + "Parent root should match" + ); + // Test 2: Build block with initial attestations let attestation = Attestation { validator_id: Uint64(0), @@ -783,7 +819,7 @@ mod tests { }, }, }; - + let result = genesis_state.build_block( Slot(1), ValidatorIndex(1), @@ -792,45 +828,48 @@ mod tests { None, None, ); - - assert!(result.is_ok(), "Building block with attestations should succeed"); + + assert!( + result.is_ok(), + "Building block with attestations should succeed" + ); let (block, _post_state, attestations, _signatures) = result.unwrap(); - + // Verify attestation was included assert_eq!(attestations.len(), 1); assert_eq!(attestations[0].validator_id, Uint64(0)); // Check that attestation list has one element - assert!(block.body.attestations.get(0).is_ok(), "Block should contain attestation"); - assert!(block.body.attestations.get(1).is_err(), "Block should have only one attestation"); + assert!( + block.body.attestations.get(0).is_ok(), + "Block should contain attestation" + ); + assert!( + block.body.attestations.get(1).is_err(), + "Block should have only one attestation" + ); } #[test] fn test_build_block_advances_state() { // Create genesis state let genesis_state = State::generate_genesis(Uint64(0), Uint64(10)); - + // Compute parent root after advancing to target slot let pre_state = genesis_state.process_slots(Slot(5)).unwrap(); let parent_root = hash_tree_root(&pre_state.latest_block_header); - + // Build block at slot 5 // Proposer for slot 5 with 10 validators is (5 % 10) = 5 - let result = genesis_state.build_block( - Slot(5), - ValidatorIndex(5), - parent_root, - None, - None, - None, - ); - + let result = + genesis_state.build_block(Slot(5), ValidatorIndex(5), parent_root, None, None, None); + assert!(result.is_ok()); let (block, post_state, _, _) = result.unwrap(); - + // Verify state advanced through slots assert_eq!(post_state.slot, Slot(5)); assert_eq!(block.slot, Slot(5)); - + // Verify block can be applied to genesis state let transition_result = genesis_state.state_transition_with_validation( SignedBlockWithAttestation { @@ -838,49 +877,45 @@ mod tests { block: block.clone(), proposer_attestation: Attestation::default(), }, - signature: BlockSignatures::default(), + signature: PersistentList::default(), }, true, // signatures are considered valid (not validating, just marking as valid) true, ); - - assert!(transition_result.is_ok(), "Built block should be valid for state transition"); + + assert!( + transition_result.is_ok(), + "Built block should be valid for state transition" + ); } #[test] fn test_build_block_state_root_matches() { // Create genesis state let genesis_state = State::generate_genesis(Uint64(0), Uint64(3)); - + // Compute parent root after advancing to target slot let pre_state = genesis_state.process_slots(Slot(1)).unwrap(); let parent_root = hash_tree_root(&pre_state.latest_block_header); - + // Build a block // Proposer for slot 1 with 3 validators is (1 % 3) = 1 - let result = genesis_state.build_block( - Slot(1), - ValidatorIndex(1), - parent_root, - None, - None, - None, - ); - + let result = + genesis_state.build_block(Slot(1), ValidatorIndex(1), parent_root, None, None, None); + assert!(result.is_ok()); let (block, post_state, _, _) = result.unwrap(); - + // Verify the state root in block matches the computed post-state let computed_state_root = hash_tree_root(&post_state); assert_eq!( - block.state_root, - computed_state_root, + block.state_root, computed_state_root, "Block state root should match computed post-state root" ); - + // Verify it's not zero assert_ne!( - block.state_root, + block.state_root, Bytes32(ssz::H256::zero()), "State root should not be zero" ); diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index 396c8f7..cc44285 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -1,7 +1,7 @@ use clap::Parser; -use containers::ssz::SszHash; +use containers::ssz::{PersistentList, SszHash}; use containers::{ - attestation::{Attestation, AttestationData, BlockSignatures}, + attestation::{Attestation, AttestationData}, block::{Block, BlockBody, BlockWithAttestation, SignedBlockWithAttestation}, checkpoint::Checkpoint, config::Config, @@ -216,7 +216,7 @@ async fn main() { block: genesis_block, proposer_attestation: genesis_proposer_attestation, }, - signature: BlockSignatures::default(), + signature: PersistentList::default(), }; let config = Config { genesis_time }; From 4c53527287b43e7c9f82e99f15faed1d1022428b Mon Sep 17 00:00:00 2001 From: Julius Mieliauskas Date: Fri, 19 Dec 2025 11:11:48 +0200 Subject: [PATCH 02/24] minor cleanups --- lean_client/containers/src/block.rs | 19 +--------- lean_client/containers/src/state.rs | 26 +------------ .../containers/tests/test_vectors/runner.rs | 38 ++----------------- .../containers/tests/unit_tests/common.rs | 8 ++-- .../tests/unit_tests/state_transition.rs | 6 ++- .../tests/fork_choice_test_vectors.rs | 8 ++-- 6 files changed, 19 insertions(+), 86 deletions(-) diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 55b4727..ac5063f 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -170,25 +170,10 @@ impl SignedBlockWithAttestation { // The ordering must be preserved: // 1. Block body attestations, // 2. The proposer attestation. - assert!( - signatures_vec.len() == all_attestations.len(), - "Number of signatures does not match number of attestations" - ); + assert_eq!(signatures_vec.len(), all_attestations.len(), "Number of signatures does not match number of attestations"); let validators = &parent_state.validators; - - // Count validators (PersistentList doesn't expose len directly) - let mut num_validators: u64 = 0; - let mut k: u64 = 0; - loop { - match validators.get(k) { - Ok(_) => { - num_validators += 1; - k += 1; - } - Err(_) => break, - } - } + let num_validators = validators.len_u64(); // Verify each attestation signature for (attestation, signature) in all_attestations.iter().zip(signatures_vec.iter()) { diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index ac84e06..46641e1 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -135,18 +135,7 @@ impl State { /// Simple RR proposer rule (round-robin). pub fn is_proposer(&self, index: ValidatorIndex) -> bool { - // Count validators by iterating (since PersistentList doesn't have len()) - let mut num_validators: u64 = 0; - let mut i: u64 = 0; - loop { - match self.validators.get(i) { - Ok(_) => { - num_validators += 1; - i += 1; - } - Err(_) => break, - } - } + let num_validators = self.validators.len_u64(); if num_validators == 0 { return false; // No validators @@ -486,18 +475,7 @@ impl State { if validator_id < votes.len() && !votes[validator_id] { votes[validator_id] = true; - // Count validators - let mut num_validators: u64 = 0; - let mut i: u64 = 0; - loop { - match self.validators.get(i) { - Ok(_) => { - num_validators += 1; - i += 1; - } - Err(_) => break, - } - } + let num_validators = self.validators.len_u64(); let count = votes.iter().filter(|&&v| v).count(); if 3 * count >= 2 * num_validators as usize { diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index 9e7ef36..910fde5 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -83,18 +83,7 @@ impl TestRunner { // Only check validator count if specified in post-state if let Some(expected_count) = post.validator_count { - // Count validators - let mut num_validators: u64 = 0; - let mut i: u64 = 0; - loop { - match state.validators.get(i) { - Ok(_) => { - num_validators += 1; - i += 1; - } - Err(_) => break, - } - } + let num_validators = state.validators.len_u64(); if num_validators as usize != expected_count { return Err(format!( @@ -436,18 +425,7 @@ impl TestRunner { let state = &test_case.pre; - // Count validators - let mut num_validators: u64 = 0; - let mut i: u64 = 0; - loop { - match state.validators.get(i) { - Ok(_) => { - num_validators += 1; - i += 1; - } - Err(_) => break, - } - } + let num_validators = state.validators.len_u64(); println!(" Genesis time: {}, slot: {}, validators: {}", state.config.genesis_time, state.slot.0, num_validators); // Verify it's at genesis (slot 0) @@ -555,17 +533,7 @@ impl TestRunner { // Verify validator count if specified if let Some(expected_count) = post.validator_count { - let mut num_validators: u64 = 0; - let mut i: u64 = 0; - loop { - match state.validators.get(i) { - Ok(_) => { - num_validators += 1; - i += 1; - } - Err(_) => break, - } - } + let num_validators = state.validators.len_u64(); if num_validators as usize != expected_count { return Err(format!( diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 77c2dd5..781c43e 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -1,7 +1,7 @@ use containers::{ - Attestation, Attestations, BlockSignatures, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators + Attestation, Attestations, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators }; -use ssz::PersistentList as List; +use ssz::{PersistentList}; pub const DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const TEST_VALIDATOR_COUNT: usize = 4; // Actual validator count used in tests @@ -12,7 +12,7 @@ const _: [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT] = pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Option) -> SignedBlockWithAttestation { let body = BlockBody { - attestations: attestations.unwrap_or_else(List::default), + attestations: attestations.unwrap_or_else(PersistentList::default), }; let block_message = Block { @@ -28,7 +28,7 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op block: block_message, proposer_attestation: Attestation::default(), }, - signature: BlockSignatures::default(), + signature: PersistentList::default(), } } diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index 91edfa7..aca04cd 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -3,10 +3,11 @@ use containers::{ block::{Block, SignedBlockWithAttestation, BlockWithAttestation, hash_tree_root}, state::State, types::{Bytes32, Uint64}, - Slot, Attestation, BlockSignatures + Slot, Attestation }; use pretty_assertions::assert_eq; use rstest::fixture; +use ssz::PersistentList; #[path = "common.rs"] mod common; @@ -78,6 +79,7 @@ fn test_state_transition_invalid_signatures() { assert_eq!(result.unwrap_err(), "Block signatures must be valid"); } +#[cfg(feature = "devnet1")] #[test] fn test_state_transition_bad_state_root() { let state = genesis_state(); @@ -93,7 +95,7 @@ fn test_state_transition_bad_state_root() { block, proposer_attestation: Attestation::default(), }, - signature: BlockSignatures::default(), + signature: PersistentList::default(), }; let result = state.state_transition(final_signed_block_with_attestation, true); diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index e2d230a..5718d48 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -4,7 +4,7 @@ use fork_choice::{ }; use containers::{ - attestation::{Attestation, AttestationData, BlockSignatures, SignedAttestation, Signature}, + attestation::{Attestation, AttestationData, SignedAttestation, Signature}, block::{hash_tree_root, Block, BlockBody, BlockHeader, BlockWithAttestation, SignedBlockWithAttestation}, checkpoint::Checkpoint, config::Config, @@ -13,7 +13,7 @@ use containers::{ }; use serde::Deserialize; -use ssz::SszHash; +use ssz::{PersistentList, SszHash}; use std::collections::HashMap; use std::panic::AssertUnwindSafe; @@ -299,7 +299,7 @@ fn convert_test_anchor_block(test_block: &TestAnchorBlock) -> SignedBlockWithAtt block, proposer_attestation, }, - signature: BlockSignatures::default(), + signature: PersistentList::default(), } } @@ -329,7 +329,7 @@ fn convert_test_block(test_block_with_att: &TestBlockWithAttestation) -> SignedB block, proposer_attestation, }, - signature: BlockSignatures::default(), + signature: PersistentList::default(), } } From f1f955a9c17852b9cf978a6396b1deff9be2a4c7 Mon Sep 17 00:00:00 2001 From: Julius Mieliauskas Date: Tue, 23 Dec 2025 16:27:26 +0200 Subject: [PATCH 03/24] added tests, fixed some types --- lean_client/containers/src/attestation.rs | 106 ++++++++- lean_client/containers/src/block.rs | 211 +++++++++++++----- lean_client/containers/src/serde_helpers.rs | 5 +- lean_client/containers/src/state.rs | 19 +- .../unit_tests/attestation_aggregation.rs | 132 +++++++++++ .../containers/tests/unit_tests/common.rs | 55 ++++- .../containers/tests/unit_tests/mod.rs | 1 + .../tests/unit_tests/state_process.rs | 1 + .../tests/unit_tests/state_transition.rs | 101 ++++++++- 9 files changed, 549 insertions(+), 82 deletions(-) create mode 100644 lean_client/containers/tests/unit_tests/attestation_aggregation.rs diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 6a820c7..302ef08 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -1,8 +1,9 @@ use crate::{Checkpoint, Slot, Uint64}; use serde::{Deserialize, Serialize}; +use ssz::BitList; use ssz::ByteVector; use ssz_derive::Ssz; -use typenum::{Prod, Sum, U100, U31, U12}; +use typenum::{Prod, Sum, U100, U12, U31}; pub type U3100 = Prod; @@ -21,11 +22,64 @@ pub type Attestations = ssz::PersistentList; pub type AggregatedAttestations = ssz::PersistentList; +#[cfg(feature = "devnet1")] pub type AttestationSignatures = ssz::PersistentList; +#[cfg(feature = "devnet2")] +pub type AttestationSignatures = ssz::PersistentList; + +#[cfg(feature = "devnet2")] +pub type NaiveAggregatedSignature = ssz::PersistentList; + /// Bitlist representing validator participation in an attestation. /// Limit is VALIDATOR_REGISTRY_LIMIT (4096). -pub type AggregationBits = ssz::BitList; +#[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] +pub struct AggregationBits(pub BitList); + +impl AggregationBits { + pub const LIMIT: u64 = 4096; + + pub fn from_validator_indices(indices: &[u64]) -> Self { + assert!( + !indices.is_empty(), + "Aggregated attestation must reference at least one validator" + ); + + let max_id = *indices.iter().max().unwrap(); + assert!( + max_id < Self::LIMIT, + "Validator index out of range for aggregation bits" + ); + + let mut bits = BitList::::with_length((max_id + 1) as usize); + + for i in 0..=max_id { + bits.set(i as usize, false); + } + + for &i in indices { + bits.set(i as usize, true); + } + + AggregationBits(bits) + } + + pub fn to_validator_indices(&self) -> Vec { + let indices: Vec = self + .0 + .iter() + .enumerate() + .filter_map(|(i, bit)| if *bit { Some(i as u64) } else { None }) + .collect(); + + assert!( + !indices.is_empty(), + "Aggregated attestation must reference at least one validator" + ); + + indices + } +} /// Naive list of validator signatures used for aggregation placeholders. /// Limit is VALIDATOR_REGISTRY_LIMIT (4096). @@ -57,13 +111,8 @@ pub struct Attestation { /// Validator attestation bundled with its signature. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct SignedAttestation { - #[cfg(feature = "devnet2")] - pub validator_id: u64, - #[cfg(feature = "devnet2")] - pub message: AttestationData, - #[cfg(feature = "devnet1")] pub message: Attestation, - /// signature over attestaion message only as it would be aggregated later in attestation + /// Signature aggregation produced by the leanVM (SNARKs in the future). pub signature: Signature, } @@ -73,12 +122,51 @@ pub struct AggregatedAttestation { /// Bitfield indicating which validators participated in the aggregation. pub aggregation_bits: AggregationBits, /// Combined attestation data similar to the beacon chain format. - /// + /// /// Multiple validator attestations are aggregated here without the complexity of /// committee assignments. pub data: AttestationData, } +impl AggregatedAttestation { + pub fn aggregate_by_data(attestations: &[Attestation]) -> Vec { + let mut groups: Vec<(AttestationData, Vec)> = Vec::new(); + + for attestation in attestations { + // Try to find an existing group with the same data + if let Some((_, validator_ids)) = groups + .iter_mut() + .find(|(data, _)| *data == attestation.data) + { + validator_ids.push(attestation.validator_id.0); + } else { + // Create a new group + groups.push((attestation.data.clone(), vec![attestation.validator_id.0])); + } + } + + groups + .into_iter() + .map(|(data, validator_ids)| AggregatedAttestation { + aggregation_bits: AggregationBits::from_validator_indices(&validator_ids), + data, + }) + .collect() + } + + pub fn to_plain(&self) -> Vec { + let validator_indices = self.aggregation_bits.to_validator_indices(); + + validator_indices + .into_iter() + .map(|validator_id| Attestation { + validator_id: Uint64(validator_id), + data: self.data.clone(), + }) + .collect() + } +} + /// Aggregated attestation bundled with aggregated signatures. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct SignedAggregatedAttestation { diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index ac5063f..0acf1b2 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -4,9 +4,10 @@ use ssz_derive::Ssz; #[cfg(feature = "xmss-verify")] use leansig::signature::generalized_xmss::instantiations_poseidon::lifetime_2_to_the_20::target_sum::SIGTargetSumLifetime20W2NoOff; -use ssz::PersistentList; +use ssz::{PersistentList, SszHash}; use typenum::U4096; -use crate::attestation::AttestationSignatures; +use crate::attestation::{AggregatedAttestations, AttestationSignatures}; +use crate::validator::BlsPublicKey; /// The body of a block, containing payload data. /// @@ -15,7 +16,7 @@ use crate::attestation::AttestationSignatures; #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct BlockBody { #[cfg(feature = "devnet2")] - pub attestations: VariableList, + pub attestations: AggregatedAttestations, #[cfg(feature = "devnet1")] #[serde(with = "crate::serde_helpers")] pub attestations: Attestations, @@ -51,7 +52,7 @@ pub struct BlockWithAttestation { pub proposer_attestation: Attestation, } -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Ssz, Deserialize, Default)] pub struct BlockSignatures { pub attestation_signatures: AttestationSignatures, pub proposer_signature: Signature, @@ -127,6 +128,7 @@ impl SignedBlockWithAttestation { /// /// - Spec: /// - XMSS Library: + #[cfg(feature = "devnet1")] pub fn verify_signatures(&self, parent_state: State) -> bool { // Unpack the signed block components let block = &self.message.block; @@ -138,7 +140,7 @@ impl SignedBlockWithAttestation { // 1. Block body attestations (from other validators) // 2. Proposer attestation (from the block producer) let mut all_attestations: Vec = Vec::new(); - + // Collect block body attestations let mut i: u64 = 0; loop { @@ -148,7 +150,7 @@ impl SignedBlockWithAttestation { } i += 1; } - + // Append proposer attestation all_attestations.push(self.message.proposer_attestation.clone()); @@ -170,7 +172,11 @@ impl SignedBlockWithAttestation { // The ordering must be preserved: // 1. Block body attestations, // 2. The proposer attestation. - assert_eq!(signatures_vec.len(), all_attestations.len(), "Number of signatures does not match number of attestations"); + assert_eq!( + signatures_vec.len(), + all_attestations.len(), + "Number of signatures does not match number of attestations" + ); let validators = &parent_state.validators; let num_validators = validators.len_u64(); @@ -193,60 +199,149 @@ impl SignedBlockWithAttestation { // - The validator possesses the secret key for their public key // - The attestation has not been tampered with // - The signature was created at the correct epoch (slot) - - #[cfg(feature = "xmss-verify")] - { - use leansig::signature::SignatureScheme; - use leansig::serialization::Serializable; - - // Compute the message hash from the attestation - let message_bytes: [u8; 32] = hash_tree_root(attestation).0.into(); - let epoch = attestation.data.slot.0 as u32; - - // Get public key bytes - use as_bytes() method - let pubkey_bytes = validator.pubkey.0.as_bytes(); - - // Deserialize the public key using Serializable trait - type PubKey = ::PublicKey; - let pubkey = match PubKey::from_bytes(pubkey_bytes) { - Ok(pk) => pk, - Err(e) => { - eprintln!("Failed to deserialize public key at slot {:?}: {:?}", attestation.data.slot, e); - return false; - } - }; - - // Get signature bytes - use as_bytes() method - let sig_bytes = signature.as_bytes(); - - // Deserialize the signature using Serializable trait - type Sig = ::Signature; - let sig = match Sig::from_bytes(sig_bytes) { - Ok(s) => s, - Err(e) => { - eprintln!("Failed to deserialize signature at slot {:?}: {:?}", attestation.data.slot, e); - return false; - } - }; - - // Verify the signature - if !SIGTargetSumLifetime20W2NoOff::verify(&pubkey, epoch, &message_bytes, &sig) { - eprintln!("XMSS signature verification failed at slot {:?}", attestation.data.slot); - return false; - } - } - - #[cfg(not(feature = "xmss-verify"))] + + let message_bytes: [u8; 32] = hash_tree_root(attestation).0.into(); + + assert!( + verify_xmss_signature( + validator.pubkey.0.as_bytes(), + attestation.data.slot, + &message_bytes, + &signature, + ), + "Attestation signature verification failed" + ); + } + + true + } + + #[cfg(feature = "devnet2")] + pub fn verify_signatures(&self, parent_state: State) -> bool { + // Unpack the signed block components + let block = &self.message.block; + let signatures = &self.signature; + let aggregated_attestations = block.body.attestations.clone(); + let attestation_signatures = signatures.attestation_signatures.clone(); + + // Verify signature count matches aggregated attestation count + assert_eq!( + aggregated_attestations.len_u64(), + attestation_signatures.len_u64(), + "Number of signatures does not match number of attestations" + ); + + let validators = &parent_state.validators; + let num_validators = validators.len_u64(); + + // Verify each attestation signature + for (aggregated_attestation, aggregated_signature) in (&aggregated_attestations) + .into_iter() + .zip((&attestation_signatures).into_iter()) + { + let validator_ids = aggregated_attestation + .aggregation_bits + .to_validator_indices(); + + assert_eq!( + aggregated_signature.len_u64(), + validator_ids.len() as u64, + "Aggregated attestation signature count mismatch" + ); + + let attestation_root = aggregated_attestation.data.hash_tree_root(); + + // Loop through zipped validator IDs and their corresponding signatures + // Verify each individual signature within the aggregated attestation + for (validator_id, signature) in + validator_ids.iter().zip(aggregated_signature.into_iter()) { - // Placeholder: XMSS verification disabled - // To enable, compile with --features xmss-verify - let _pubkey = &validator.pubkey; - let _slot = attestation.data.slot; - let _message = hash_tree_root(attestation); - let _sig = signature; + // Ensure validator exists in the active set + assert!( + *validator_id < num_validators, + "Validator index out of range" + ); + + let validator = validators.get(*validator_id).expect("validator must exist"); + + // Get the actual payload root for the attestation data + let attestation_root: [u8; 32] = + hash_tree_root(&aggregated_attestation.data).0.into(); + + // Verify the XMSS signature + assert!( + verify_xmss_signature( + validator.pubkey.0.as_bytes(), + aggregated_attestation.data.slot, + &attestation_root, + signature, + ), + "Attestation signature verification failed" + ); } + + // Verify the proposer attestation signature + let proposer_attestation = self.message.proposer_attestation.clone(); + let proposer_signature = signatures.proposer_signature; + + assert!( + proposer_attestation.validator_id.0 < num_validators, + "Proposer index out of range" + ); + + let proposer = validators + .get(proposer_attestation.validator_id.0) + .expect("proposer must exist"); + + let proposer_root: [u8; 32] = hash_tree_root(&proposer_attestation).0.into(); + assert!( + verify_xmss_signature( + proposer.pubkey.0.as_bytes(), + proposer_attestation.data.slot, + &proposer_root, + &proposer_signature, + ), + "Proposer attestation signature verification failed" + ); } true } -} \ No newline at end of file +} + +#[cfg(feature = "xmss-verify")] +pub fn verify_xmss_signature( + pubkey_bytes: &[u8], + slot: Slot, + message_bytes: &[u8; 32], + signature: &Signature, +) -> bool { + use leansig::serialization::Serializable; + use leansig::signature::SignatureScheme; + + let epoch = slot.0 as u32; + + type PubKey = ::PublicKey; + let pubkey = match PubKey::from_bytes(pubkey_bytes) { + Ok(pk) => pk, + Err(_) => return false, + }; + + type Sig = ::Signature; + let sig = match Sig::from_bytes(signature.as_bytes()) { + Ok(s) => s, + Err(_) => return false, + }; + + SIGTargetSumLifetime20W2NoOff::verify(&pubkey, epoch, message_bytes, &sig) +} + +#[cfg(not(feature = "xmss-verify"))] +pub fn verify_xmss_signature( + _pubkey_bytes: &[u8], + _slot: Slot, + _message_bytes: &[u8; 32], + _signature: &Signature, +) -> bool { + true +} diff --git a/lean_client/containers/src/serde_helpers.rs b/lean_client/containers/src/serde_helpers.rs index 0568f71..01604e5 100644 --- a/lean_client/containers/src/serde_helpers.rs +++ b/lean_client/containers/src/serde_helpers.rs @@ -187,6 +187,7 @@ pub mod signature { /// where each signature can be either hex string or structured XMSS format pub mod block_signatures { use super::*; + use crate::block::BlockSignatures; use crate::Signature; use serde_json::Value; use ssz::PersistentList; @@ -309,11 +310,11 @@ pub mod block_signatures { } #[cfg(feature = "devnet2")] - pub fn serialize(value: &BlockSignatures, serializer: S) -> Result + pub fn serialize(_value: &BlockSignatures, _serializer: S) -> Result where S: Serializer, { - Err(serde::de::Error::custom( + Err(serde::ser::Error::custom( "BlockSignatures serialization not implemented for devnet2", )) } diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 46641e1..02a26e3 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -8,6 +8,8 @@ use ssz::{PersistentList as List, PersistentList}; use ssz_derive::Ssz; use std::collections::BTreeMap; use typenum::U4096; +use crate::attestation::AggregatedAttestations; +use crate::block::BlockSignatures; pub const VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const JUSTIFICATION_ROOTS_LIMIT: usize = 1 << 18; // 262144 @@ -294,7 +296,20 @@ impl State { pub fn process_block(&self, block: &Block) -> Result { let state = self.process_block_header(block)?; + #[cfg(feature = "devnet1")] let state_after_ops = state.process_attestations(&block.body.attestations); + #[cfg(feature = "devnet2")] + let state_after_ops = { + let mut unaggregated_attestations = Attestations::default(); + for aggregated_attestation in &block.body.attestations { + let plain_attestations = aggregated_attestation.to_plain(); + // For each attestatio in the vector, push to the list + for attestation in plain_attestations { + unaggregated_attestations.push(attestation).map_err(|e| format!("Failed to push attestation: {:?}", e))?; + } + } + state.process_attestations(&unaggregated_attestations) + }; // State root validation is handled by state_transition_with_validation when needed @@ -688,7 +703,7 @@ mod tests { config: st.config.clone(), ..st.clone() } - .is_proposer(ValidatorIndex(0))); + .is_proposer(ValidatorIndex(0))); } #[test] @@ -828,6 +843,7 @@ mod tests { } #[test] + #[cfg(feature = "devnet1")] fn test_build_block_advances_state() { // Create genesis state let genesis_state = State::generate_genesis(Uint64(0), Uint64(10)); @@ -868,6 +884,7 @@ mod tests { } #[test] + #[cfg(feature = "devnet1")] fn test_build_block_state_root_matches() { // Create genesis state let genesis_state = State::generate_genesis(Uint64(0), Uint64(3)); diff --git a/lean_client/containers/tests/unit_tests/attestation_aggregation.rs b/lean_client/containers/tests/unit_tests/attestation_aggregation.rs new file mode 100644 index 0000000..285aa46 --- /dev/null +++ b/lean_client/containers/tests/unit_tests/attestation_aggregation.rs @@ -0,0 +1,132 @@ +#[cfg(feature = "devnet2")] +#[cfg(test)] +mod tests { + use containers::attestation::{AggregatedAttestation, AggregationBits, Attestation, AttestationData}; + use containers::{Bytes32, Uint64}; + use containers::checkpoint::Checkpoint; + use containers::slot::Slot; + + #[test] + fn test_aggregated_attestation_structure() { + let att_data = AttestationData { + slot: Slot(5), + head: Checkpoint { + root: Bytes32::default(), + slot: Slot(4), + }, + target: Checkpoint { + root: Bytes32::default(), + slot: Slot(3), + }, + source: Checkpoint { + root: Bytes32::default(), + slot: Slot(2), + } + }; + + let bits = AggregationBits::from_validator_indices(&vec![2, 7]); + let agg = AggregatedAttestation { + aggregation_bits: bits.clone(), + data: att_data.clone() + }; + + let indices = agg.aggregation_bits.to_validator_indices(); + assert_eq!(indices.into_iter().collect::>(), vec![2, 7].into_iter().collect()); + assert_eq!(agg.data, att_data); + } + + #[test] + fn test_aggregate_attestations_by_common_data() { + let att_data1 = AttestationData { + slot: Slot(5), + head: Checkpoint { + root: Bytes32::default(), + slot: Slot(4), + }, + target: Checkpoint { + root: Bytes32::default(), + slot: Slot(3), + }, + source: Checkpoint { + root: Bytes32::default(), + slot: Slot(2), + } + }; + let att_data2 = AttestationData { + slot: Slot(6), + head: Checkpoint { + root: Bytes32::default(), + slot: Slot(5), + }, + target: Checkpoint { + root: Bytes32::default(), + slot: Slot(4), + }, + source: Checkpoint { + root: Bytes32::default(), + slot: Slot(3), + } + }; + + let attestations = vec![ + Attestation { + validator_id: Uint64(1), + data: att_data1.clone(), + }, + Attestation { + validator_id: Uint64(3), + data: att_data1.clone(), + }, + Attestation { + validator_id: Uint64(5), + data: att_data2.clone(), + }, + ]; + + let aggregated = AggregatedAttestation::aggregate_by_data(&attestations); + assert_eq!(aggregated.len(), 2); + + let agg1 = aggregated.iter().find(|agg| agg.data == att_data1).unwrap(); + let validator_ids1 = agg1.aggregation_bits.to_validator_indices(); + assert_eq!(validator_ids1.into_iter().collect::>(), vec![1, 3].into_iter().collect()); + + let agg2 = aggregated.iter().find(|agg| agg.data == att_data2).unwrap(); + let validator_ids2 = agg2.aggregation_bits.to_validator_indices(); + assert_eq!(validator_ids2, vec![5]); + } + + #[test] + fn test_aggregate_empty_attestations() { + let aggregated = AggregatedAttestation::aggregate_by_data(&[]); + assert!(aggregated.is_empty()); + } + + #[test] + fn test_aggregate_single_attestation() { + let att_data = AttestationData { + slot: Slot(5), + head: Checkpoint { + root: Bytes32::default(), + slot: Slot(4), + }, + target: Checkpoint { + root: Bytes32::default(), + slot: Slot(3), + }, + source: Checkpoint { + root: Bytes32::default(), + slot: Slot(2), + } + }; + + let attestations = vec![Attestation { + validator_id: Uint64(5), + data: att_data.clone(), + }]; + let aggregated = AggregatedAttestation::aggregate_by_data(&attestations); + + assert_eq!(aggregated.len(), 1); + let validator_ids = aggregated[0].aggregation_bits.to_validator_indices(); + assert_eq!(validator_ids, vec![5]); + } +} diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 781c43e..26fa0a5 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -1,7 +1,7 @@ -use containers::{ - Attestation, Attestations, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators -}; +use containers::{Attestation, Attestations, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators, AggregatedAttestation, Signature}; use ssz::{PersistentList}; +use typenum::U4096; +use containers::block::BlockSignatures; pub const DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const TEST_VALIDATOR_COUNT: usize = 4; // Actual validator count used in tests @@ -11,9 +11,38 @@ const _: [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT] = [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT]; pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Option) -> SignedBlockWithAttestation { + #[cfg(feature = "devnet1")] let body = BlockBody { attestations: attestations.unwrap_or_else(PersistentList::default), }; + #[cfg(feature = "devnet2")] + let body = BlockBody { + attestations: { + let attestations_vec = attestations.unwrap_or_default(); + + // Convert PersistentList into a Vec + let attestations_vec: Vec = attestations_vec.into_iter().cloned().collect(); + + let aggregated: Vec = + AggregatedAttestation::aggregate_by_data(&attestations_vec); + + + let aggregated: Vec = + AggregatedAttestation::aggregate_by_data(&attestations_vec); + + // Create a new empty PersistentList + let mut persistent_list: PersistentList = PersistentList::default(); + + // Push each aggregated attestation + for agg in aggregated { + persistent_list.push(agg).expect("PersistentList capacity exceeded"); + } + + persistent_list + }, + // other BlockBody fields... + }; + let block_message = Block { slot: Slot(slot), @@ -23,13 +52,29 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op body: body, }; - SignedBlockWithAttestation { + #[cfg(feature = "devnet1")] + let return_value = SignedBlockWithAttestation { message: BlockWithAttestation { block: block_message, proposer_attestation: Attestation::default(), }, signature: PersistentList::default(), - } + }; + + #[cfg(feature = "devnet2")] + let return_value = SignedBlockWithAttestation { + message: BlockWithAttestation { + block: block_message, + proposer_attestation: Attestation::default(), + }, + signature: BlockSignatures { + attestation_signatures: PersistentList::default(), + proposer_signature: Signature::default(), + } + }; + + return_value + } pub fn create_attestations(indices: &[usize]) -> Vec { diff --git a/lean_client/containers/tests/unit_tests/mod.rs b/lean_client/containers/tests/unit_tests/mod.rs index 16a5646..b9f442f 100644 --- a/lean_client/containers/tests/unit_tests/mod.rs +++ b/lean_client/containers/tests/unit_tests/mod.rs @@ -4,3 +4,4 @@ mod state_basic; mod state_justifications; mod state_process; mod state_transition; +mod attestation_aggregation; diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index 7db1849..afc1887 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -106,6 +106,7 @@ fn test_process_block_header_invalid( } // This test verifies that attestations correctly justify and finalize slots +#[cfg(feature = "devnet1")] #[test] fn test_process_attestations_justification_and_finalization() { let mut state = genesis_state(); diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index aca04cd..9fe6abb 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -1,9 +1,9 @@ // tests/state_transition.rs use containers::{ - block::{Block, SignedBlockWithAttestation, BlockWithAttestation, hash_tree_root}, + block::{hash_tree_root, Block, BlockWithAttestation, SignedBlockWithAttestation}, state::State, types::{Bytes32, Uint64}, - Slot, Attestation + Attestation, Attestations, Slot, }; use pretty_assertions::assert_eq; use rstest::fixture; @@ -24,13 +24,29 @@ fn test_state_transition_full() { let state = genesis_state(); let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - let signed_block_with_attestation = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let signed_block_with_attestation = + create_block(1, &mut state_at_slot_1.latest_block_header, None); let block = signed_block_with_attestation.message.block.clone(); // Use process_block_header + process_operations to avoid state root validation during setup let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); + + #[cfg(feature = "devnet1")] let expected_state = state_after_header.process_attestations(&block.body.attestations); + #[cfg(feature = "devnet2")] + let expected_state = { + let mut unaggregated_attestations = Attestations::default(); + for aggregated_attestation in &block.body.attestations { + let plain_attestations = aggregated_attestation.to_plain(); + // For each attestatio in the vector, push to the list + for attestation in plain_attestations { + unaggregated_attestations.push(attestation); + } + } + state_after_header.process_attestations(&unaggregated_attestations) + }; + let block_with_correct_root = Block { state_root: hash_tree_root(&expected_state), ..block @@ -44,7 +60,9 @@ fn test_state_transition_full() { signature: signed_block_with_attestation.signature, }; - let final_state = state.state_transition(final_signed_block_with_attestation, true).unwrap(); + let final_state = state + .state_transition(final_signed_block_with_attestation, true) + .unwrap(); assert_eq!(final_state, expected_state); } @@ -54,13 +72,29 @@ fn test_state_transition_invalid_signatures() { let state = genesis_state(); let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - let signed_block_with_attestation = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let signed_block_with_attestation = + create_block(1, &mut state_at_slot_1.latest_block_header, None); let block = signed_block_with_attestation.message.block.clone(); // Use process_block_header + process_operations to avoid state root validation during setup let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); + + #[cfg(feature = "devnet1")] let expected_state = state_after_header.process_attestations(&block.body.attestations); + #[cfg(feature = "devnet2")] + let expected_state = { + let mut list = Attestations::default(); + for aggregated_attestation in &block.body.attestations { + let plain_attestations = aggregated_attestation.to_plain(); + // For each attestatio in the vector, push to the list + for attestation in plain_attestations { + list.push(attestation); + } + } + list + }; + let block_with_correct_root = Block { state_root: hash_tree_root(&expected_state), ..block @@ -85,7 +119,8 @@ fn test_state_transition_bad_state_root() { let state = genesis_state(); let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - let signed_block_with_attestation = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let signed_block_with_attestation = + create_block(1, &mut state_at_slot_1.latest_block_header, None); let mut block = signed_block_with_attestation.message.block.clone(); block.state_root = Bytes32(ssz::H256::zero()); @@ -101,4 +136,56 @@ fn test_state_transition_bad_state_root() { let result = state.state_transition(final_signed_block_with_attestation, true); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Invalid block state root"); -} \ No newline at end of file +} + +#[cfg(feature = "devnet2")] +#[test] +fn test_state_transition_devnet2() { + let state = genesis_state(); + let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); + + // Create a block with attestations for devnet2 + let signed_block_with_attestation = + create_block(1, &mut state_at_slot_1.latest_block_header, None); + let block = signed_block_with_attestation.message.block.clone(); + + // Process the block header and attestations + let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); + + #[cfg(feature = "devnet1")] + let expected_state = state_after_header.process_attestations(&block.body.attestations); + + #[cfg(feature = "devnet2")] + let expected_state = { + let mut unaggregated_attestations = Attestations::default(); + for aggregated_attestation in &block.body.attestations { + let plain_attestations = aggregated_attestation.to_plain(); + // For each attestatio in the vector, push to the list + for attestation in plain_attestations { + unaggregated_attestations.push(attestation); + } + } + state_after_header.process_attestations(&unaggregated_attestations) + }; + + // Ensure the state root matches the expected state + let block_with_correct_root = Block { + state_root: hash_tree_root(&expected_state), + ..block + }; + + let final_signed_block_with_attestation = SignedBlockWithAttestation { + message: BlockWithAttestation { + block: block_with_correct_root, + proposer_attestation: signed_block_with_attestation.message.proposer_attestation, + }, + signature: signed_block_with_attestation.signature, + }; + + // Perform the state transition and validate the result + let final_state = state + .state_transition(final_signed_block_with_attestation, true) + .unwrap(); + + assert_eq!(final_state, expected_state); +} From ed67aaf959a481a99db6ae1ecb3679920444152e Mon Sep 17 00:00:00 2001 From: Julius Mieliauskas Date: Mon, 29 Dec 2025 12:37:57 +0200 Subject: [PATCH 04/24] fixed environment selection by adding a minimal crate `env-config`. Added readme on how to select devnet --- lean_client/Cargo.lock | 18 +- lean_client/Cargo.toml | 8 +- lean_client/ENVIRONMENT_SELECTION.md | 26 +++ lean_client/containers/Cargo.toml | 7 +- lean_client/containers/src/attestation.rs | 6 +- lean_client/containers/src/state.rs | 4 +- lean_client/containers/tests/main.rs | 2 +- .../tests/test_vectors/block_processing.rs | 10 + .../containers/tests/test_vectors/runner.rs | 19 +- .../tests/test_vectors/verify_signatures.rs | 4 + lean_client/env-config/Cargo.toml | 12 ++ lean_client/env-config/src/lib.rs | 1 + lean_client/fork_choice/Cargo.toml | 8 +- lean_client/fork_choice/src/handlers.rs | 183 +++++++++++++----- lean_client/fork_choice/src/store.rs | 3 + .../tests/fork_choice_test_vectors.rs | 10 + .../fork_choice/tests/unit_tests/votes.rs | 9 + lean_client/networking/Cargo.toml | 6 + lean_client/networking/src/network/service.rs | 7 + lean_client/networking/src/types.rs | 5 + lean_client/src/main.rs | 54 +++++- lean_client/validator/Cargo.toml | 3 + lean_client/validator/src/lib.rs | 120 ++++++++++-- 23 files changed, 430 insertions(+), 95 deletions(-) create mode 100644 lean_client/ENVIRONMENT_SELECTION.md create mode 100644 lean_client/env-config/Cargo.toml create mode 100644 lean_client/env-config/src/lib.rs diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index 93fb9dd..910d9c1 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -859,6 +859,7 @@ dependencies = [ name = "containers" version = "0.1.0" dependencies = [ + "env-config", "hex", "leansig", "pretty_assertions", @@ -1399,6 +1400,10 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "env-config" +version = "0.1.0" + [[package]] name = "equivalent" version = "1.0.2" @@ -1412,7 +1417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -1586,6 +1591,7 @@ name = "fork-choice" version = "0.1.0" dependencies = [ "containers", + "env-config", "serde", "serde_json", "ssz", @@ -2311,7 +2317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", "serde_core", ] @@ -3217,6 +3223,7 @@ dependencies = [ "async-trait", "containers", "enr", + "env-config", "futures", "libp2p", "libp2p-identity 0.2.12", @@ -3265,7 +3272,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] @@ -4268,7 +4275,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -4919,7 +4926,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -5340,6 +5347,7 @@ name = "validator" version = "0.1.0" dependencies = [ "containers", + "env-config", "fork-choice", "leansig", "serde", diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index 7d98ae7..9e72c43 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["chain", "containers", "fork_choice", "networking", "validator"] +members = ["chain", "containers", "env-config", "fork_choice", "networking", "validator"] resolver = "2" [workspace.package] @@ -14,7 +14,7 @@ containers = { path = "./containers" } fork_choice = { path = "./fork_choice" } networking = { path = "./networking" } validator = { path = "./validator" } -libp2p = {version = "0.56.0", default-features = false, features = [ +libp2p = { version = "0.56.0", default-features = false, features = [ 'dns', 'gossipsub', 'identify', @@ -52,8 +52,10 @@ version = "0.1.0" edition = "2021" [features] -default = ["xmss-signing"] +default = ["devnet2", "xmss-signing"] xmss-signing = ["validator/xmss-signing"] +devnet1 = ["containers/devnet1", "fork-choice/devnet1", "networking/devnet1", "validator/devnet1"] +devnet2 = ["containers/devnet2", "fork-choice/devnet2", "networking/devnet2", "validator/devnet2"] [dependencies] chain = { path = "./chain" } diff --git a/lean_client/ENVIRONMENT_SELECTION.md b/lean_client/ENVIRONMENT_SELECTION.md new file mode 100644 index 0000000..d906c9d --- /dev/null +++ b/lean_client/ENVIRONMENT_SELECTION.md @@ -0,0 +1,26 @@ +### To select which devnet you want to compile + +#### Option A +- Change the default features in root `Cargo.toml`: +```toml +[features] +default = ["devnet1", "<...other features>"] # Change to "devnet2" if needed +devnet1 = [...] +devnet2 = [...] +``` + +#### Option B +- Use the `--no-default-features` flag and specify the desired devnet feature when building or running the project: +```bash +cargo build --no-default-features --features devnet1 # Change to devnet2 +``` + + +### Running tests for a specific devnet + +From root directory, use the following command: +```bash +cargo test -p --no-default-features --features devnet1 # Change to devnet2 +``` + +Use `` to specify the crate you want to test. \ No newline at end of file diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index b6a45c3..29e8ecd 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -5,15 +5,16 @@ edition = "2021" [features] xmss-verify = ["leansig"] -default = ["devnet1"] -devnet1 = [] -devnet2 = [] +default = [] +devnet1 = ["env-config/devnet1"] +devnet2 = ["env-config/devnet2"] [lib] name = "containers" path = "src/lib.rs" [dependencies] +env-config = { path = "../env-config", default-features = false } ssz = { git = "https://github.com/grandinetech/grandine", package = "ssz", branch = "develop", submodules = true } ssz_derive = { git = "https://github.com/grandinetech/grandine", package = "ssz_derive", branch = "develop", submodules = false } typenum = "1" diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 302ef08..9c3537c 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -111,8 +111,12 @@ pub struct Attestation { /// Validator attestation bundled with its signature. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct SignedAttestation { + #[cfg(feature = "devnet2")] + pub validator_id: u64, + #[cfg(feature = "devnet2")] + pub message: AttestationData, + #[cfg(feature = "devnet1")] pub message: Attestation, - /// Signature aggregation produced by the leanVM (SNARKs in the future). pub signature: Signature, } diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 02a26e3..b176e55 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -1,5 +1,5 @@ use crate::validator::Validator; -use crate::{block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, Attestation, Attestations, Bytes32, Checkpoint, Config, Signature, Slot, Uint64, ValidatorIndex}; +use crate::{block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, Attestation, Attestations, Bytes32, Checkpoint, Config, Signature, SignedAttestation, Slot, Uint64, ValidatorIndex}; use crate::{ HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, Validators, }; @@ -686,7 +686,7 @@ impl State { _proposer_index: ValidatorIndex, _parent_root: Bytes32, _initial_attestations: Option>, - _available_signed_attestations: Option<&[SignedBlockWithAttestation]>, + _available_signed_attestations: Option<&[SignedAttestation]>, _known_block_roots: Option<&std::collections::HashSet>, ) -> Result<(Block, Self, Vec, BlockSignatures), String> { Err("build_block is not implemented for devnet2".to_string()) diff --git a/lean_client/containers/tests/main.rs b/lean_client/containers/tests/main.rs index 96deacd..4d48535 100644 --- a/lean_client/containers/tests/main.rs +++ b/lean_client/containers/tests/main.rs @@ -1,4 +1,4 @@ -// tests/main.rs - Test entry point +// tests/lib - Test entry point mod debug_deserialize; mod unit_tests; mod test_vectors; \ No newline at end of file diff --git a/lean_client/containers/tests/test_vectors/block_processing.rs b/lean_client/containers/tests/test_vectors/block_processing.rs index 4dcd641..caec865 100644 --- a/lean_client/containers/tests/test_vectors/block_processing.rs +++ b/lean_client/containers/tests/test_vectors/block_processing.rs @@ -2,6 +2,7 @@ use super::runner::TestRunner; #[test] +#[cfg(feature = "devnet1")] fn test_process_first_block_after_genesis() { let test_path = "../tests/test_vectors/test_blocks/test_process_first_block_after_genesis.json"; TestRunner::run_block_processing_test(test_path) @@ -9,6 +10,7 @@ fn test_process_first_block_after_genesis() { } #[test] +#[cfg(feature = "devnet1")] fn test_blocks_with_gaps() { let test_path = "../tests/test_vectors/test_blocks/test_blocks_with_gaps.json"; TestRunner::run_block_processing_test(test_path) @@ -16,6 +18,7 @@ fn test_blocks_with_gaps() { } #[test] +#[cfg(feature = "devnet1")] fn test_linear_chain_multiple_blocks() { let test_path = "../tests/test_vectors/test_blocks/test_linear_chain_multiple_blocks.json"; TestRunner::run_block_processing_test(test_path) @@ -23,6 +26,7 @@ fn test_linear_chain_multiple_blocks() { } #[test] +#[cfg(feature = "devnet1")] fn test_block_extends_deep_chain() { let test_path = "../tests/test_vectors/test_blocks/test_block_extends_deep_chain.json"; TestRunner::run_block_processing_test(test_path) @@ -30,6 +34,7 @@ fn test_block_extends_deep_chain() { } #[test] +#[cfg(feature = "devnet1")] fn test_empty_blocks() { let test_path = "../tests/test_vectors/test_blocks/test_empty_blocks.json"; TestRunner::run_block_processing_test(test_path) @@ -37,6 +42,7 @@ fn test_empty_blocks() { } #[test] +#[cfg(feature = "devnet1")] fn test_empty_blocks_with_missed_slots() { let test_path = "../tests/test_vectors/test_blocks/test_empty_blocks_with_missed_slots.json"; TestRunner::run_block_processing_test(test_path) @@ -44,6 +50,7 @@ fn test_empty_blocks_with_missed_slots() { } #[test] +#[cfg(feature = "devnet1")] fn test_block_at_large_slot_number() { let test_path = "../tests/test_vectors/test_blocks/test_block_at_large_slot_number.json"; TestRunner::run_block_processing_test(test_path) @@ -53,6 +60,7 @@ fn test_block_at_large_slot_number() { // Invalid block tests (expecting failures) #[test] +#[cfg(feature = "devnet1")] fn test_block_with_invalid_parent_root() { let test_path = "../tests/test_vectors/test_blocks/test_block_with_invalid_parent_root.json"; TestRunner::run_block_processing_test(test_path) @@ -60,6 +68,7 @@ fn test_block_with_invalid_parent_root() { } #[test] +#[cfg(feature = "devnet1")] fn test_block_with_invalid_proposer() { let test_path = "../tests/test_vectors/test_blocks/test_block_with_invalid_proposer.json"; TestRunner::run_block_processing_test(test_path) @@ -67,6 +76,7 @@ fn test_block_with_invalid_proposer() { } #[test] +#[cfg(feature = "devnet1")] fn test_block_with_invalid_state_root() { let test_path = "../tests/test_vectors/test_blocks/test_block_with_invalid_state_root.json"; TestRunner::run_block_processing_test(test_path) diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index 910fde5..bf23138 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -552,6 +552,7 @@ impl TestRunner { /// Test runner for verify_signatures test vectors /// Tests XMSS signature verification on SignedBlockWithAttestation + #[cfg(feature = "devnet1")] pub fn run_verify_signatures_test>(path: P) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; @@ -571,25 +572,11 @@ impl TestRunner { println!(" Block slot: {}", signed_block.message.block.slot.0); println!(" Proposer index: {}", signed_block.message.block.proposer_index.0); - // Count attestations - let mut attestation_count = 0u64; - loop { - match signed_block.message.block.body.attestations.get(attestation_count) { - Ok(_) => attestation_count += 1, - Err(_) => break, - } - } + let attestation_count = signed_block.message.block.body.attestations.len_u64(); println!(" Attestations in block: {}", attestation_count); println!(" Proposer attestation validator: {}", signed_block.message.proposer_attestation.validator_id.0); - // Count signatures - let mut signature_count = 0u64; - loop { - match signed_block.signature.get(signature_count) { - Ok(_) => signature_count += 1, - Err(_) => break, - } - } + let signature_count = signed_block.signature.len_u64(); println!(" Signatures: {}", signature_count); // Check if we expect this test to fail diff --git a/lean_client/containers/tests/test_vectors/verify_signatures.rs b/lean_client/containers/tests/test_vectors/verify_signatures.rs index 2bca4ca..cfc3301 100644 --- a/lean_client/containers/tests/test_vectors/verify_signatures.rs +++ b/lean_client/containers/tests/test_vectors/verify_signatures.rs @@ -15,6 +15,7 @@ use super::runner::TestRunner; // Without xmss-verify feature, they pass because structural validation succeeds. #[test] +#[cfg(feature = "devnet1")] fn test_proposer_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json"; TestRunner::run_verify_signatures_test(test_path) @@ -22,6 +23,7 @@ fn test_proposer_signature() { } #[test] +#[cfg(feature = "devnet1")] fn test_proposer_and_attester_signatures() { let test_path = "../tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_and_attester_signatures.json"; TestRunner::run_verify_signatures_test(test_path) @@ -34,6 +36,7 @@ fn test_proposer_and_attester_signatures() { // Run with `cargo test --features xmss-verify` to enable full signature verification. #[test] +#[cfg(feature = "devnet1")] #[ignore = "Requires xmss-verify feature for actual signature validation. Run with: cargo test --features xmss-verify"] fn test_invalid_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json"; @@ -42,6 +45,7 @@ fn test_invalid_signature() { } #[test] +#[cfg(feature = "devnet1")] #[ignore = "Requires xmss-verify feature for actual signature validation. Run with: cargo test --features xmss-verify"] fn test_mixed_valid_invalid_signatures() { let test_path = "../tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_mixed_valid_invalid_signatures.json"; diff --git a/lean_client/env-config/Cargo.toml b/lean_client/env-config/Cargo.toml new file mode 100644 index 0000000..4b761e5 --- /dev/null +++ b/lean_client/env-config/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "env-config" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[features] +devnet1 = [] +devnet2 = [] + +[dependencies] diff --git a/lean_client/env-config/src/lib.rs b/lean_client/env-config/src/lib.rs new file mode 100644 index 0000000..972005d --- /dev/null +++ b/lean_client/env-config/src/lib.rs @@ -0,0 +1 @@ +// Empty on purpose \ No newline at end of file diff --git a/lean_client/fork_choice/Cargo.toml b/lean_client/fork_choice/Cargo.toml index f906f59..b16f561 100644 --- a/lean_client/fork_choice/Cargo.toml +++ b/lean_client/fork_choice/Cargo.toml @@ -3,8 +3,14 @@ name = "fork-choice" version = "0.1.0" edition = "2021" +[features] +default = [] +devnet1 = ["containers/devnet1", "env-config/devnet1"] +devnet2 = ["containers/devnet2", "env-config/devnet1"] + [dependencies] -containers = { path = "../containers" } +env-config = { path = "../env-config", default-features = false } +containers = { path = "../containers", default-features = false } ssz = { git = "https://github.com/grandinetech/grandine", package = "ssz", branch = "develop"} ssz_derive = { git = "https://github.com/grandinetech/grandine", package = "ssz_derive", branch = "develop" } typenum = "1.17.0" diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 618c8c9..fa9aa89 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -1,16 +1,13 @@ use crate::store::*; use containers::{ - attestation::SignedAttestation, - block::SignedBlockWithAttestation, - Bytes32, ValidatorIndex, + attestation::SignedAttestation, block::SignedBlockWithAttestation, Bytes32, ValidatorIndex, }; use ssz::SszHash; #[inline] pub fn on_tick(store: &mut Store, time: u64, has_proposal: bool) { // Calculate target time in intervals - let tick_interval_time = - time.saturating_sub(store.config.genesis_time) / SECONDS_PER_INTERVAL; + let tick_interval_time = time.saturating_sub(store.config.genesis_time) / SECONDS_PER_INTERVAL; // Tick forward one interval at a time while store.time < tick_interval_time { @@ -28,11 +25,25 @@ pub fn on_attestation( signed_attestation: SignedAttestation, is_from_block: bool, ) -> Result<(), String> { + #[cfg(feature = "devnet1")] let validator_id = ValidatorIndex(signed_attestation.message.validator_id.0); + #[cfg(feature = "devnet1")] let attestation_slot = signed_attestation.message.data.slot; + #[cfg(feature = "devnet1")] let source_slot = signed_attestation.message.data.source.slot; + #[cfg(feature = "devnet1")] let target_slot = signed_attestation.message.data.target.slot; + + #[cfg(feature = "devnet2")] + let validator_id = ValidatorIndex(signed_attestation.validator_id); + #[cfg(feature = "devnet2")] + let attestation_slot = signed_attestation.message.slot; + #[cfg(feature = "devnet2")] + let source_slot = signed_attestation.message.source.slot; + #[cfg(feature = "devnet2")] + let target_slot = signed_attestation.message.target.slot; + // Validate attestation is not from future let curr_slot = store.time / INTERVALS_PER_SLOT; if attestation_slot.0 > curr_slot { @@ -52,28 +63,69 @@ pub fn on_attestation( if is_from_block { // On-chain attestation processing - immediately becomes "known" + #[cfg(feature = "devnet1")] + if store + .latest_known_attestations + .get(&validator_id) + .map_or(true, |existing| { + existing.message.data.slot < attestation_slot + }) + { + store + .latest_known_attestations + .insert(validator_id, signed_attestation.clone()); + } + + #[cfg(feature = "devnet2")] if store .latest_known_attestations .get(&validator_id) - .map_or(true, |existing| existing.message.data.slot < attestation_slot) + .map_or(true, |existing| { + existing.message.slot < attestation_slot + }) { - store.latest_known_attestations.insert(validator_id, signed_attestation.clone()); + store + .latest_known_attestations + .insert(validator_id, signed_attestation.clone()); } // Remove from new attestations if superseded if let Some(existing_new) = store.latest_new_attestations.get(&validator_id) { + #[cfg(feature = "devnet1")] if existing_new.message.data.slot <= attestation_slot { store.latest_new_attestations.remove(&validator_id); } + #[cfg(feature = "devnet2")] + if existing_new.message.slot <= attestation_slot { + store.latest_new_attestations.remove(&validator_id); + } } } else { // Network gossip attestation processing - goes to "new" stage + #[cfg(feature = "devnet1")] + if store + .latest_new_attestations + .get(&validator_id) + .map_or(true, |existing| { + existing.message.data.slot < attestation_slot + }) + { + store + .latest_new_attestations + .insert(validator_id, signed_attestation); + } + + #[cfg(feature = "devnet2")] if store .latest_new_attestations .get(&validator_id) - .map_or(true, |existing| existing.message.data.slot < attestation_slot) + .map_or(true, |existing| { + existing.message.slot < attestation_slot + }) { - store.latest_new_attestations.insert(validator_id, signed_attestation); + store + .latest_new_attestations + .insert(validator_id, signed_attestation); } } Ok(()) @@ -125,8 +177,7 @@ fn process_block_internal( }; // Execute state transition to get post-state - let new_state = - state.state_transition_with_validation(signed_block.clone(), true, true)?; + let new_state = state.state_transition_with_validation(signed_block.clone(), true, true)?; // Store block and state store.blocks.insert(block_root, signed_block.clone()); @@ -143,49 +194,93 @@ fn process_block_internal( let attestations = &signed_block.message.block.body.attestations; let signatures = &signed_block.signature; - for i in 0.. { - match (attestations.get(i), signatures.get(i)) { - (Ok(attestation), Ok(signature)) => { - let signed_attestation = SignedAttestation { - message: attestation.clone(), - signature: signature.clone(), - }; - on_attestation(store, signed_attestation, true)?; + #[cfg(feature = "devnet1")] + { + for i in 0.. { + match (attestations.get(i), signatures.get(i)) { + (Ok(attestation), Ok(signature)) => { + let signed_attestation = SignedAttestation { + message: attestation.clone(), + signature: signature.clone(), + }; + on_attestation(store, signed_attestation, true)?; + } + _ => break, } - _ => break, } + + // Update head BEFORE processing proposer attestation + update_head(store); + + // Process proposer attestation as gossip (is_from_block=false) + // This ensures it goes to "new" attestations and doesn't immediately affect fork choice + let num_body_attestations = attestations.len_u64(); + + // Get proposer signature or use default if not present (for tests) + use containers::attestation::Signature; + let proposer_signature = signatures + .get(num_body_attestations) + .map(|sig| sig.clone()) + .unwrap_or_else(|_| Signature::default()); + + let proposer_signed_attestation = SignedAttestation { + message: signed_block.message.proposer_attestation.clone(), + signature: proposer_signature, + }; + + // Process proposer attestation as if received via gossip (is_from_block=false) + // This ensures it goes to "new" attestations and doesn't immediately affect fork choice + on_attestation(store, proposer_signed_attestation, false)?; + + Ok(()) } - // Update head BEFORE processing proposer attestation - update_head(store); + #[cfg(feature = "devnet2")] + { + let aggregated_attestations = &signed_block.message.block.body.attestations; + let attestation_signatures = &signed_block.signature.attestation_signatures; + let proposer_attestation = &signed_block.message.proposer_attestation; - // Process proposer attestation as gossip (is_from_block=false) - // This ensures it goes to "new" attestations and doesn't immediately affect fork choice - let num_body_attestations = { - let mut count = 0; - while attestations.get(count).is_ok() { - count += 1; + for (aggregated_attestation, aggregated_signature) in aggregated_attestations + .into_iter() + .zip(attestation_signatures) + { + let validator_ids: Vec = aggregated_attestation + .aggregation_bits.0 + .iter() + .enumerate() + .filter(|(_, bit)| **bit) + .map(|(index, _)| index as u64) + .collect(); + + for (validator_id, signature) in validator_ids.into_iter().zip(aggregated_signature) { + on_attestation( + store, + SignedAttestation { + validator_id, + message: aggregated_attestation.data.clone(), + signature: *signature, + }, + true, + )?; + } } - count - }; - // Get proposer signature or use default if not present (for tests) - use containers::attestation::Signature; - let proposer_signature = signatures - .get(num_body_attestations) - .map(|sig| sig.clone()) - .unwrap_or_else(|_| Signature::default()); + // Update head BEFORE processing proposer attestation + update_head(store); - let proposer_signed_attestation = SignedAttestation { - message: signed_block.message.proposer_attestation.clone(), - signature: proposer_signature, - }; + let proposer_signed_attestation = SignedAttestation { + validator_id: proposer_attestation.validator_id.0, + message: proposer_attestation.data.clone(), + signature: signed_block.signature.proposer_signature, + }; - // Process proposer attestation as if received via gossip (is_from_block=false) - // This ensures it goes to "new" attestations and doesn't immediately affect fork choice - on_attestation(store, proposer_signed_attestation, false)?; + // Process proposer attestation as if received via gossip (is_from_block=false) + // This ensures it goes to "new" attestations and doesn't immediately affect fork choice + on_attestation(store, proposer_signed_attestation, false)?; - Ok(()) + Ok(()) + } } fn process_pending_blocks(store: &mut Store, mut roots: Vec) { diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 4c746d4..3296d06 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -85,7 +85,10 @@ pub fn get_fork_choice_head( // stage 1: accumulate weights by walking up from each attestation's head for attestation in latest_attestations.values() { + #[cfg(feature = "devnet1")] let mut curr = attestation.message.data.head.root; + #[cfg(feature = "devnet2")] + let mut curr = attestation.message.head.root; if let Some(block) = store.blocks.get(&curr) { let mut curr_slot = block.message.block.slot; diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index 5718d48..50bd240 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -256,6 +256,7 @@ fn convert_test_attestation(test_att: &TestAttestation) -> Attestation { } } +#[cfg(feature = "devnet1")] fn convert_test_anchor_block(test_block: &TestAnchorBlock) -> SignedBlockWithAttestation { let mut attestations = ssz::PersistentList::default(); @@ -303,6 +304,7 @@ fn convert_test_anchor_block(test_block: &TestAnchorBlock) -> SignedBlockWithAtt } } +#[cfg(feature = "devnet1")] fn convert_test_block(test_block_with_att: &TestBlockWithAttestation) -> SignedBlockWithAttestation { let test_block = &test_block_with_att.block; let mut attestations = ssz::PersistentList::default(); @@ -405,6 +407,7 @@ fn initialize_state_from_test(test_state: &TestAnchorState) -> State { } } +#[cfg(feature = "devnet1")] fn verify_checks( store: &Store, checks: &Option, @@ -493,6 +496,7 @@ fn verify_checks( Ok(()) } +#[cfg(feature = "devnet1")] fn run_single_test(_test_name: &str, test: TestVector) -> Result<(), String> { println!(" Running: {}", test.info.test_id); @@ -624,6 +628,7 @@ fn run_single_test(_test_name: &str, test: TestVector) -> Result<(), String> { Ok(()) } +#[cfg(feature = "devnet1")] fn run_test_vector_file(test_path: &str) -> Result<(), String> { let json_str = std::fs::read_to_string(test_path) .map_err(|e| format!("Failed to read file {}: {}", test_path, e))?; @@ -639,6 +644,7 @@ fn run_test_vector_file(test_path: &str) -> Result<(), String> { } #[test] +#[cfg(feature = "devnet1")] fn test_fork_choice_head_vectors() { let test_dir = "../tests/test_vectors/test_fork_choice/test_fork_choice_head"; @@ -682,6 +688,7 @@ fn test_fork_choice_head_vectors() { } #[test] +#[cfg(feature = "devnet1")] fn test_attestation_processing_vectors() { let test_dir = "../tests/test_vectors/test_fork_choice/test_attestation_processing"; @@ -725,6 +732,7 @@ fn test_attestation_processing_vectors() { } #[test] +#[cfg(feature = "devnet1")] fn test_fork_choice_reorgs_vectors() { let test_dir = "../tests/test_vectors/test_fork_choice/test_fork_choice_reorgs"; @@ -768,6 +776,7 @@ fn test_fork_choice_reorgs_vectors() { } #[test] +#[cfg(feature = "devnet1")] fn test_attestation_target_selection_vectors() { let test_dir = "../tests/test_vectors/test_fork_choice/test_attestation_target_selection"; @@ -811,6 +820,7 @@ fn test_attestation_target_selection_vectors() { } #[test] +#[cfg(feature = "devnet1")] fn test_lexicographic_tiebreaker_vectors() { let test_dir = "../tests/test_vectors/test_fork_choice/test_lexicographic_tiebreaker"; diff --git a/lean_client/fork_choice/tests/unit_tests/votes.rs b/lean_client/fork_choice/tests/unit_tests/votes.rs index 805e785..4a1b688 100644 --- a/lean_client/fork_choice/tests/unit_tests/votes.rs +++ b/lean_client/fork_choice/tests/unit_tests/votes.rs @@ -7,6 +7,7 @@ use containers::{ Bytes32, Slot, Uint64, ValidatorIndex, }; +#[cfg(feature = "devnet1")] fn create_signed_attestation(validator_id: u64, slot: Slot, head_root: Bytes32) -> SignedAttestation { SignedAttestation { message: Attestation { @@ -23,6 +24,7 @@ fn create_signed_attestation(validator_id: u64, slot: Slot, head_root: Bytes32) } #[test] +#[cfg(feature = "devnet1")] fn test_accept_new_attestations() { let mut store = create_test_store(); @@ -63,6 +65,7 @@ fn test_accept_new_attestations() { } #[test] +#[cfg(feature = "devnet1")] fn test_accept_new_attestations_multiple() { let mut store = create_test_store(); @@ -94,6 +97,7 @@ fn test_accept_new_attestations_empty() { } #[test] +#[cfg(feature = "devnet1")] fn test_on_attestation_lifecycle() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); @@ -129,6 +133,7 @@ fn test_on_attestation_lifecycle() { } #[test] +#[cfg(feature = "devnet1")] fn test_on_attestation_future_slot() { let mut store = create_test_store(); let future_slot = Slot(100); // Far in the future @@ -140,6 +145,7 @@ fn test_on_attestation_future_slot() { } #[test] +#[cfg(feature = "devnet1")] fn test_on_attestation_update_vote() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); @@ -161,6 +167,7 @@ fn test_on_attestation_update_vote() { } #[test] +#[cfg(feature = "devnet1")] fn test_on_attestation_ignore_old_vote() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); @@ -183,6 +190,7 @@ fn test_on_attestation_ignore_old_vote() { } #[test] +#[cfg(feature = "devnet1")] fn test_on_attestation_from_block_supersedes_new() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); @@ -204,6 +212,7 @@ fn test_on_attestation_from_block_supersedes_new() { } #[test] +#[cfg(feature = "devnet1")] fn test_on_attestation_newer_from_block_removes_older_new() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); diff --git a/lean_client/networking/Cargo.toml b/lean_client/networking/Cargo.toml index f107994..8f47702 100644 --- a/lean_client/networking/Cargo.toml +++ b/lean_client/networking/Cargo.toml @@ -3,7 +3,13 @@ name = "networking" version = "0.1.0" edition = "2024" +[features] +default = [] +devnet1 = ["containers/devnet1", "env-config/devnet1"] +devnet2 = ["containers/devnet2", "env-config/devnet1"] + [dependencies] +env-config = { path = "../env-config", default-features = false } containers = {workspace = true} alloy-primitives = { workspace = true} libp2p = {workspace = true} diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 9c0993f..93e749c 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -311,7 +311,10 @@ where } } Ok(GossipsubMessage::Attestation(signed_attestation)) => { + #[cfg(feature = "devnet1")] let slot = signed_attestation.message.data.slot.0; + #[cfg(feature = "devnet2")] + let slot = signed_attestation.message.slot.0; if let Err(err) = self .chain_message_sink @@ -521,7 +524,11 @@ where } } OutboundP2pRequest::GossipAttestation(signed_attestation) => { + #[cfg(feature = "devnet1")] let slot = signed_attestation.message.data.slot.0; + #[cfg(feature = "devnet2")] + let slot = signed_attestation.message.slot.0; + match signed_attestation.to_ssz() { Ok(bytes) => { if let Err(err) = self.publish_to_topic(GossipsubKind::Attestation, bytes) { diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index 37644c2..028a883 100644 --- a/lean_client/networking/src/types.rs +++ b/lean_client/networking/src/types.rs @@ -93,9 +93,14 @@ impl Display for ChainMessage { ChainMessage::ProcessBlock { signed_block_with_attestation, .. } => { write!(f, "ProcessBlockWithAttestation(slot={})", signed_block_with_attestation.message.block.slot.0) } + #[cfg(feature = "devnet1")] ChainMessage::ProcessAttestation { signed_attestation, .. } => { write!(f, "ProcessAttestation(slot={})", signed_attestation.message.data.slot.0) } + #[cfg(feature = "devnet2")] + ChainMessage::ProcessAttestation { signed_attestation, .. } => { + write!(f, "ProcessAttestation(slot={})", signed_attestation.message.slot.0) + } } } } diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index cc44285..d1c3e24 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use containers::block::BlockSignatures; use containers::ssz::{PersistentList, SszHash}; use containers::{ attestation::{Attestation, AttestationData}, @@ -8,7 +9,7 @@ use containers::{ ssz, state::State, types::{Bytes32, Uint64, ValidatorIndex}, - Slot, + Signature, Slot, }; use fork_choice::{ handlers::{on_attestation, on_block, on_tick}, @@ -95,7 +96,10 @@ fn print_chain_status(store: &Store, connected_peers: u64) { println!(" Head Block Root: 0x{:x}", head_root.0); println!(" Parent Block Root: 0x{:x}", parent_root.0); println!(" State Root: 0x{:x}", state_root.0); - println!(" Timely: {}", if timely { "YES" } else { "NO" }); + println!( + " Timely: {}", + if timely { "YES" } else { "NO" } + ); println!("+---------------------------------------------------------------+"); println!( " Latest Justified: Slot {:>5} | Root: 0x{:x}", @@ -216,7 +220,13 @@ async fn main() { block: genesis_block, proposer_attestation: genesis_proposer_attestation, }, + #[cfg(feature = "devnet1")] signature: PersistentList::default(), + #[cfg(feature = "devnet2")] + signature: BlockSignatures { + attestation_signatures: PersistentList::default(), + proposer_signature: Signature::default(), + }, }; let config = Config { genesis_time }; @@ -234,7 +244,11 @@ async fn main() { if let Some(ref keys_dir) = args.hash_sig_key_dir { let keys_path = std::path::Path::new(keys_dir); if keys_path.exists() { - match ValidatorService::new_with_keys(config.clone(), num_validators, keys_path) { + match ValidatorService::new_with_keys( + config.clone(), + num_validators, + keys_path, + ) { Ok(service) => { info!( node_id = %node_id, @@ -245,7 +259,10 @@ async fn main() { Some(service) } Err(e) => { - warn!("Failed to load XMSS keys: {}, falling back to zero signatures", e); + warn!( + "Failed to load XMSS keys: {}, falling back to zero signatures", + e + ); Some(ValidatorService::new(config, num_validators)) } } @@ -417,14 +434,29 @@ async fn main() { if last_attestation_slot != Some(current_slot) { let attestations = vs.create_attestations(&store, Slot(current_slot)); for signed_att in attestations { + #[cfg(feature = "devnet1")] let validator_id = signed_att.message.validator_id.0; + #[cfg(feature = "devnet2")] + let validator_id = signed_att.validator_id; info!( slot = current_slot, validator = validator_id, "Broadcasting attestation" ); + #[cfg(feature = "devnet1")] + match on_attestation(&mut store, signed_att.clone(), false) { + Ok(()) => { + if let Err(e) = chain_outbound_sender.send( + OutboundP2pRequest::GossipAttestation(signed_att) + ) { + warn!("Failed to gossip attestation: {}", e); + } + } + Err(e) => warn!("Error processing own attestation: {}", e), + } + #[cfg(feature = "devnet2")] match on_attestation(&mut store, signed_att.clone(), false) { Ok(()) => { if let Err(e) = chain_outbound_sender.send( @@ -520,10 +552,24 @@ async fn main() { should_gossip, .. } => { + #[cfg(feature = "devnet1")] let att_slot = signed_attestation.message.data.slot.0; + #[cfg(feature = "devnet1")] let source_slot = signed_attestation.message.data.source.slot.0; + #[cfg(feature = "devnet1")] let target_slot = signed_attestation.message.data.target.slot.0; + #[cfg(feature = "devnet1")] let validator_id = signed_attestation.message.validator_id.0; + + #[cfg(feature = "devnet2")] + let att_slot = signed_attestation.message.slot.0; + #[cfg(feature = "devnet2")] + let source_slot = signed_attestation.message.source.slot.0; + #[cfg(feature = "devnet2")] + let target_slot = signed_attestation.message.target.slot.0; + #[cfg(feature = "devnet2")] + let validator_id = signed_attestation.validator_id; + info!( slot = att_slot, source_slot = source_slot, diff --git a/lean_client/validator/Cargo.toml b/lean_client/validator/Cargo.toml index b658c48..8311b9d 100644 --- a/lean_client/validator/Cargo.toml +++ b/lean_client/validator/Cargo.toml @@ -6,8 +6,11 @@ edition = "2021" [features] default = ["xmss-signing"] xmss-signing = ["leansig"] +devnet1 = ["containers/devnet1", "fork-choice/devnet1", "env-config/devnet1"] +devnet2 = ["containers/devnet2", "fork-choice/devnet2", "env-config/devnet1"] [dependencies] +env-config = { path = "../env-config", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.9" containers = { path = "../containers" } diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index e26bcf7..2c65fa7 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -2,12 +2,16 @@ use std::collections::HashMap; use std::path::Path; +use containers::attestation::{AggregatedAttestations}; +#[cfg(feature = "devnet2")] +use containers::attestation::{NaiveAggregatedSignature}; +use containers::block::BlockSignatures; use containers::{ attestation::{Attestation, AttestationData, Signature, SignedAttestation}, - block::{BlockWithAttestation, SignedBlockWithAttestation, hash_tree_root}, + block::{hash_tree_root, BlockWithAttestation, SignedBlockWithAttestation}, checkpoint::Checkpoint, types::{Uint64, ValidatorIndex}, - Slot, + AggregatedAttestation, Slot, }; use fork_choice::store::{get_proposal_head, get_vote_target, Store}; use tracing::{info, warn}; @@ -172,23 +176,33 @@ impl ValidatorService { .latest_new_attestations .values() .filter(|att| { + #[cfg(feature = "devnet1")] let data = &att.message.data; + #[cfg(feature = "devnet2")] + let data = &att.message; // Source must match the parent state's justified checkpoint (not store's!) let source_matches = data.source == parent_state.latest_justified; // Target must be strictly after source let target_after_source = data.target.slot > data.source.slot; // Target block must be known let target_known = store.blocks.contains_key(&data.target.root); - + source_matches && target_after_source && target_known }) .collect(); + #[cfg(feature = "devnet1")] let valid_attestations: Vec = valid_signed_attestations .iter() .map(|att| att.message.clone()) .collect(); + #[cfg(feature = "devnet2")] + let valid_attestations: Vec = valid_signed_attestations + .iter() + .map(|att| att.message.clone()) + .collect(); + info!( slot = slot.0, valid_attestations = valid_attestations.len(), @@ -197,14 +211,52 @@ impl ValidatorService { ); // Build block with collected attestations (empty body - attestations go to state) - let (block, _post_state, _collected_atts, sigs) = - parent_state.build_block(slot, proposer_index, parent_root, Some(valid_attestations), None, None)?; + #[cfg(feature = "devnet1")] + let (block, _post_state, _collected_atts, sigs) = parent_state.build_block( + slot, + proposer_index, + parent_root, + Some(valid_attestations), + None, + None, + )?; + #[cfg(feature = "devnet2")] + let (block, _post_state, _collected_atts, sigs) = { + let valid_attestations: Vec = valid_attestations + .iter() + .map(|data| Attestation { + validator_id: Uint64(0), // Placeholder, real validator IDs should be used + data: data.clone(), + }) + .collect(); + parent_state.build_block( + slot, + proposer_index, + parent_root, + Some(valid_attestations), + None, + None, + )? + }; // Collect signatures from the attestations we included + #[cfg(feature = "devnet1")] let mut signatures = sigs; + #[cfg(feature = "devnet2")] + let mut signatures = sigs.attestation_signatures; for signed_att in &valid_signed_attestations { - signatures.push(signed_att.signature.clone()) + #[cfg(feature = "devnet1")] + signatures + .push(signed_att.signature.clone()) .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; + #[cfg(feature = "devnet2")] + { + // TODO: Use real aggregation instead of naive placeholder when spec is more up to date + let aggregated_sig: NaiveAggregatedSignature = NaiveAggregatedSignature::default(); + signatures + .push(aggregated_sig) + .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; + } } info!( @@ -224,11 +276,20 @@ impl ValidatorService { match key_manager.sign(proposer_index.0, epoch, &message.0.into()) { Ok(sig) => { - signatures.push(sig).map_err(|e| format!("Failed to add proposer signature: {:?}", e))?; - info!( - proposer = proposer_index.0, - "Signed proposer attestation" - ); + #[cfg(feature = "devnet1")] + signatures + .push(sig) + .map_err(|e| format!("Failed to add proposer signature: {:?}", e))?; + #[cfg(feature = "devnet2")] + { + // TODO: Use real aggregation instead of naive placeholder when spec is more up to date + let aggregated_sig: NaiveAggregatedSignature = + NaiveAggregatedSignature::default(); + signatures + .push(aggregated_sig) + .map_err(|e| format!("Failed to add proposer signature: {:?}", e))?; + } + info!(proposer = proposer_index.0, "Signed proposer attestation"); } Err(e) => { return Err(format!("Failed to sign proposer attestation: {}", e)); @@ -244,7 +305,13 @@ impl ValidatorService { block, proposer_attestation, }, + #[cfg(feature = "devnet1")] signature: signatures, + #[cfg(feature = "devnet2")] + signature: BlockSignatures { + attestation_signatures: signatures, + proposer_signature: Signature::default(), + }, }; Ok(signed_block) @@ -284,6 +351,7 @@ impl ValidatorService { .validator_indices .iter() .filter_map(|&idx| { + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id: Uint64(idx), data: AttestationData { @@ -294,6 +362,14 @@ impl ValidatorService { }, }; + #[cfg(feature = "devnet2")] + let attestation = AttestationData { + slot, + head: head_checkpoint.clone(), + target: vote_target.clone(), + source: store.latest_justified.clone(), + }; + let signature = if let Some(ref key_manager) = self.key_manager { // Sign with XMSS let message = hash_tree_root(&attestation); @@ -331,10 +407,24 @@ impl ValidatorService { Signature::default() }; - Some(SignedAttestation { - message: attestation, - signature, - }) + { + #[cfg(feature = "devnet1")] + { + Some(SignedAttestation { + message: attestation, + signature, + }) + } + + #[cfg(feature = "devnet2")] + { + Some(SignedAttestation { + validator_id: idx, + message: attestation, + signature, + }) + } + } }) .collect() } From 99029a75399a67e06bbbabf10204f2da46dce749 Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Sun, 11 Jan 2026 23:31:55 +0200 Subject: [PATCH 05/24] Attestations changes based on leanSpec #234 commit: removed to_plain() method, added check for duplicate data --- lean_client/containers/src/attestation.rs | 27 ++- lean_client/containers/src/state.rs | 219 ++++++++++-------- .../tests/unit_tests/state_transition.rs | 44 +--- 3 files changed, 135 insertions(+), 155 deletions(-) diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 9c3537c..b4f5c9a 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -157,17 +157,26 @@ impl AggregatedAttestation { }) .collect() } +} - pub fn to_plain(&self) -> Vec { - let validator_indices = self.aggregation_bits.to_validator_indices(); +/// Trait for checking duplicate attestation data. +pub trait HasDuplicateData { + /// Returns true if the list contains duplicate AttestationData. + fn has_duplicate_data(&self) -> bool; +} - validator_indices - .into_iter() - .map(|validator_id| Attestation { - validator_id: Uint64(validator_id), - data: self.data.clone(), - }) - .collect() +impl HasDuplicateData for AggregatedAttestations { + fn has_duplicate_data(&self) -> bool { + use ssz::SszHash; + use std::collections::HashSet; + let mut seen: HashSet = HashSet::new(); + for attestation in self { + let root = attestation.data.hash_tree_root(); + if !seen.insert(root) { + return true; + } + } + false } } diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index b176e55..d6c7cf8 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -8,7 +8,7 @@ use ssz::{PersistentList as List, PersistentList}; use ssz_derive::Ssz; use std::collections::BTreeMap; use typenum::U4096; -use crate::attestation::AggregatedAttestations; +use crate::attestation::{AggregatedAttestations, HasDuplicateData}; use crate::block::BlockSignatures; pub const VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 @@ -296,24 +296,13 @@ impl State { pub fn process_block(&self, block: &Block) -> Result { let state = self.process_block_header(block)?; - #[cfg(feature = "devnet1")] - let state_after_ops = state.process_attestations(&block.body.attestations); + #[cfg(feature = "devnet2")] - let state_after_ops = { - let mut unaggregated_attestations = Attestations::default(); - for aggregated_attestation in &block.body.attestations { - let plain_attestations = aggregated_attestation.to_plain(); - // For each attestatio in the vector, push to the list - for attestation in plain_attestations { - unaggregated_attestations.push(attestation).map_err(|e| format!("Failed to push attestation: {:?}", e))?; - } - } - state.process_attestations(&unaggregated_attestations) - }; - - // State root validation is handled by state_transition_with_validation when needed + if block.body.attestations.has_duplicate_data() { + return Err("Block contains duplicate AttestationData".to_string()); + } - Ok(state_after_ops) + Ok(state.process_attestations(&block.body.attestations)) } pub fn process_block_header(&self, block: &Block) -> Result { @@ -403,16 +392,14 @@ impl State { }) } + #[cfg(feature = "devnet1")] pub fn process_attestations(&self, attestations: &Attestations) -> Self { let mut justifications = self.get_justifications(); let mut latest_justified = self.latest_justified.clone(); let mut latest_finalized = self.latest_finalized.clone(); - // Store initial finalized slot for justifiability checks (per leanSpec) let initial_finalized_slot = self.latest_finalized.slot; let justified_slots = self.justified_slots.clone(); - // PersistentList doesn't expose iter; convert to Vec for simple iteration for now - // Build a temporary Vec by probing sequentially until index error let mut votes_vec: Vec = Vec::new(); let mut i: u64 = 0; loop { @@ -423,116 +410,142 @@ impl State { i += 1; } - // Create mutable working BitList for justified_slots tracking let mut justified_slots_working = Vec::new(); for i in 0..justified_slots.len() { justified_slots_working.push(justified_slots.get(i).map(|b| *b).unwrap_or(false)); } for attestation in votes_vec.iter() { - let vote = attestation.data.clone(); - let target_slot = vote.target.slot; - let source_slot = vote.source.slot; - let target_root = vote.target.root; - let source_root = vote.source.root; - - let target_slot_int = target_slot.0 as usize; - let source_slot_int = source_slot.0 as usize; - - let source_is_justified = justified_slots_working - .get(source_slot_int) - .copied() - .unwrap_or(false); - let target_already_justified = justified_slots_working - .get(target_slot_int) - .copied() - .unwrap_or(false); - - let source_root_matches_history = self - .historical_block_hashes - .get(source_slot_int as u64) - .map(|root| *root == source_root) - .unwrap_or(false); - - let target_root_matches_history = self - .historical_block_hashes - .get(target_slot_int as u64) - .map(|root| *root == target_root) - .unwrap_or(false); - - let target_is_after_source = target_slot > source_slot; - // Use initial_finalized_slot per leanSpec (not the mutating local copy) - let target_is_justifiable = target_slot.is_justifiable_after(initial_finalized_slot); - - // leanSpec logic: skip if BOTH source and target roots don't match history - // i.e., continue if EITHER matches - let roots_valid = source_root_matches_history || target_root_matches_history; - - let is_valid_vote = source_is_justified - && !target_already_justified - && roots_valid - && target_is_after_source - && target_is_justifiable; - - if !is_valid_vote { - continue; - } + self.process_single_attestation( + &attestation.data, + &[attestation.validator_id.0], + &mut justifications, + &mut latest_justified, + &mut latest_finalized, + &mut justified_slots_working, + initial_finalized_slot, + ); + } - if !justifications.contains_key(&target_root) { - // Use actual validator count, not VALIDATOR_REGISTRY_LIMIT - // This matches leanSpec: justifications[target.root] = [Boolean(False)] * self.validators.count - let num_validators = self.validator_count(); - justifications.insert(target_root, vec![false; num_validators]); - } + self.finalize_attestation_processing(justifications, latest_justified, latest_finalized, justified_slots_working) + } - let validator_id = attestation.validator_id.0 as usize; - if let Some(votes) = justifications.get_mut(&target_root) { - if validator_id < votes.len() && !votes[validator_id] { - votes[validator_id] = true; + #[cfg(feature = "devnet2")] + pub fn process_attestations(&self, attestations: &AggregatedAttestations) -> Self { + let mut justifications = self.get_justifications(); + let mut latest_justified = self.latest_justified.clone(); + let mut latest_finalized = self.latest_finalized.clone(); + let initial_finalized_slot = self.latest_finalized.slot; + let justified_slots = self.justified_slots.clone(); + + let mut justified_slots_working = Vec::new(); + for i in 0..justified_slots.len() { + justified_slots_working.push(justified_slots.get(i).map(|b| *b).unwrap_or(false)); + } + + for aggregated_attestation in attestations { + let validator_ids = aggregated_attestation.aggregation_bits.to_validator_indices(); + self.process_single_attestation( + &aggregated_attestation.data, + &validator_ids, + &mut justifications, + &mut latest_justified, + &mut latest_finalized, + &mut justified_slots_working, + initial_finalized_slot, + ); + } - let num_validators = self.validators.len_u64(); + self.finalize_attestation_processing(justifications, latest_justified, latest_finalized, justified_slots_working) + } - let count = votes.iter().filter(|&&v| v).count(); - if 3 * count >= 2 * num_validators as usize { - latest_justified = vote.target; + /// Process a single attestation's votes. + fn process_single_attestation( + &self, + vote: &crate::attestation::AttestationData, + validator_ids: &[u64], + justifications: &mut BTreeMap>, + latest_justified: &mut Checkpoint, + latest_finalized: &mut Checkpoint, + justified_slots_working: &mut Vec, + initial_finalized_slot: Slot, + ) { + let target_slot = vote.target.slot; + let source_slot = vote.source.slot; + let target_root = vote.target.root; + let source_root = vote.source.root; + + let target_slot_int = target_slot.0 as usize; + let source_slot_int = source_slot.0 as usize; + + let source_is_justified = justified_slots_working.get(source_slot_int).copied().unwrap_or(false); + let target_already_justified = justified_slots_working.get(target_slot_int).copied().unwrap_or(false); + + let source_root_matches = self.historical_block_hashes.get(source_slot_int as u64).map(|r| *r == source_root).unwrap_or(false); + let target_root_matches = self.historical_block_hashes.get(target_slot_int as u64).map(|r| *r == target_root).unwrap_or(false); + + let is_valid_vote = source_is_justified + && !target_already_justified + && (source_root_matches || target_root_matches) + && target_slot > source_slot + && target_slot.is_justifiable_after(initial_finalized_slot); + + if !is_valid_vote { + return; + } - // Extend justified_slots_working if needed - while justified_slots_working.len() <= target_slot_int { - justified_slots_working.push(false); - } - justified_slots_working[target_slot_int] = true; + if !justifications.contains_key(&target_root) { + justifications.insert(target_root, vec![false; self.validator_count()]); + } - justifications.remove(&target_root); + for &validator_id in validator_ids { + let vid = validator_id as usize; + if let Some(votes) = justifications.get_mut(&target_root) { + if vid < votes.len() && !votes[vid] { + votes[vid] = true; + } + } + } - let mut is_finalizable = true; - for s in (source_slot_int + 1)..target_slot_int { - // Use initial_finalized_slot per leanSpec - if Slot(s as u64).is_justifiable_after(initial_finalized_slot) { - is_finalizable = false; - break; - } - } + if let Some(votes) = justifications.get(&target_root) { + let num_validators = self.validators.len_u64() as usize; + let count = votes.iter().filter(|&&v| v).count(); + if 3 * count >= 2 * num_validators { + *latest_justified = vote.target.clone(); - if is_finalizable { - latest_finalized = vote.source; - } - } + while justified_slots_working.len() <= target_slot_int { + justified_slots_working.push(false); + } + justified_slots_working[target_slot_int] = true; + + justifications.remove(&target_root); + + let is_finalizable = (source_slot_int + 1..target_slot_int) + .all(|s| !Slot(s as u64).is_justifiable_after(initial_finalized_slot)); + + if is_finalizable { + *latest_finalized = vote.source.clone(); } } } + } + fn finalize_attestation_processing( + &self, + justifications: BTreeMap>, + latest_justified: Checkpoint, + latest_finalized: Checkpoint, + justified_slots_working: Vec, + ) -> Self { let mut new_state = self.clone().with_justifications(justifications); - new_state.latest_justified = latest_justified; new_state.latest_finalized = latest_finalized; - // Convert justified_slots_working Vec back to BitList let mut new_justified_slots = JustifiedSlots::with_length(justified_slots_working.len()); for (i, &val) in justified_slots_working.iter().enumerate() { new_justified_slots.set(i, val); } new_state.justified_slots = new_justified_slots; - new_state } diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index 9fe6abb..a18ac61 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -3,7 +3,7 @@ use containers::{ block::{hash_tree_root, Block, BlockWithAttestation, SignedBlockWithAttestation}, state::State, types::{Bytes32, Uint64}, - Attestation, Attestations, Slot, + Attestation, Slot, }; use pretty_assertions::assert_eq; use rstest::fixture; @@ -31,22 +31,8 @@ fn test_state_transition_full() { // Use process_block_header + process_operations to avoid state root validation during setup let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); - #[cfg(feature = "devnet1")] let expected_state = state_after_header.process_attestations(&block.body.attestations); - #[cfg(feature = "devnet2")] - let expected_state = { - let mut unaggregated_attestations = Attestations::default(); - for aggregated_attestation in &block.body.attestations { - let plain_attestations = aggregated_attestation.to_plain(); - // For each attestatio in the vector, push to the list - for attestation in plain_attestations { - unaggregated_attestations.push(attestation); - } - } - state_after_header.process_attestations(&unaggregated_attestations) - }; - let block_with_correct_root = Block { state_root: hash_tree_root(&expected_state), ..block @@ -79,22 +65,8 @@ fn test_state_transition_invalid_signatures() { // Use process_block_header + process_operations to avoid state root validation during setup let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); - #[cfg(feature = "devnet1")] let expected_state = state_after_header.process_attestations(&block.body.attestations); - #[cfg(feature = "devnet2")] - let expected_state = { - let mut list = Attestations::default(); - for aggregated_attestation in &block.body.attestations { - let plain_attestations = aggregated_attestation.to_plain(); - // For each attestatio in the vector, push to the list - for attestation in plain_attestations { - list.push(attestation); - } - } - list - }; - let block_with_correct_root = Block { state_root: hash_tree_root(&expected_state), ..block @@ -152,21 +124,7 @@ fn test_state_transition_devnet2() { // Process the block header and attestations let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); - #[cfg(feature = "devnet1")] let expected_state = state_after_header.process_attestations(&block.body.attestations); - - #[cfg(feature = "devnet2")] - let expected_state = { - let mut unaggregated_attestations = Attestations::default(); - for aggregated_attestation in &block.body.attestations { - let plain_attestations = aggregated_attestation.to_plain(); - // For each attestatio in the vector, push to the list - for attestation in plain_attestations { - unaggregated_attestations.push(attestation); - } - } - state_after_header.process_attestations(&unaggregated_attestations) - }; // Ensure the state root matches the expected state let block_with_correct_root = Block { From 671715d6f2b146127eb57b114e35860700cc7caf Mon Sep 17 00:00:00 2001 From: LiudasBaronas1 <144480589+LiudasBaronas1@users.noreply.github.com> Date: Mon, 12 Jan 2026 01:16:22 +0200 Subject: [PATCH 06/24] Refactor: remove redundant constants from config, implement ChainConfig::devnet() and update lib exports --- lean_client/chain/src/config.rs | 57 ++++++++++++++------------------- lean_client/chain/src/lib.rs | 3 +- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/lean_client/chain/src/config.rs b/lean_client/chain/src/config.rs index b8be4f1..537d13b 100644 --- a/lean_client/chain/src/config.rs +++ b/lean_client/chain/src/config.rs @@ -3,30 +3,21 @@ pub struct BasisPoint(pub u64); impl BasisPoint { pub const MAX: u64 = 10_000; + pub const fn new(value: u64) -> Option { if value <= Self::MAX { Some(BasisPoint(value)) } else { None } } - #[inline] pub fn get(&self) -> u64 { self.0 } + + #[inline] + pub fn get(&self) -> u64 { self.0 } } -pub const INTERVALS_PER_SLOT: u64 = 4; -pub const SLOT_DURATION_MS: u64 = 4_000; -pub const SECONDS_PER_SLOT: u64 = SLOT_DURATION_MS / 1_000; -pub const SECONDS_PER_INTERVAL: u64 = SECONDS_PER_SLOT / INTERVALS_PER_SLOT; -pub const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3; - -pub const PROPOSER_REORG_CUTOFF_BPS: BasisPoint = match BasisPoint::new(2_500) { Some(x) => x, None => panic!() }; -pub const VOTE_DUE_BPS: BasisPoint = match BasisPoint::new(5_000) { Some(x) => x, None => panic!() }; -pub const FAST_CONFIRM_DUE_BPS: BasisPoint = match BasisPoint::new(7_500) { Some(x) => x, None => panic!() }; -pub const VIEW_FREEZE_CUTOFF_BPS: BasisPoint= match BasisPoint::new(7_500) { Some(x) => x, None => panic!() }; - -pub const HISTORICAL_ROOTS_LIMIT: u64 = 1u64 << 18; -pub const VALIDATOR_REGISTRY_LIMIT: u64 = 1u64 << 12; - #[derive(Clone, Debug)] pub struct ChainConfig { + pub intervals_per_slot: u64, pub slot_duration_ms: u64, pub second_per_slot: u64, + pub seconds_per_interval: u64, pub justification_lookback_slots: u64, pub proposer_reorg_cutoff_bps: BasisPoint, pub vote_due_bps: BasisPoint, @@ -36,24 +27,24 @@ pub struct ChainConfig { pub validator_registry_limit: u64, } -pub const DEVNET_CONFIG: ChainConfig = ChainConfig { - slot_duration_ms: SLOT_DURATION_MS, - second_per_slot: SECONDS_PER_SLOT, - justification_lookback_slots: JUSTIFICATION_LOOKBACK_SLOTS, - proposer_reorg_cutoff_bps: PROPOSER_REORG_CUTOFF_BPS, - vote_due_bps: VOTE_DUE_BPS, - fast_confirm_due_bps: FAST_CONFIRM_DUE_BPS, - view_freeze_cutoff_bps: VIEW_FREEZE_CUTOFF_BPS, - historical_roots_limit: HISTORICAL_ROOTS_LIMIT, - validator_registry_limit: VALIDATOR_REGISTRY_LIMIT, -}; +impl ChainConfig { + pub fn devnet() -> Self { + let slot_duration_ms = 4_000; + let seconds_per_slot = slot_duration_ms / 1_000; + let intervals_per_slot = 4; -#[cfg(test)] -mod tests { - use super::*; - #[test] fn time_math_is_consistent() { - assert_eq!(SLOT_DURATION_MS, 4_000); - assert_eq!(SECONDS_PER_SLOT, 4); - assert_eq!(SECONDS_PER_INTERVAL, 1); + Self { + slot_duration_ms, + second_per_slot: seconds_per_slot, + intervals_per_slot, + seconds_per_interval: seconds_per_slot / intervals_per_slot, + justification_lookback_slots: 3, + proposer_reorg_cutoff_bps: BasisPoint::new(2_500).expect("Valid BPS"), + vote_due_bps: BasisPoint::new(5_000).expect("Valid BPS"), + fast_confirm_due_bps: BasisPoint::new(7_500).expect("Valid BPS"), + view_freeze_cutoff_bps: BasisPoint::new(7_500).expect("Valid BPS"), + historical_roots_limit: 1u64 << 18, + validator_registry_limit: 1u64 << 12, + } } } \ No newline at end of file diff --git a/lean_client/chain/src/lib.rs b/lean_client/chain/src/lib.rs index ef68c36..12cf630 100644 --- a/lean_client/chain/src/lib.rs +++ b/lean_client/chain/src/lib.rs @@ -1 +1,2 @@ -pub mod config; +mod config; +pub use config::ChainConfig; \ No newline at end of file From ab48bc101e35356f9c49cd21f178135037c11dad Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Mon, 12 Jan 2026 23:20:22 +0200 Subject: [PATCH 07/24] =?UTF-8?q?fix:=20use=20env-config/devnet=C4=8D=20in?= =?UTF-8?q?=20devnet2=20feature=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lean_client/fork_choice/Cargo.toml | 2 +- lean_client/networking/Cargo.toml | 2 +- lean_client/validator/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lean_client/fork_choice/Cargo.toml b/lean_client/fork_choice/Cargo.toml index b16f561..badc834 100644 --- a/lean_client/fork_choice/Cargo.toml +++ b/lean_client/fork_choice/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [features] default = [] devnet1 = ["containers/devnet1", "env-config/devnet1"] -devnet2 = ["containers/devnet2", "env-config/devnet1"] +devnet2 = ["containers/devnet2", "env-config/devnet2"] [dependencies] env-config = { path = "../env-config", default-features = false } diff --git a/lean_client/networking/Cargo.toml b/lean_client/networking/Cargo.toml index 8f47702..0584a0e 100644 --- a/lean_client/networking/Cargo.toml +++ b/lean_client/networking/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [features] default = [] devnet1 = ["containers/devnet1", "env-config/devnet1"] -devnet2 = ["containers/devnet2", "env-config/devnet1"] +devnet2 = ["containers/devnet2", "env-config/devnet2"] [dependencies] env-config = { path = "../env-config", default-features = false } diff --git a/lean_client/validator/Cargo.toml b/lean_client/validator/Cargo.toml index 8311b9d..ab09109 100644 --- a/lean_client/validator/Cargo.toml +++ b/lean_client/validator/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" default = ["xmss-signing"] xmss-signing = ["leansig"] devnet1 = ["containers/devnet1", "fork-choice/devnet1", "env-config/devnet1"] -devnet2 = ["containers/devnet2", "fork-choice/devnet2", "env-config/devnet1"] +devnet2 = ["containers/devnet2", "fork-choice/devnet2", "env-config/devnet2"] [dependencies] env-config = { path = "../env-config", default-features = false } From 2842fe421d78d29729d4207d4cb989753c77e948 Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Mon, 12 Jan 2026 23:59:39 +0200 Subject: [PATCH 08/24] Added lean-multisig dependency for signature aggregation --- lean_client/Cargo.lock | 1577 +++++++++++++++++++++-------- lean_client/containers/Cargo.toml | 2 + 2 files changed, 1149 insertions(+), 430 deletions(-) diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index 910d9c1..4173794 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -51,13 +51,25 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "air" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "multilinear-toolkit", + "p3-air", + "p3-util 0.3.0", + "tracing", + "utils", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -66,9 +78,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-primitives" -version = "1.4.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" +checksum = "f6a0fb18dd5fb43ec5f0f6a20be1ce0287c79825827de5744afaa6c957737c33" dependencies = [ "alloy-rlp", "bytes", @@ -76,14 +88,15 @@ dependencies = [ "const-hex", "derive_more", "foldhash 0.2.0", - "hashbrown 0.16.0", - "indexmap 2.11.4", + "hashbrown 0.16.1", + "indexmap 2.13.0", "itoa", "k256 0.13.4", "keccak-asm", "paste", "proptest", "rand 0.9.2", + "rapidhash", "ruint", "rustc-hash", "serde", @@ -110,6 +123,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.21" @@ -142,22 +164,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -169,7 +191,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arithmetic" version = "0.0.0" -source = "git+https://github.com/grandinetech/grandine?branch=develop#5bdc78763c8959ad689c79d51d7d59978460bb1e" +source = "git+https://github.com/grandinetech/grandine?branch=develop#8ac065da176067bc4eb8b79ebfc48a6433f52499" dependencies = [ "easy-ext", "typenum", @@ -260,7 +282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -298,7 +320,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -388,7 +410,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] @@ -400,7 +422,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "synstructure 0.13.2", ] @@ -412,7 +434,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -448,7 +470,7 @@ dependencies = [ "polling", "rustix", "slab", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -459,7 +481,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -514,7 +536,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -523,6 +545,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backend" +version = "0.3.0" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#62766141561550c3540f9f644085fec53d721f16" +dependencies = [ + "fiat-shamir", + "itertools 0.14.0", + "p3-field 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", + "rayon", + "tracing", +] + [[package]] name = "base-x" version = "0.2.11" @@ -565,9 +601,18 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bincode" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] [[package]] name = "bit-set" @@ -598,9 +643,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitvec" @@ -658,9 +703,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byte-slice-cast" @@ -676,18 +721,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.2.38" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "shlex", @@ -695,9 +740,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -758,9 +803,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -768,9 +813,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -787,14 +832,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" @@ -802,6 +847,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -813,9 +867,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6407bff74dea37e0fa3dc1c1c974e5d46405f0c987bf9997a0762adce71eda6" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", "cpufeatures", @@ -837,9 +891,9 @@ checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -855,12 +909,23 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constraints-folder" +version = "0.3.0" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#62766141561550c3540f9f644085fec53d721f16" +dependencies = [ + "fiat-shamir", + "p3-air", + "p3-field 0.3.0", +] + [[package]] name = "containers" version = "0.1.0" dependencies = [ "env-config", "hex", + "lean-multisig", "leansig", "pretty_assertions", "rstest", @@ -875,9 +940,9 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -988,9 +1053,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1030,7 +1095,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1054,7 +1119,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1065,7 +1130,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1084,15 +1149,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "data-encoding-macro" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1100,12 +1165,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1144,12 +1209,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1165,23 +1230,24 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.106", + "rustc_version 0.4.1", + "syn 2.0.114", "unicode-xid", ] @@ -1220,14 +1286,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dyn-clone" @@ -1301,7 +1367,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1377,7 +1443,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1397,7 +1463,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1417,7 +1483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1550,11 +1616,22 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-shamir" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/fiat-shamir.git#bcf23c766f2e930acf11e68777449483a55af077" +dependencies = [ + "p3-challenger 0.3.0", + "p3-field 0.3.0", + "p3-koala-bear 0.3.0", + "serde", +] + [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fixed-hash" @@ -1692,7 +1769,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1756,28 +1833,28 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -1821,9 +1898,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1831,7 +1908,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1866,23 +1943,24 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] name = "hashing" version = "0.0.0" -source = "git+https://github.com/grandinetech/grandine?branch=develop#5bdc78763c8959ad689c79d51d7d59978460bb1e" +source = "git+https://github.com/grandinetech/grandine?branch=develop#8ac065da176067bc4eb8b79ebfc48a6433f52499" dependencies = [ "ethereum-types", "generic-array", "hex-literal", - "sha2 0.10.9 (git+https://github.com/grandinetech/universal-precompiles.git?tag=sha2-v0.10.9-up.1)", + "sha2 0.10.9 (git+https://github.com/grandinetech/universal-precompiles.git?tag=sha2-v0.10.9-up.2)", ] [[package]] @@ -1917,9 +1995,9 @@ dependencies = [ [[package]] name = "hex-literal" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hex_fmt" @@ -1946,7 +2024,7 @@ dependencies = [ "rand 0.9.2", "ring", "socket2 0.5.10", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -1969,7 +2047,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1994,12 +2072,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -2034,9 +2111,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -2056,9 +2133,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "bytes", "futures-channel", @@ -2087,7 +2164,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.0", + "windows-core 0.62.2", ] [[package]] @@ -2147,9 +2224,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -2161,9 +2238,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -2296,7 +2373,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -2312,12 +2389,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2393,15 +2470,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2458,6 +2535,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lean-multisig" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "clap", + "multilinear-toolkit", + "p3-koala-bear 0.3.0", + "poseidon_circuit", + "rec_aggregation", + "whir-p3", + "xmss", +] + [[package]] name = "lean_client" version = "0.1.0" @@ -2467,7 +2558,7 @@ dependencies = [ "containers", "fork-choice", "hex", - "libp2p-identity 0.2.12", + "libp2p-identity 0.2.13", "networking", "tokio", "tracing", @@ -2475,31 +2566,120 @@ dependencies = [ "validator", ] +[[package]] +name = "lean_compiler" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "air", + "lean_vm", + "lookup", + "multilinear-toolkit", + "p3-air", + "p3-challenger 0.3.0", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "pest", + "pest_derive", + "rand 0.9.2", + "sub_protocols", + "tracing", + "utils", + "whir-p3", + "xmss", +] + +[[package]] +name = "lean_prover" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "air", + "itertools 0.14.0", + "lean_compiler", + "lean_vm", + "lookup", + "multilinear-toolkit", + "p3-air", + "p3-challenger 0.3.0", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "pest", + "pest_derive", + "poseidon_circuit", + "rand 0.9.2", + "sub_protocols", + "tracing", + "utils", + "whir-p3", + "witness_generation", + "xmss", +] + +[[package]] +name = "lean_vm" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "air", + "colored", + "derive_more", + "itertools 0.14.0", + "lookup", + "multilinear-toolkit", + "num_enum", + "p3-air", + "p3-challenger 0.3.0", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "pest", + "pest_derive", + "rand 0.9.2", + "strum", + "sub_protocols", + "thiserror 2.0.17", + "tracing", + "utils", + "whir-p3", + "xmss", +] + [[package]] name = "leansig" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanSig?branch=main#d9610e7fbbc75197f134e065df79acc630994706" +source = "git+https://github.com/leanEthereum/leanSig?branch=main#ae12a5feb25d917c42b6466444ebd56ec115a629" dependencies = [ "dashmap", "ethereum_ssz", "num-bigint", "num-traits", - "p3-baby-bear", - "p3-field", - "p3-koala-bear", - "p3-symmetric", + "p3-baby-bear 0.4.1", + "p3-field 0.4.1", + "p3-koala-bear 0.4.1", + "p3-symmetric 0.4.1", "rand 0.9.2", "rayon", "serde", "sha3", - "thiserror 2.0.16", + "thiserror 2.0.17", ] +[[package]] +name = "lib-c" +version = "0.13.0" +source = "git+https://github.com/0xPolygonHermez/zisk.git?tag=v0.13.0#ea1ed4c518992a170fc59ec19f1228eb4829a9e1" + [[package]] name = "libc" -version = "0.2.175" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" @@ -2517,14 +2697,14 @@ dependencies = [ "either", "futures", "futures-timer", - "getrandom 0.2.16", + "getrandom 0.2.17", "libp2p-allow-block-list", "libp2p-connection-limits", - "libp2p-core 0.43.1", + "libp2p-core 0.43.2", "libp2p-dns", "libp2p-gossipsub", "libp2p-identify", - "libp2p-identity 0.2.12", + "libp2p-identity 0.2.13", "libp2p-mdns", "libp2p-metrics", "libp2p-noise", @@ -2537,7 +2717,7 @@ dependencies = [ "multiaddr 0.18.2", "pin-project", "rw-stream-sink 0.4.0", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2546,8 +2726,8 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16ccf824ee859ca83df301e1c0205270206223fd4b1f2e512a693e1912a8f4a" dependencies = [ - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "libp2p-swarm", ] @@ -2557,8 +2737,8 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18b8b607cf3bfa2f8c57db9c7d8569a315d5cc0a282e6bfd5ebfc0a9840b2a0" dependencies = [ - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "libp2p-swarm", ] @@ -2592,15 +2772,15 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.43.1" +version = "0.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d28e2d2def7c344170f5c6450c0dbe3dfef655610dbfde2f6ac28a527abbe36" +checksum = "249128cd37a2199aff30a7675dffa51caf073b51aa612d2f544b19932b9aebca" dependencies = [ "either", "fnv", "futures", "futures-timer", - "libp2p-identity 0.2.12", + "libp2p-identity 0.2.13", "multiaddr 0.18.2", "multihash 0.19.3", "multistream-select 0.13.0", @@ -2609,7 +2789,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "rw-stream-sink 0.4.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "unsigned-varint 0.8.0", "web-time", @@ -2624,8 +2804,8 @@ dependencies = [ "async-trait", "futures", "hickory-resolver", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "parking_lot", "smallvec", "tracing", @@ -2646,11 +2826,11 @@ dependencies = [ "fnv", "futures", "futures-timer", - "getrandom 0.2.16", + "getrandom 0.2.17", "hashlink", "hex_fmt", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", @@ -2672,13 +2852,13 @@ dependencies = [ "futures", "futures-bounded", "futures-timer", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] @@ -2702,9 +2882,9 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3104e13b51e4711ff5738caa1fb54467c8604c2e94d607e27745bcf709068774" +checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" dependencies = [ "asn1_der", "bs58 0.5.1", @@ -2715,7 +2895,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "sha2 0.10.9 (registry+https://github.com/rust-lang/crates.io-index)", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "zeroize", ] @@ -2729,8 +2909,8 @@ dependencies = [ "futures", "hickory-proto", "if-watch", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "libp2p-swarm", "rand 0.8.5", "smallvec", @@ -2746,10 +2926,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "805a555148522cb3414493a5153451910cb1a146c53ffbf4385708349baf62b7" dependencies = [ "futures", - "libp2p-core 0.43.1", + "libp2p-core 0.43.2", "libp2p-gossipsub", "libp2p-identify", - "libp2p-identity 0.2.12", + "libp2p-identity 0.2.13", "libp2p-swarm", "pin-project", "prometheus-client", @@ -2783,15 +2963,15 @@ dependencies = [ "asynchronous-codec 0.7.0", "bytes", "futures", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "multiaddr 0.18.2", "multihash 0.19.3", "quick-protobuf", "rand 0.8.5", "snow", "static_assertions", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "x25519-dalek", "zeroize", @@ -2806,15 +2986,15 @@ dependencies = [ "futures", "futures-timer", "if-watch", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "libp2p-tls", "quinn", "rand 0.8.5", "ring", "rustls", "socket2 0.5.10", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -2828,8 +3008,8 @@ dependencies = [ "async-trait", "futures", "futures-bounded", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "libp2p-swarm", "rand 0.8.5", "smallvec", @@ -2846,8 +3026,8 @@ dependencies = [ "fnv", "futures", "futures-timer", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "libp2p-swarm-derive", "lru", "multistream-select 0.13.0", @@ -2866,7 +3046,7 @@ checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" dependencies = [ "heck", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -2879,7 +3059,7 @@ dependencies = [ "futures-timer", "if-watch", "libc", - "libp2p-core 0.43.1", + "libp2p-core 0.43.2", "socket2 0.5.10", "tokio", "tracing", @@ -2893,13 +3073,13 @@ checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" dependencies = [ "futures", "futures-rustls", - "libp2p-core 0.43.1", - "libp2p-identity 0.2.12", + "libp2p-core 0.43.2", + "libp2p-identity 0.2.13", "rcgen", "ring", "rustls", "rustls-webpki", - "thiserror 2.0.16", + "thiserror 2.0.17", "x509-parser", "yasna", ] @@ -2913,7 +3093,7 @@ dependencies = [ "futures", "futures-timer", "igd-next", - "libp2p-core 0.43.1", + "libp2p-core 0.43.2", "libp2p-swarm", "tokio", "tracing", @@ -2927,8 +3107,8 @@ checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" dependencies = [ "either", "futures", - "libp2p-core 0.43.1", - "thiserror 2.0.16", + "libp2p-core 0.43.2", + "thiserror 2.0.17", "tracing", "yamux 0.12.1", "yamux 0.13.8", @@ -2957,9 +3137,24 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lookup" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "multilinear-toolkit", + "p3-challenger 0.3.0", + "p3-koala-bear 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", + "tracing", + "utils", + "whir-p3", +] [[package]] name = "lru" @@ -2998,9 +3193,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minimal-lexical" @@ -3010,20 +3205,20 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.61.1", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "moka" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -3031,7 +3226,6 @@ dependencies = [ "equivalent", "parking_lot", "portable-atomic", - "rustc_version 0.4.1", "smallvec", "tagptr", "uuid", @@ -3065,7 +3259,7 @@ dependencies = [ "arrayref", "byteorder", "data-encoding", - "libp2p-identity 0.2.12", + "libp2p-identity 0.2.13", "multibase", "multihash 0.19.3", "percent-encoding", @@ -3122,6 +3316,20 @@ dependencies = [ "synstructure 0.12.6", ] +[[package]] +name = "multilinear-toolkit" +version = "0.3.0" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#62766141561550c3540f9f644085fec53d721f16" +dependencies = [ + "backend", + "constraints-folder", + "fiat-shamir", + "p3-field 0.3.0", + "p3-util 0.3.0", + "rayon", + "sumcheck", +] + [[package]] name = "multistream-select" version = "0.12.1" @@ -3198,7 +3406,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3226,7 +3434,7 @@ dependencies = [ "env-config", "futures", "libp2p", - "libp2p-identity 0.2.12", + "libp2p-identity 0.2.13", "libp2p-mplex", "parking_lot", "rand 0.8.5", @@ -3272,7 +3480,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -3320,6 +3528,28 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -3351,29 +3581,104 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "p3-air" +version = "0.3.0" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "p3-field 0.3.0", + "p3-matrix 0.3.0", +] + [[package]] name = "p3-baby-bear" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" dependencies = [ - "p3-field", - "p3-mds", - "p3-monty-31", - "p3-poseidon2", - "p3-symmetric", + "p3-field 0.3.0", + "p3-mds 0.3.0", + "p3-monty-31 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", "rand 0.9.2", ] +[[package]] +name = "p3-baby-bear" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-challenger 0.4.1", + "p3-field 0.4.1", + "p3-mds 0.4.1", + "p3-monty-31 0.4.1", + "p3-poseidon2 0.4.1", + "p3-symmetric 0.4.1", + "rand 0.9.2", +] + +[[package]] +name = "p3-challenger" +version = "0.3.0" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "p3-field 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "tracing", +] + +[[package]] +name = "p3-challenger" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-field 0.4.1", + "p3-maybe-rayon 0.4.1", + "p3-monty-31 0.4.1", + "p3-symmetric 0.4.1", + "p3-util 0.4.1", + "tracing", +] + +[[package]] +name = "p3-commit" +version = "0.3.0" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "itertools 0.14.0", + "p3-challenger 0.3.0", + "p3-dft 0.3.0", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-util 0.3.0", + "serde", +] + [[package]] name = "p3-dft" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" dependencies = [ "itertools 0.14.0", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-util 0.3.0", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "itertools 0.14.0", + "p3-field 0.4.1", + "p3-matrix 0.4.1", + "p3-maybe-rayon 0.4.1", + "p3-util 0.4.1", "spin", "tracing", ] @@ -3381,39 +3686,97 @@ dependencies = [ [[package]] name = "p3-field" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "itertools 0.14.0", + "num-bigint", + "p3-maybe-rayon 0.3.0", + "p3-util 0.3.0", + "paste", + "rand 0.9.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" dependencies = [ "itertools 0.14.0", "num-bigint", - "p3-maybe-rayon", - "p3-util", + "p3-maybe-rayon 0.4.1", + "p3-util 0.4.1", "paste", "rand 0.9.2", "serde", "tracing", ] +[[package]] +name = "p3-interpolation" +version = "0.3.0" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-util 0.3.0", +] + [[package]] name = "p3-koala-bear" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" dependencies = [ - "p3-field", - "p3-monty-31", - "p3-poseidon2", - "p3-symmetric", + "itertools 0.14.0", + "num-bigint", + "p3-field 0.3.0", + "p3-monty-31 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", + "serde", +] + +[[package]] +name = "p3-koala-bear" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-challenger 0.4.1", + "p3-field 0.4.1", + "p3-monty-31 0.4.1", + "p3-poseidon2 0.4.1", + "p3-symmetric 0.4.1", "rand 0.9.2", ] [[package]] name = "p3-matrix" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" dependencies = [ "itertools 0.14.0", - "p3-field", - "p3-maybe-rayon", - "p3-util", + "p3-field 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", + "serde", + "tracing", + "transpose", +] + +[[package]] +name = "p3-matrix" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "itertools 0.14.0", + "p3-field 0.4.1", + "p3-maybe-rayon 0.4.1", + "p3-util 0.4.1", "rand 0.9.2", "serde", "tracing", @@ -3423,35 +3786,94 @@ dependencies = [ [[package]] name = "p3-maybe-rayon" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "rayon", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" [[package]] name = "p3-mds" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "p3-dft 0.3.0", + "p3-field 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", +] + +[[package]] +name = "p3-mds" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" dependencies = [ - "p3-dft", - "p3-field", - "p3-symmetric", - "p3-util", + "p3-dft 0.4.1", + "p3-field 0.4.1", + "p3-symmetric 0.4.1", + "p3-util 0.4.1", "rand 0.9.2", ] +[[package]] +name = "p3-merkle-tree" +version = "0.3.0" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "itertools 0.14.0", + "p3-commit", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", + "serde", + "tracing", +] + [[package]] name = "p3-monty-31" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "itertools 0.14.0", + "num-bigint", + "p3-dft 0.3.0", + "p3-field 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-mds 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "paste", + "rand 0.9.2", + "serde", + "tracing", + "transpose", +] + +[[package]] +name = "p3-monty-31" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" dependencies = [ "itertools 0.14.0", "num-bigint", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", - "p3-util", + "p3-dft 0.4.1", + "p3-field 0.4.1", + "p3-matrix 0.4.1", + "p3-maybe-rayon 0.4.1", + "p3-mds 0.4.1", + "p3-poseidon2 0.4.1", + "p3-symmetric 0.4.1", + "p3-util 0.4.1", "paste", "rand 0.9.2", "serde", @@ -3463,29 +3885,60 @@ dependencies = [ [[package]] name = "p3-poseidon2" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" dependencies = [ - "p3-field", - "p3-mds", - "p3-symmetric", - "p3-util", + "p3-field 0.3.0", + "p3-mds 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", +] + +[[package]] +name = "p3-poseidon2" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" +dependencies = [ + "p3-field 0.4.1", + "p3-mds 0.4.1", + "p3-symmetric 0.4.1", + "p3-util 0.4.1", "rand 0.9.2", ] [[package]] name = "p3-symmetric" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "itertools 0.14.0", + "p3-field 0.3.0", + "serde", +] + +[[package]] +name = "p3-symmetric" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" dependencies = [ "itertools 0.14.0", - "p3-field", + "p3-field 0.4.1", "serde", ] [[package]] name = "p3-util" version = "0.3.0" -source = "git+https://github.com/Plonky3/Plonky3.git?rev=a33a312#a33a31274a5e78bb5fbe3f82ffd2c294e17fa830" +source = "git+https://github.com/TomWambsgans/Plonky3.git?branch=lean-multisig#1db9df28abd6db586eaa891af2416d94d1b026ae" +dependencies = [ + "rayon", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.4.1" +source = "git+https://github.com/Plonky3/Plonky3.git?rev=d421e32#d421e32d3821174ae1f7e528d4bb92b7b18ab295" dependencies = [ "serde", ] @@ -3515,7 +3968,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -3571,14 +4024,47 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.3" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" dependencies = [ "memchr", "ucd-trie", ] +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2 0.10.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -3596,7 +4082,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -3642,7 +4128,7 @@ dependencies = [ "hermit-abi", "pin-project-lite", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -3670,9 +4156,25 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "poseidon_circuit" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "multilinear-toolkit", + "p3-koala-bear 0.3.0", + "p3-monty-31 0.3.0", + "p3-poseidon2 0.3.0", + "rand 0.9.2", + "sub_protocols", + "tracing", + "utils", + "whir-p3", +] [[package]] name = "potential_utf" @@ -3766,9 +4268,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -3793,19 +4295,18 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "proptest" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.4", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -3859,7 +4360,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.1", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -3872,7 +4373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", @@ -3880,7 +4381,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3902,9 +4403,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -3939,7 +4440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.4", "serde", ] @@ -3960,7 +4461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.4", ] [[package]] @@ -3969,16 +4470,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "serde", ] @@ -3988,7 +4489,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.4", +] + +[[package]] +name = "rapidhash" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8b5b858a440a0bc02625b62dd95131b9201aa9f69f411195dd4a7cfb1de3d7" +dependencies = [ + "rustversion", ] [[package]] @@ -4024,40 +4534,68 @@ dependencies = [ "yasna", ] +[[package]] +name = "rec_aggregation" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "air", + "bincode", + "lean_compiler", + "lean_prover", + "lean_vm", + "lookup", + "multilinear-toolkit", + "p3-air", + "p3-challenger 0.3.0", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", + "serde", + "serde_json", + "sub_protocols", + "tracing", + "utils", + "whir-p3", + "xmss", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -4067,9 +4605,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -4078,9 +4616,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "relative-path" @@ -4096,9 +4634,9 @@ checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" [[package]] name = "resolv-conf" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "rfc6979" @@ -4129,7 +4667,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -4170,7 +4708,7 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.1", - "syn 2.0.106", + "syn 2.0.114", "unicode-ident", ] @@ -4194,9 +4732,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.0" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -4267,22 +4805,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", "ring", @@ -4294,9 +4832,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "web-time", "zeroize", @@ -4355,9 +4893,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "schemars" @@ -4373,9 +4911,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -4443,9 +4981,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -4453,42 +4991,42 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "serde_utils" version = "0.0.0" -source = "git+https://github.com/grandinetech/grandine?branch=develop#5bdc78763c8959ad689c79d51d7d59978460bb1e" +source = "git+https://github.com/grandinetech/grandine?branch=develop#8ac065da176067bc4eb8b79ebfc48a6433f52499" dependencies = [ "const-hex", "generic-array", @@ -4503,19 +5041,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.2.0", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -4523,14 +5060,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -4539,7 +5076,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "itoa", "ryu", "serde", @@ -4573,11 +5110,12 @@ dependencies = [ [[package]] name = "sha2" version = "0.10.9" -source = "git+https://github.com/grandinetech/universal-precompiles.git?tag=sha2-v0.10.9-up.1#dab12204de2e6a40f7a4c93b59347f60174c6953" +source = "git+https://github.com/grandinetech/universal-precompiles.git?tag=sha2-v0.10.9-up.2#7d57ea01cd5fe5f6458142ce6ac269cc44b425bd" dependencies = [ "cfg-if", "cpufeatures", "digest 0.10.7", + "ziskos", ] [[package]] @@ -4617,10 +5155,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -4731,7 +5270,7 @@ dependencies = [ [[package]] name = "ssz" version = "0.0.0" -source = "git+https://github.com/grandinetech/grandine?branch=develop#5bdc78763c8959ad689c79d51d7d59978460bb1e" +source = "git+https://github.com/grandinetech/grandine?branch=develop#8ac065da176067bc4eb8b79ebfc48a6433f52499" dependencies = [ "arithmetic", "bit_field", @@ -4754,7 +5293,7 @@ dependencies = [ "static_assertions", "std_ext", "tap", - "thiserror 2.0.16", + "thiserror 2.0.17", "triomphe", "try_from_iterator", "typenum", @@ -4763,7 +5302,7 @@ dependencies = [ [[package]] name = "ssz_derive" version = "0.0.0" -source = "git+https://github.com/grandinetech/grandine?branch=develop#5bdc78763c8959ad689c79d51d7d59978460bb1e" +source = "git+https://github.com/grandinetech/grandine?branch=develop#8ac065da176067bc4eb8b79ebfc48a6433f52499" dependencies = [ "darling", "easy-ext", @@ -4771,7 +5310,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -4801,9 +5340,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -4814,7 +5353,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "std_ext" version = "0.0.0" -source = "git+https://github.com/grandinetech/grandine?branch=develop#5bdc78763c8959ad689c79d51d7d59978460bb1e" +source = "git+https://github.com/grandinetech/grandine?branch=develop#8ac065da176067bc4eb8b79ebfc48a6433f52499" dependencies = [ "easy-ext", "triomphe", @@ -4832,12 +5371,61 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "sub_protocols" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "derive_more", + "lookup", + "multilinear-toolkit", + "p3-util 0.3.0", + "tracing", + "utils", + "whir-p3", +] + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "sumcheck" +version = "0.3.0" +source = "git+https://github.com/leanEthereum/multilinear-toolkit.git#62766141561550c3540f9f644085fec53d721f16" +dependencies = [ + "backend", + "constraints-folder", + "fiat-shamir", + "p3-air", + "p3-field 0.3.0", + "p3-util 0.3.0", + "rayon", +] + [[package]] name = "syn" version = "1.0.109" @@ -4851,9 +5439,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -4880,7 +5468,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -4889,7 +5477,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation", "system-configuration-sys", ] @@ -4918,15 +5506,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -4940,11 +5528,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -4955,18 +5543,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -5045,9 +5633,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -5057,7 +5645,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.1", "tokio-macros", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -5068,14 +5656,14 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5095,20 +5683,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "toml_datetime", "toml_parser", "winnow", @@ -5116,9 +5704,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -5131,9 +5719,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -5142,25 +5730,38 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", ] +[[package]] +name = "tracing-forest" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92bdb3c949c9e81b71f78ba782f956b896019d82cc2f31025d21e04adab4d695" +dependencies = [ + "ansi_term", + "smallvec", + "thiserror 2.0.17", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -5174,9 +5775,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -5202,9 +5803,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" dependencies = [ "serde", "stable_deref_trait", @@ -5219,7 +5820,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "try_from_iterator" version = "0.0.0" -source = "git+https://github.com/grandinetech/grandine?branch=develop#5bdc78763c8959ad689c79d51d7d59978460bb1e" +source = "git+https://github.com/grandinetech/grandine?branch=develop#8ac065da176067bc4eb8b79ebfc48a6433f52499" [[package]] name = "typenum" @@ -5253,9 +5854,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -5309,9 +5910,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -5331,13 +5932,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utils" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "multilinear-toolkit", + "p3-air", + "p3-challenger 0.3.0", + "p3-koala-bear 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "tracing", + "tracing-forest", + "tracing-subscriber", +] + [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -5398,15 +6016,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -5418,9 +6027,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -5429,25 +6038,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5455,22 +6050,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -5485,12 +6080,61 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whir-p3" +version = "0.1.0" +source = "git+https://github.com/TomWambsgans/whir-p3?branch=lean-multisig#04fb1c1f2e3bbd14e6e4aee32621656eb3f3949f" +dependencies = [ + "itertools 0.14.0", + "multilinear-toolkit", + "p3-baby-bear 0.3.0", + "p3-challenger 0.3.0", + "p3-commit", + "p3-dft 0.3.0", + "p3-field 0.3.0", + "p3-interpolation", + "p3-koala-bear 0.3.0", + "p3-matrix 0.3.0", + "p3-maybe-rayon 0.3.0", + "p3-merkle-tree", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", + "rayon", + "thiserror 2.0.17", + "tracing", + "tracing-forest", + "tracing-subscriber", +] + [[package]] name = "widestring" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.53.0" @@ -5513,44 +6157,44 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.62.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", - "windows-result 0.4.0", + "windows-result 0.4.1", "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" @@ -5563,18 +6207,18 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -5597,20 +6241,29 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] @@ -5648,9 +6301,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.1", @@ -5803,9 +6456,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -5826,6 +6479,35 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "witness_generation" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "air", + "derive_more", + "lean_compiler", + "lean_vm", + "lookup", + "multilinear-toolkit", + "p3-air", + "p3-challenger 0.3.0", + "p3-koala-bear 0.3.0", + "p3-monty-31 0.3.0", + "p3-poseidon2 0.3.0", + "p3-symmetric 0.3.0", + "p3-util 0.3.0", + "pest", + "pest_derive", + "poseidon_circuit", + "rand 0.9.2", + "sub_protocols", + "tracing", + "utils", + "whir-p3", + "xmss", +] + [[package]] name = "writeable" version = "0.6.2" @@ -5866,7 +6548,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] @@ -5885,6 +6567,19 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "xmss" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig?branch=main#72c27460314770dee435adf80494b2d684d3959b" +dependencies = [ + "multilinear-toolkit", + "p3-koala-bear 0.3.0", + "p3-util 0.3.0", + "rand 0.9.2", + "sha3", + "utils", +] + [[package]] name = "yamux" version = "0.12.1" @@ -5950,28 +6645,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "synstructure 0.13.2", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -5991,7 +6686,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "synstructure 0.13.2", ] @@ -6006,13 +6701,13 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -6045,5 +6740,27 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] + +[[package]] +name = "ziskos" +version = "0.13.0" +source = "git+https://github.com/0xPolygonHermez/zisk.git?tag=v0.13.0#ea1ed4c518992a170fc59ec19f1228eb4829a9e1" +dependencies = [ + "cfg-if", + "getrandom 0.2.17", + "lazy_static", + "lib-c", + "num-bigint", + "num-traits", + "rand 0.8.5", + "static_assertions", + "tiny-keccak", +] + +[[package]] +name = "zmij" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index 29e8ecd..e290ca4 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [features] xmss-verify = ["leansig"] +multisig = ["dep:lean-multisig"] default = [] devnet1 = ["env-config/devnet1"] devnet2 = ["env-config/devnet2"] @@ -24,6 +25,7 @@ serde_yaml = "0.9" hex = "0.4.3" sha2 = "0.10" leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "main", optional = true } +lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig", branch = "main", optional = true } [dev-dependencies] rstest = "0.18" From 72e22b28f0d859eff195cead7e826ba87aead27a Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Tue, 13 Jan 2026 00:06:40 +0200 Subject: [PATCH 09/24] Added MultisigAggregatedSignature struct with helper methods --- lean_client/containers/Cargo.toml | 3 +-- lean_client/containers/src/attestation.rs | 27 +++++++++++++++++++++++ lean_client/containers/src/lib.rs | 3 +++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index e290ca4..fff5b70 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -5,10 +5,9 @@ edition = "2021" [features] xmss-verify = ["leansig"] -multisig = ["dep:lean-multisig"] default = [] devnet1 = ["env-config/devnet1"] -devnet2 = ["env-config/devnet2"] +devnet2 = ["env-config/devnet2", "dep:lean-multisig"] [lib] name = "containers" diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index b4f5c9a..d0e0b70 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -31,6 +31,33 @@ pub type AttestationSignatures = ssz::PersistentList; +/// Aggregated signature proof from lean-multisig zkVM. +/// This wraps the serialized proof bytes from `xmss_aggregate_signatures()`. +#[cfg(feature = "devnet2")] +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct MultisigAggregatedSignature { + /// The serialized zkVM proof bytes from lean-multisig aggregation. + pub proof: Vec, +} + +#[cfg(feature = "devnet2")] +impl MultisigAggregatedSignature { + /// Create a new MultisigAggregatedSignature from proof bytes. + pub fn new(proof: Vec) -> Self { + Self { proof } + } + + /// Get the proof bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.proof + } + + /// Check if the signature is empty (no proof). + pub fn is_empty(&self) -> bool { + self.proof.is_empty() + } +} + /// Bitlist representing validator participation in an attestation. /// Limit is VALIDATOR_REGISTRY_LIMIT (4096). #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index c73a9f9..8b94ee1 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -13,6 +13,9 @@ pub use attestation::{ AggregatedAttestation, AggregatedSignatures, AggregationBits, Attestation, AttestationData, Attestations, Signature, SignedAggregatedAttestation, SignedAttestation, }; + +#[cfg(feature = "devnet2")] +pub use attestation::MultisigAggregatedSignature; pub use block::{ Block, BlockBody, BlockHeader, BlockWithAttestation, SignedBlock, SignedBlockWithAttestation, }; From 3c71a5872277d463978bf845382c33ce1a704dd4 Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Tue, 13 Jan 2026 00:30:40 +0200 Subject: [PATCH 10/24] add verify_aggregated_payload method to MultisigAggregatedSignature --- lean_client/containers/src/attestation.rs | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index d0e0b70..89d03be 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -56,6 +56,38 @@ impl MultisigAggregatedSignature { pub fn is_empty(&self) -> bool { self.proof.is_empty() } + + /// Verify the aggregated signature proof against the given public keys and message. + /// This uses the lean-multisig zkVM to verify that the aggregated proof is valid + /// for all the given public keys signing the same message at the given epoch. + /// + /// `Ok(())` if the proof is valid, `Err` with the proof error otherwise. + #[cfg(feature = "xmss-verify")] + pub fn verify_aggregated_payload( + &self, + public_keys: &[lean_multisig::XmssPublicKey], + message: [lean_multisig::F; 8], + epoch: u64, + ) -> Result<(), lean_multisig::ProofError> { + lean_multisig::xmss_verify_aggregated_signatures( + public_keys, + message, + &self.proof, + epoch, + ) + } + + /// Stub verification when xmss-verify feature is disabled. + /// Always returns Ok(()) for testing without cryptographic verification. + #[cfg(not(feature = "xmss-verify"))] + pub fn verify_aggregated_payload( + &self, + _public_keys: &[P], + _message: M, + _epoch: u64, + ) -> Result<(), ()> { + Ok(()) + } } /// Bitlist representing validator participation in an attestation. From 192ff6e235c472830b7a29a6a1a411d715d202fc Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Tue, 13 Jan 2026 00:43:02 +0200 Subject: [PATCH 11/24] Fix: moved proposer signature verification outside attestation loop --- lean_client/containers/src/block.rs | 76 +++++++++++++++-------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 0acf1b2..3bb3cae 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -216,6 +216,9 @@ impl SignedBlockWithAttestation { true } + + /// Currently uses naive per-signature verification. In a future update, + /// this will use `MultisigAggregatedSignature::verify_aggregated_payload()` #[cfg(feature = "devnet2")] pub fn verify_signatures(&self, parent_state: State) -> bool { // Unpack the signed block components @@ -228,13 +231,16 @@ impl SignedBlockWithAttestation { assert_eq!( aggregated_attestations.len_u64(), attestation_signatures.len_u64(), - "Number of signatures does not match number of attestations" + "Attestation signature groups must align with block body attestations" ); let validators = &parent_state.validators; let num_validators = validators.len_u64(); - // Verify each attestation signature + // Verify each aggregated attestation's signatures + // + // TODO: Replace with MultisigAggregatedSignature::verify_aggregated_payload() + // once AttestationSignatures type is updated to use zkVM proofs. for (aggregated_attestation, aggregated_signature) in (&aggregated_attestations) .into_iter() .zip((&attestation_signatures).into_iter()) @@ -249,24 +255,22 @@ impl SignedBlockWithAttestation { "Aggregated attestation signature count mismatch" ); - let attestation_root = aggregated_attestation.data.hash_tree_root(); - - // Loop through zipped validator IDs and their corresponding signatures - // Verify each individual signature within the aggregated attestation - for (validator_id, signature) in - validator_ids.iter().zip(aggregated_signature.into_iter()) - { - // Ensure validator exists in the active set + // Ensure all validators exist in the active set + for validator_id in &validator_ids { assert!( *validator_id < num_validators, "Validator index out of range" ); + } - let validator = validators.get(*validator_id).expect("validator must exist"); + let attestation_root: [u8; 32] = + hash_tree_root(&aggregated_attestation.data).0.into(); - // Get the actual payload root for the attestation data - let attestation_root: [u8; 32] = - hash_tree_root(&aggregated_attestation.data).0.into(); + // Verify each individual signature within the aggregated attestation + for (validator_id, signature) in + validator_ids.iter().zip(aggregated_signature.into_iter()) + { + let validator = validators.get(*validator_id).expect("validator must exist"); // Verify the XMSS signature assert!( @@ -279,31 +283,31 @@ impl SignedBlockWithAttestation { "Attestation signature verification failed" ); } + } - // Verify the proposer attestation signature - let proposer_attestation = self.message.proposer_attestation.clone(); - let proposer_signature = signatures.proposer_signature; + // Verify the proposer attestation signature (outside the attestation loop) + let proposer_attestation = &self.message.proposer_attestation; + let proposer_signature = &signatures.proposer_signature; - assert!( - proposer_attestation.validator_id.0 < num_validators, - "Proposer index out of range" - ); - - let proposer = validators - .get(proposer_attestation.validator_id.0) - .expect("proposer must exist"); + assert!( + proposer_attestation.validator_id.0 < num_validators, + "Proposer index out of range" + ); - let proposer_root: [u8; 32] = hash_tree_root(&proposer_attestation).0.into(); - assert!( - verify_xmss_signature( - proposer.pubkey.0.as_bytes(), - proposer_attestation.data.slot, - &proposer_root, - &proposer_signature, - ), - "Proposer attestation signature verification failed" - ); - } + let proposer = validators + .get(proposer_attestation.validator_id.0) + .expect("proposer must exist"); + + let proposer_root: [u8; 32] = hash_tree_root(proposer_attestation).0.into(); + assert!( + verify_xmss_signature( + proposer.pubkey.0.as_bytes(), + proposer_attestation.data.slot, + &proposer_root, + proposer_signature, + ), + "Proposer attestation signature verification failed" + ); true } From eac81dd8a2d115a7efa1cc628df6d8593f23909f Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Tue, 13 Jan 2026 23:24:11 +0200 Subject: [PATCH 12/24] Changes to cfg, made devnet1/devnet2 features mutually exclusive to avoid. Changed AttestationSignatures to use MultisigAggregatedSignature. --- lean_client/containers/src/attestation.rs | 36 +++++++----- lean_client/containers/src/block.rs | 55 ++++++++----------- lean_client/containers/src/serde_helpers.rs | 41 +++++++++++++- lean_client/containers/src/state.rs | 4 +- lean_client/fork_choice/src/handlers.rs | 30 +++++----- lean_client/fork_choice/src/store.rs | 2 +- lean_client/networking/src/network/service.rs | 4 +- lean_client/networking/src/types.rs | 2 +- lean_client/src/main.rs | 14 ++--- lean_client/validator/src/lib.rs | 30 +++++----- 10 files changed, 127 insertions(+), 91 deletions(-) diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 89d03be..2bb45b4 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -3,7 +3,10 @@ use serde::{Deserialize, Serialize}; use ssz::BitList; use ssz::ByteVector; use ssz_derive::Ssz; -use typenum::{Prod, Sum, U100, U12, U31}; +use typenum::{Prod, Sum, U100, U12, U31, U1024}; + +// Type-level number for 1 MiB (1048576 = 1024 * 1024) +pub type U1048576 = Prod; pub type U3100 = Prod; @@ -22,39 +25,46 @@ pub type Attestations = ssz::PersistentList; pub type AggregatedAttestations = ssz::PersistentList; -#[cfg(feature = "devnet1")] +#[cfg(not(feature = "devnet2"))] pub type AttestationSignatures = ssz::PersistentList; #[cfg(feature = "devnet2")] -pub type AttestationSignatures = ssz::PersistentList; +pub type AttestationSignatures = ssz::PersistentList; +/// Legacy naive aggregated signature type (list of individual XMSS signatures). +/// Kept for backwards compatibility but no longer used in wire format. #[cfg(feature = "devnet2")] pub type NaiveAggregatedSignature = ssz::PersistentList; /// Aggregated signature proof from lean-multisig zkVM. -/// This wraps the serialized proof bytes from `xmss_aggregate_signatures()`. +/// +/// This is a variable-length byte list (up to 1 MiB) containing the serialized +/// proof bytes from `xmss_aggregate_signatures()`. The `#[ssz(transparent)]` +/// attribute makes this type serialize directly as a ByteList for SSZ wire format. #[cfg(feature = "devnet2")] -#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct MultisigAggregatedSignature { +#[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] +#[ssz(transparent)] +pub struct MultisigAggregatedSignature( /// The serialized zkVM proof bytes from lean-multisig aggregation. - pub proof: Vec, -} + #[serde(with = "crate::serde_helpers::byte_list")] + pub ssz::ByteList, +); #[cfg(feature = "devnet2")] impl MultisigAggregatedSignature { /// Create a new MultisigAggregatedSignature from proof bytes. pub fn new(proof: Vec) -> Self { - Self { proof } + Self(ssz::ByteList::try_from(proof).expect("proof exceeds 1 MiB limit")) } /// Get the proof bytes. pub fn as_bytes(&self) -> &[u8] { - &self.proof + self.0.as_bytes() } /// Check if the signature is empty (no proof). pub fn is_empty(&self) -> bool { - self.proof.is_empty() + self.0.as_bytes().is_empty() } /// Verify the aggregated signature proof against the given public keys and message. @@ -72,7 +82,7 @@ impl MultisigAggregatedSignature { lean_multisig::xmss_verify_aggregated_signatures( public_keys, message, - &self.proof, + self.0.as_bytes(), epoch, ) } @@ -174,7 +184,7 @@ pub struct SignedAttestation { pub validator_id: u64, #[cfg(feature = "devnet2")] pub message: AttestationData, - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] pub message: Attestation, pub signature: Signature, } diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 3bb3cae..fdf3ca6 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -17,7 +17,7 @@ use crate::validator::BlsPublicKey; pub struct BlockBody { #[cfg(feature = "devnet2")] pub attestations: AggregatedAttestations, - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] #[serde(with = "crate::serde_helpers")] pub attestations: Attestations, } @@ -67,7 +67,7 @@ pub struct SignedBlockWithAttestation { /// Aggregated signature payload for the block. /// /// Signatures remain in attestation order followed by the proposer signature. - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] #[serde(with = "crate::serde_helpers::block_signatures")] pub signature: PersistentList, #[cfg(feature = "devnet2")] @@ -128,7 +128,7 @@ impl SignedBlockWithAttestation { /// /// - Spec: /// - XMSS Library: - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] pub fn verify_signatures(&self, parent_state: State) -> bool { // Unpack the signed block components let block = &self.message.block; @@ -217,8 +217,9 @@ impl SignedBlockWithAttestation { } - /// Currently uses naive per-signature verification. In a future update, - /// this will use `MultisigAggregatedSignature::verify_aggregated_payload()` + /// Verifies all attestation signatures using lean-multisig aggregated proofs. + /// Each attestation has a single `MultisigAggregatedSignature` proof that covers + /// all participating validators. #[cfg(feature = "devnet2")] pub fn verify_signatures(&self, parent_state: State) -> bool { // Unpack the signed block components @@ -237,10 +238,7 @@ impl SignedBlockWithAttestation { let validators = &parent_state.validators; let num_validators = validators.len_u64(); - // Verify each aggregated attestation's signatures - // - // TODO: Replace with MultisigAggregatedSignature::verify_aggregated_payload() - // once AttestationSignatures type is updated to use zkVM proofs. + // Verify each aggregated attestation's zkVM proof for (aggregated_attestation, aggregated_signature) in (&aggregated_attestations) .into_iter() .zip((&attestation_signatures).into_iter()) @@ -249,12 +247,6 @@ impl SignedBlockWithAttestation { .aggregation_bits .to_validator_indices(); - assert_eq!( - aggregated_signature.len_u64(), - validator_ids.len() as u64, - "Aggregated attestation signature count mismatch" - ); - // Ensure all validators exist in the active set for validator_id in &validator_ids { assert!( @@ -263,26 +255,23 @@ impl SignedBlockWithAttestation { ); } - let attestation_root: [u8; 32] = + let attestation_data_root: [u8; 32] = hash_tree_root(&aggregated_attestation.data).0.into(); - // Verify each individual signature within the aggregated attestation - for (validator_id, signature) in - validator_ids.iter().zip(aggregated_signature.into_iter()) - { - let validator = validators.get(*validator_id).expect("validator must exist"); - - // Verify the XMSS signature - assert!( - verify_xmss_signature( - validator.pubkey.0.as_bytes(), - aggregated_attestation.data.slot, - &attestation_root, - signature, - ), - "Attestation signature verification failed" - ); - } + // Verify the lean-multisig aggregated proof for this attestation + // + // The proof verifies that all validators in aggregation_bits signed + // the same attestation_data_root at the given epoch (slot). + aggregated_signature + .verify_aggregated_payload( + &validator_ids + .iter() + .map(|vid| validators.get(*vid).expect("validator must exist")) + .collect::>(), + &attestation_data_root, + aggregated_attestation.data.slot.0, + ) + .expect("Attestation aggregated signature verification failed"); } // Verify the proposer attestation signature (outside the attestation loop) diff --git a/lean_client/containers/src/serde_helpers.rs b/lean_client/containers/src/serde_helpers.rs index 01604e5..55298da 100644 --- a/lean_client/containers/src/serde_helpers.rs +++ b/lean_client/containers/src/serde_helpers.rs @@ -249,7 +249,7 @@ pub mod block_signatures { Signature::try_from(bytes.as_slice()).map_err(|_| "Failed to create signature".to_string()) } - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] pub fn deserialize<'de, D>( deserializer: D, ) -> Result, D::Error> @@ -284,7 +284,7 @@ pub mod block_signatures { )) } - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] pub fn serialize( value: &PersistentList, serializer: S, @@ -319,3 +319,40 @@ pub mod block_signatures { )) } } + +/// Serde helper for ssz::ByteList - serializes as hex string +pub mod byte_list { + use super::*; + use ssz::ByteList; + use typenum::Unsigned; + + pub fn deserialize<'de, D, N>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + N: Unsigned, + { + use serde::de::Error; + + let hex_str = String::deserialize(deserializer)?; + let hex_str = hex_str.trim_start_matches("0x"); + + if hex_str.is_empty() { + return Ok(ByteList::default()); + } + + let bytes = hex::decode(hex_str) + .map_err(|e| D::Error::custom(format!("Invalid hex string: {}", e)))?; + + ByteList::try_from(bytes) + .map_err(|_| D::Error::custom("ByteList exceeds maximum length")) + } + + pub fn serialize(value: &ByteList, serializer: S) -> Result + where + S: Serializer, + N: Unsigned, + { + let hex_str = format!("0x{}", hex::encode(value.as_bytes())); + hex_str.serialize(serializer) + } +} diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index d6c7cf8..4de68d6 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -392,7 +392,7 @@ impl State { }) } - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] pub fn process_attestations(&self, attestations: &Attestations) -> Self { let mut justifications = self.get_justifications(); let mut latest_justified = self.latest_justified.clone(); @@ -570,7 +570,7 @@ impl State { /// # Returns /// /// Tuple of (Block, post-State, collected attestations, signatures) - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] pub fn build_block( &self, slot: Slot, diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index fa9aa89..e914019 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -25,13 +25,13 @@ pub fn on_attestation( signed_attestation: SignedAttestation, is_from_block: bool, ) -> Result<(), String> { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let validator_id = ValidatorIndex(signed_attestation.message.validator_id.0); - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let attestation_slot = signed_attestation.message.data.slot; - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let source_slot = signed_attestation.message.data.source.slot; - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let target_slot = signed_attestation.message.data.target.slot; @@ -63,7 +63,7 @@ pub fn on_attestation( if is_from_block { // On-chain attestation processing - immediately becomes "known" - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] if store .latest_known_attestations .get(&validator_id) @@ -91,7 +91,7 @@ pub fn on_attestation( // Remove from new attestations if superseded if let Some(existing_new) = store.latest_new_attestations.get(&validator_id) { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] if existing_new.message.data.slot <= attestation_slot { store.latest_new_attestations.remove(&validator_id); } @@ -102,7 +102,7 @@ pub fn on_attestation( } } else { // Network gossip attestation processing - goes to "new" stage - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] if store .latest_new_attestations .get(&validator_id) @@ -194,7 +194,7 @@ fn process_block_internal( let attestations = &signed_block.message.block.body.attestations; let signatures = &signed_block.signature; - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] { for i in 0.. { match (attestations.get(i), signatures.get(i)) { @@ -238,13 +238,11 @@ fn process_block_internal( #[cfg(feature = "devnet2")] { let aggregated_attestations = &signed_block.message.block.body.attestations; - let attestation_signatures = &signed_block.signature.attestation_signatures; let proposer_attestation = &signed_block.message.proposer_attestation; - for (aggregated_attestation, aggregated_signature) in aggregated_attestations - .into_iter() - .zip(attestation_signatures) - { + // Process each aggregated attestation's validators for fork choice + // Note: Signature verification is done in verify_signatures() before on_block() + for aggregated_attestation in aggregated_attestations.into_iter() { let validator_ids: Vec = aggregated_attestation .aggregation_bits.0 .iter() @@ -253,13 +251,15 @@ fn process_block_internal( .map(|(index, _)| index as u64) .collect(); - for (validator_id, signature) in validator_ids.into_iter().zip(aggregated_signature) { + // Each validator in the aggregation votes for this attestation data + for validator_id in validator_ids { on_attestation( store, SignedAttestation { validator_id, message: aggregated_attestation.data.clone(), - signature: *signature, + // Use a default signature since verification already happened + signature: containers::Signature::default(), }, true, )?; diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 3296d06..c48fe61 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -85,7 +85,7 @@ pub fn get_fork_choice_head( // stage 1: accumulate weights by walking up from each attestation's head for attestation in latest_attestations.values() { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let mut curr = attestation.message.data.head.root; #[cfg(feature = "devnet2")] let mut curr = attestation.message.head.root; diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 93e749c..f062f63 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -311,7 +311,7 @@ where } } Ok(GossipsubMessage::Attestation(signed_attestation)) => { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let slot = signed_attestation.message.data.slot.0; #[cfg(feature = "devnet2")] let slot = signed_attestation.message.slot.0; @@ -524,7 +524,7 @@ where } } OutboundP2pRequest::GossipAttestation(signed_attestation) => { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let slot = signed_attestation.message.data.slot.0; #[cfg(feature = "devnet2")] let slot = signed_attestation.message.slot.0; diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index 028a883..fab4260 100644 --- a/lean_client/networking/src/types.rs +++ b/lean_client/networking/src/types.rs @@ -93,7 +93,7 @@ impl Display for ChainMessage { ChainMessage::ProcessBlock { signed_block_with_attestation, .. } => { write!(f, "ProcessBlockWithAttestation(slot={})", signed_block_with_attestation.message.block.slot.0) } - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] ChainMessage::ProcessAttestation { signed_attestation, .. } => { write!(f, "ProcessAttestation(slot={})", signed_attestation.message.data.slot.0) } diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index d1c3e24..172bd90 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -220,7 +220,7 @@ async fn main() { block: genesis_block, proposer_attestation: genesis_proposer_attestation, }, - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] signature: PersistentList::default(), #[cfg(feature = "devnet2")] signature: BlockSignatures { @@ -434,7 +434,7 @@ async fn main() { if last_attestation_slot != Some(current_slot) { let attestations = vs.create_attestations(&store, Slot(current_slot)); for signed_att in attestations { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let validator_id = signed_att.message.validator_id.0; #[cfg(feature = "devnet2")] let validator_id = signed_att.validator_id; @@ -444,7 +444,7 @@ async fn main() { "Broadcasting attestation" ); - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] match on_attestation(&mut store, signed_att.clone(), false) { Ok(()) => { if let Err(e) = chain_outbound_sender.send( @@ -552,13 +552,13 @@ async fn main() { should_gossip, .. } => { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let att_slot = signed_attestation.message.data.slot.0; - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let source_slot = signed_attestation.message.data.source.slot.0; - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let target_slot = signed_attestation.message.data.target.slot.0; - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let validator_id = signed_attestation.message.validator_id.0; #[cfg(feature = "devnet2")] diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 2c65fa7..93e9429 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -4,7 +4,7 @@ use std::path::Path; use containers::attestation::{AggregatedAttestations}; #[cfg(feature = "devnet2")] -use containers::attestation::{NaiveAggregatedSignature}; +use containers::attestation::MultisigAggregatedSignature; use containers::block::BlockSignatures; use containers::{ attestation::{Attestation, AttestationData, Signature, SignedAttestation}, @@ -176,7 +176,7 @@ impl ValidatorService { .latest_new_attestations .values() .filter(|att| { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let data = &att.message.data; #[cfg(feature = "devnet2")] let data = &att.message; @@ -191,7 +191,7 @@ impl ValidatorService { }) .collect(); - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let valid_attestations: Vec = valid_signed_attestations .iter() .map(|att| att.message.clone()) @@ -211,7 +211,7 @@ impl ValidatorService { ); // Build block with collected attestations (empty body - attestations go to state) - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let (block, _post_state, _collected_atts, sigs) = parent_state.build_block( slot, proposer_index, @@ -240,19 +240,20 @@ impl ValidatorService { }; // Collect signatures from the attestations we included - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let mut signatures = sigs; #[cfg(feature = "devnet2")] let mut signatures = sigs.attestation_signatures; for signed_att in &valid_signed_attestations { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] signatures .push(signed_att.signature.clone()) .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; #[cfg(feature = "devnet2")] { - // TODO: Use real aggregation instead of naive placeholder when spec is more up to date - let aggregated_sig: NaiveAggregatedSignature = NaiveAggregatedSignature::default(); + // TODO: Use real lean-multisig aggregation once we have individual signatures + // For now, use an empty proof placeholder that verify_aggregated_payload will accept + let aggregated_sig = MultisigAggregatedSignature::default(); signatures .push(aggregated_sig) .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; @@ -276,15 +277,14 @@ impl ValidatorService { match key_manager.sign(proposer_index.0, epoch, &message.0.into()) { Ok(sig) => { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] signatures .push(sig) .map_err(|e| format!("Failed to add proposer signature: {:?}", e))?; #[cfg(feature = "devnet2")] { - // TODO: Use real aggregation instead of naive placeholder when spec is more up to date - let aggregated_sig: NaiveAggregatedSignature = - NaiveAggregatedSignature::default(); + // TODO: Use real lean-multisig aggregation for proposer attestation + let aggregated_sig = MultisigAggregatedSignature::default(); signatures .push(aggregated_sig) .map_err(|e| format!("Failed to add proposer signature: {:?}", e))?; @@ -305,7 +305,7 @@ impl ValidatorService { block, proposer_attestation, }, - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] signature: signatures, #[cfg(feature = "devnet2")] signature: BlockSignatures { @@ -351,7 +351,7 @@ impl ValidatorService { .validator_indices .iter() .filter_map(|&idx| { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] let attestation = Attestation { validator_id: Uint64(idx), data: AttestationData { @@ -408,7 +408,7 @@ impl ValidatorService { }; { - #[cfg(feature = "devnet1")] + #[cfg(not(feature = "devnet2"))] { Some(SignedAttestation { message: attestation, From f891e2a2b53653634a45e1ee3ccb7d1cd31b3ad7 Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Tue, 13 Jan 2026 23:40:28 +0200 Subject: [PATCH 13/24] Added store infrastructure for signature aggregation. --- lean_client/containers/src/attestation.rs | 25 ++++++++++ lean_client/containers/src/lib.rs | 2 +- lean_client/containers/src/state.rs | 2 +- .../containers/tests/test_vectors/mod.rs | 3 ++ .../containers/tests/unit_tests/common.rs | 3 ++ .../containers/tests/unit_tests/mod.rs | 11 ++++- .../tests/unit_tests/state_basic.rs | 3 ++ .../tests/unit_tests/state_justifications.rs | 3 ++ .../tests/unit_tests/state_process.rs | 3 ++ .../tests/unit_tests/state_transition.rs | 3 ++ lean_client/fork_choice/src/handlers.rs | 47 +++++++++++++++---- lean_client/fork_choice/src/store.rs | 12 +++++ .../tests/fork_choice_test_vectors.rs | 3 ++ .../fork_choice/tests/unit_tests/votes.rs | 3 ++ 14 files changed, 111 insertions(+), 12 deletions(-) diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 2bb45b4..291a8f0 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -167,6 +167,31 @@ pub struct AttestationData { pub source: Checkpoint, } +impl AttestationData { + /// Compute the data root bytes for signature lookup. + /// This is the hash tree root of the attestation data. + pub fn data_root_bytes(&self) -> crate::Bytes32 { + crate::Bytes32(ssz::SszHash::hash_tree_root(self)) + } +} + +/// Key for looking up individual validator signatures. +/// Used to index signature caches by (validator, attestation_data_root) pairs. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SignatureKey { + /// The validator who produced the signature. + pub validator_id: u64, + /// The hash of the signed attestation data. + pub data_root: crate::Bytes32, +} + +impl SignatureKey { + /// Create a new signature key. + pub fn new(validator_id: u64, data_root: crate::Bytes32) -> Self { + Self { validator_id, data_root } + } +} + /// Validator specific attestation wrapping shared attestation data. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index 8b94ee1..4945252 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -11,7 +11,7 @@ pub mod validator; pub use attestation::{ AggregatedAttestation, AggregatedSignatures, AggregationBits, Attestation, AttestationData, - Attestations, Signature, SignedAggregatedAttestation, SignedAttestation, + Attestations, Signature, SignatureKey, SignedAggregatedAttestation, SignedAttestation, }; #[cfg(feature = "devnet2")] diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 4de68d6..2c09215 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -706,7 +706,7 @@ impl State { } } -#[cfg(test)] +#[cfg(all(test, not(feature = "devnet2")))] mod tests { use super::*; #[test] diff --git a/lean_client/containers/tests/test_vectors/mod.rs b/lean_client/containers/tests/test_vectors/mod.rs index 8859847..f29ca74 100644 --- a/lean_client/containers/tests/test_vectors/mod.rs +++ b/lean_client/containers/tests/test_vectors/mod.rs @@ -1,3 +1,6 @@ +//! Test vector modules for devnet1 format +#![cfg(not(feature = "devnet2"))] + // Test vector modules pub mod runner; pub mod block_processing; diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 26fa0a5..9d618d6 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -1,3 +1,6 @@ +//! Common test utilities for devnet1 format +#![cfg(not(feature = "devnet2"))] + use containers::{Attestation, Attestations, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators, AggregatedAttestation, Signature}; use ssz::{PersistentList}; use typenum::U4096; diff --git a/lean_client/containers/tests/unit_tests/mod.rs b/lean_client/containers/tests/unit_tests/mod.rs index b9f442f..c02c404 100644 --- a/lean_client/containers/tests/unit_tests/mod.rs +++ b/lean_client/containers/tests/unit_tests/mod.rs @@ -1,7 +1,14 @@ // tests/unit_tests/mod.rs -mod common; + +// Modules that work with both devnet1 and devnet2 mod state_basic; mod state_justifications; +mod attestation_aggregation; + +// Modules that are only compatible with devnet1 format +#[cfg(not(feature = "devnet2"))] +mod common; +#[cfg(not(feature = "devnet2"))] mod state_process; +#[cfg(not(feature = "devnet2"))] mod state_transition; -mod attestation_aggregation; diff --git a/lean_client/containers/tests/unit_tests/state_basic.rs b/lean_client/containers/tests/unit_tests/state_basic.rs index 5fa16e1..d83fc91 100644 --- a/lean_client/containers/tests/unit_tests/state_basic.rs +++ b/lean_client/containers/tests/unit_tests/state_basic.rs @@ -1,3 +1,6 @@ +//! State basic tests for devnet1 format +#![cfg(not(feature = "devnet2"))] + // tests/state_basic.rs use containers::{block::{BlockBody, hash_tree_root}, state::State, types::Uint64, ValidatorIndex}; use pretty_assertions::assert_eq; diff --git a/lean_client/containers/tests/unit_tests/state_justifications.rs b/lean_client/containers/tests/unit_tests/state_justifications.rs index 9a7b0cc..74dce9e 100644 --- a/lean_client/containers/tests/unit_tests/state_justifications.rs +++ b/lean_client/containers/tests/unit_tests/state_justifications.rs @@ -1,3 +1,6 @@ +//! State justifications tests for devnet1 format +#![cfg(not(feature = "devnet2"))] + // tests/state_justifications.rs use containers::{ state::State, diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index afc1887..459c853 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -1,3 +1,6 @@ +//! State process tests for devnet1 format +#![cfg(not(feature = "devnet2"))] + // tests/state_process.rs use containers::{ block::{Block, BlockBody, hash_tree_root}, diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index a18ac61..53ec667 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -1,3 +1,6 @@ +//! State transition tests for devnet1 format +#![cfg(not(feature = "devnet2"))] + // tests/state_transition.rs use containers::{ block::{hash_tree_root, Block, BlockWithAttestation, SignedBlockWithAttestation}, diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index e914019..a4d24c4 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -2,6 +2,8 @@ use crate::store::*; use containers::{ attestation::SignedAttestation, block::SignedBlockWithAttestation, Bytes32, ValidatorIndex, }; +#[cfg(feature = "devnet2")] +use containers::SignatureKey; use ssz::SszHash; #[inline] @@ -116,16 +118,24 @@ pub fn on_attestation( } #[cfg(feature = "devnet2")] - if store - .latest_new_attestations - .get(&validator_id) - .map_or(true, |existing| { - existing.message.slot < attestation_slot - }) { - store + // Store signature for later aggregation during block building + let data_root = signed_attestation.message.data_root_bytes(); + let sig_key = SignatureKey::new(signed_attestation.validator_id, data_root); + store.gossip_signatures.insert(sig_key, signed_attestation.signature.clone()); + + // Track attestation for fork choice + if store .latest_new_attestations - .insert(validator_id, signed_attestation); + .get(&validator_id) + .map_or(true, |existing| { + existing.message.slot < attestation_slot + }) + { + store + .latest_new_attestations + .insert(validator_id, signed_attestation); + } } } Ok(()) @@ -239,6 +249,27 @@ fn process_block_internal( { let aggregated_attestations = &signed_block.message.block.body.attestations; let proposer_attestation = &signed_block.message.proposer_attestation; + + // Store aggregated proofs for future block building + // Each attestation_signature proof is indexed by (validator_id, data_root) for each participating validator + for (att_idx, aggregated_attestation) in aggregated_attestations.into_iter().enumerate() { + let data_root = aggregated_attestation.data.data_root_bytes(); + + // Get the corresponding proof from attestation_signatures + if let Ok(proof) = signatures.attestation_signatures.get(att_idx as u64) { + // Store proof for each validator in the aggregation + for (bit_idx, bit) in aggregated_attestation.aggregation_bits.0.iter().enumerate() { + if *bit { + let validator_id = bit_idx as u64; + let sig_key = SignatureKey::new(validator_id, data_root); + store.aggregated_payloads + .entry(sig_key) + .or_insert_with(Vec::new) + .push(proof.clone()); + } + } + } + } // Process each aggregated attestation's validators for fork choice // Note: Signature verification is done in verify_signatures() before on_block() diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index c48fe61..5fa6b9b 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -3,6 +3,8 @@ use containers::{ block::SignedBlockWithAttestation, checkpoint::Checkpoint, config::Config, state::State, Bytes32, Root, Slot, ValidatorIndex, }; +#[cfg(feature = "devnet2")] +use containers::{MultisigAggregatedSignature, Signature, SignatureKey}; use ssz::SszHash; use std::collections::HashMap; pub type Interval = u64; @@ -23,6 +25,12 @@ pub struct Store { pub latest_known_attestations: HashMap, pub latest_new_attestations: HashMap, pub blocks_queue: HashMap>, + + #[cfg(feature = "devnet2")] + pub gossip_signatures: HashMap, + + #[cfg(feature = "devnet2")] + pub aggregated_payloads: HashMap>, } pub fn get_forkchoice_store( @@ -63,6 +71,10 @@ pub fn get_forkchoice_store( latest_known_attestations: HashMap::new(), latest_new_attestations: HashMap::new(), blocks_queue: HashMap::new(), + #[cfg(feature = "devnet2")] + gossip_signatures: HashMap::new(), + #[cfg(feature = "devnet2")] + aggregated_payloads: HashMap::new(), } } diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index 50bd240..7c92780 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -1,3 +1,6 @@ + +#![cfg(not(feature = "devnet2"))] + use fork_choice::{ handlers::{on_attestation, on_block, on_tick}, store::{get_forkchoice_store, Store}, diff --git a/lean_client/fork_choice/tests/unit_tests/votes.rs b/lean_client/fork_choice/tests/unit_tests/votes.rs index 4a1b688..7b94932 100644 --- a/lean_client/fork_choice/tests/unit_tests/votes.rs +++ b/lean_client/fork_choice/tests/unit_tests/votes.rs @@ -1,3 +1,6 @@ + +#![cfg(not(feature = "devnet2"))] + use super::common::create_test_store; use fork_choice::handlers::on_attestation; use fork_choice::store::{accept_new_attestations, INTERVALS_PER_SLOT}; From 9fbbf348f0fa8ae55278cf6c55394f22e149cae6 Mon Sep 17 00:00:00 2001 From: Darius Spr <108625236+Dariusspr@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:57:59 +0200 Subject: [PATCH 14/24] rename to PublicKey and use constant for key size --- lean_client/containers/src/block.rs | 1 - lean_client/containers/src/state.rs | 2 +- lean_client/containers/src/validator.rs | 56 +++++++++++-------- .../tests/fork_choice_test_vectors.rs | 2 +- lean_client/src/main.rs | 4 +- lean_client/validator/src/keys.rs | 2 +- 6 files changed, 37 insertions(+), 30 deletions(-) diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 0acf1b2..00f5894 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -7,7 +7,6 @@ use leansig::signature::generalized_xmss::instantiations_poseidon::lifetime_2_to use ssz::{PersistentList, SszHash}; use typenum::U4096; use crate::attestation::{AggregatedAttestations, AttestationSignatures}; -use crate::validator::BlsPublicKey; /// The body of a block, containing payload data. /// diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index b176e55..5e3dd52 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -107,7 +107,7 @@ impl State { let mut validators = List::default(); for i in 0..num_validators.0 { let validator = Validator { - pubkey: crate::validator::BlsPublicKey::default(), + pubkey: crate::validator::PublicKey::default(), index: Uint64(i), }; validators.push(validator).expect("Failed to add validator"); diff --git a/lean_client/containers/src/validator.rs b/lean_client/containers/src/validator.rs index 513b09d..29f5716 100644 --- a/lean_client/containers/src/validator.rs +++ b/lean_client/containers/src/validator.rs @@ -1,21 +1,24 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use ssz::ByteVector; use ssz_derive::Ssz; -use typenum::U52; +use typenum::{Unsigned, U52}; -/// BLS public key - 52 bytes (as defined in lean spec) +/// Size of XMSS public keys in bytes (as defined in lean spec) +pub type PublicKeySize = U52; + +/// XMSS public key (as defined in lean spec) #[derive(Clone, Debug, PartialEq, Eq, Ssz)] #[ssz(transparent)] -pub struct BlsPublicKey(pub ByteVector); +pub struct PublicKey(pub ByteVector); -impl Default for BlsPublicKey { +impl Default for PublicKey { fn default() -> Self { - BlsPublicKey(ByteVector::default()) + PublicKey(ByteVector::default()) } } // Custom serde implementation -impl Serialize for BlsPublicKey { +impl Serialize for PublicKey { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -24,8 +27,8 @@ impl Serialize for BlsPublicKey { // For now, use unsafe to access the underlying bytes let bytes = unsafe { std::slice::from_raw_parts( - &self.0 as *const ByteVector as *const u8, - 52 + &self.0 as *const ByteVector as *const u8, + PublicKeySize::USIZE, ) }; let hex_string = format!("0x{}", hex::encode(bytes)); @@ -33,52 +36,57 @@ impl Serialize for BlsPublicKey { } } -impl<'de> Deserialize<'de> for BlsPublicKey { +impl<'de> Deserialize<'de> for PublicKey { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; let s = s.strip_prefix("0x").unwrap_or(&s); - + let decoded = hex::decode(s).map_err(serde::de::Error::custom)?; - if decoded.len() != 52 { + if decoded.len() != PublicKeySize::USIZE { return Err(serde::de::Error::custom(format!( - "Expected 52 bytes, got {}", + "Expected {} bytes, got {}", + PublicKeySize::USIZE, decoded.len() ))); } - + // Create ByteVector from decoded bytes using unsafe let mut byte_vec = ByteVector::default(); unsafe { - let dest = &mut byte_vec as *mut ByteVector as *mut u8; - std::ptr::copy_nonoverlapping(decoded.as_ptr(), dest, 52); + let dest = &mut byte_vec as *mut ByteVector as *mut u8; + std::ptr::copy_nonoverlapping(decoded.as_ptr(), dest, PublicKeySize::USIZE); } - - Ok(BlsPublicKey(byte_vec)) + + Ok(PublicKey(byte_vec)) } } -impl BlsPublicKey { +impl PublicKey { pub fn from_hex(s: &str) -> Result { let s = s.strip_prefix("0x").unwrap_or(s); let decoded = hex::decode(s).map_err(|e| e.to_string())?; - if decoded.len() != 52 { - return Err(format!("Expected 52 bytes, got {}", decoded.len())); + if decoded.len() != PublicKeySize::USIZE { + return Err(format!( + "Expected {} bytes, got {}", + PublicKeySize::USIZE, + decoded.len() + )); } let mut byte_vec = ByteVector::default(); unsafe { - let dest = &mut byte_vec as *mut ByteVector as *mut u8; - std::ptr::copy_nonoverlapping(decoded.as_ptr(), dest, 52); + let dest = &mut byte_vec as *mut ByteVector as *mut u8; + std::ptr::copy_nonoverlapping(decoded.as_ptr(), dest, PublicKeySize::USIZE); } - Ok(BlsPublicKey(byte_vec)) + Ok(PublicKey(byte_vec)) } } #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] pub struct Validator { - pub pubkey: BlsPublicKey, + pub pubkey: PublicKey, #[serde(default)] pub index: crate::Uint64, } diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index 50bd240..4f8521f 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -384,7 +384,7 @@ fn initialize_state_from_test(test_state: &TestAnchorState) -> State { let mut validators = List::default(); for test_validator in &test_state.validators.data { - let pubkey = containers::validator::BlsPublicKey::from_hex(&test_validator.pubkey) + let pubkey = containers::validator::PublicKey::from_hex(&test_validator.pubkey) .expect("Failed to parse validator pubkey"); let validator = containers::validator::Validator { pubkey, diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index d1c3e24..ad2276e 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -164,7 +164,7 @@ async fn main() { .iter() .enumerate() .map(|(i, v_str)| { - let pubkey = containers::validator::BlsPublicKey::from_hex(v_str) + let pubkey = containers::validator::PublicKey::from_hex(v_str) .expect("Invalid genesis validator pubkey"); containers::validator::Validator { pubkey, @@ -178,7 +178,7 @@ async fn main() { let num_validators = 3; let validators = (0..num_validators) .map(|i| containers::validator::Validator { - pubkey: containers::validator::BlsPublicKey::default(), + pubkey: containers::validator::PublicKey::default(), index: Uint64(i as u64), }) .collect(); diff --git a/lean_client/validator/src/keys.rs b/lean_client/validator/src/keys.rs index 13c0deb..cae38f1 100644 --- a/lean_client/validator/src/keys.rs +++ b/lean_client/validator/src/keys.rs @@ -93,7 +93,7 @@ impl KeyManager { ).into()); } - // Convert to ByteVector using unsafe pointer copy (same pattern as BlsPublicKey) + // Convert to ByteVector using unsafe pointer copy (same pattern as PublicKey) let mut byte_vec: ByteVector = ByteVector::default(); unsafe { let dest = &mut byte_vec as *mut ByteVector as *mut u8; From 830a70c9935d52fa0bbe7fafdaa381bc09942716 Mon Sep 17 00:00:00 2001 From: Darius Spr <108625236+Dariusspr@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:23:13 +0200 Subject: [PATCH 15/24] hide key size --- lean_client/containers/src/validator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lean_client/containers/src/validator.rs b/lean_client/containers/src/validator.rs index 29f5716..8a2da60 100644 --- a/lean_client/containers/src/validator.rs +++ b/lean_client/containers/src/validator.rs @@ -4,7 +4,7 @@ use ssz_derive::Ssz; use typenum::{Unsigned, U52}; /// Size of XMSS public keys in bytes (as defined in lean spec) -pub type PublicKeySize = U52; +type PublicKeySize = U52; /// XMSS public key (as defined in lean spec) #[derive(Clone, Debug, PartialEq, Eq, Ssz)] From 572d54578a8d83439631624734110dedc2a4e2eb Mon Sep 17 00:00:00 2001 From: Darius Spr <108625236+Dariusspr@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:34:01 +0200 Subject: [PATCH 16/24] format code --- lean_client/chain/src/config.rs | 18 +- lean_client/chain/src/lib.rs | 2 +- lean_client/containers/src/attestation.rs | 2 +- lean_client/containers/src/checkpoint.rs | 6 +- lean_client/containers/src/config.rs | 4 +- lean_client/containers/src/lib.rs | 4 +- lean_client/containers/src/slot.rs | 45 +-- lean_client/containers/src/state.rs | 26 +- lean_client/containers/src/status.rs | 2 +- lean_client/containers/src/types.rs | 2 +- .../containers/tests/debug_deserialize.rs | 43 ++- lean_client/containers/tests/main.rs | 2 +- .../tests/test_vectors/block_processing.rs | 9 +- .../containers/tests/test_vectors/genesis.rs | 9 +- .../containers/tests/test_vectors/mod.rs | 17 +- .../containers/tests/test_vectors/runner.rs | 314 +++++++++++------- .../tests/test_vectors/verify_signatures.rs | 6 +- .../unit_tests/attestation_aggregation.rs | 30 +- .../containers/tests/unit_tests/common.rs | 54 +-- .../containers/tests/unit_tests/mod.rs | 2 +- .../tests/unit_tests/state_basic.rs | 22 +- .../tests/unit_tests/state_justifications.rs | 32 +- .../tests/unit_tests/state_process.rs | 66 +++- .../tests/unit_tests/state_transition.rs | 2 +- lean_client/env-config/src/lib.rs | 2 +- lean_client/fork_choice/src/handlers.rs | 14 +- lean_client/fork_choice/src/store.rs | 8 +- .../tests/fork_choice_test_vectors.rs | 16 +- lean_client/fork_choice/tests/unit_tests.rs | 2 +- .../fork_choice/tests/unit_tests/common.rs | 18 +- .../tests/unit_tests/fork_choice.rs | 8 +- .../fork_choice/tests/unit_tests/time.rs | 9 +- .../fork_choice/tests/unit_tests/votes.rs | 213 ++++++++---- .../networking/src/gossipsub/config.rs | 4 +- .../networking/src/gossipsub/message.rs | 5 +- .../networking/src/gossipsub/tests/config.rs | 12 +- .../networking/src/gossipsub/tests/message.rs | 4 +- .../src/gossipsub/tests/message_id.rs | 6 +- .../networking/src/gossipsub/tests/mod.rs | 4 +- .../networking/src/gossipsub/tests/topic.rs | 4 +- lean_client/networking/src/gossipsub/topic.rs | 14 +- lean_client/networking/src/network/mod.rs | 2 +- lean_client/networking/src/network/service.rs | 34 +- lean_client/networking/src/req_resp.rs | 74 +++-- lean_client/networking/src/types.rs | 35 +- lean_client/validator/src/keys.rs | 29 +- lean_client/validator/src/lib.rs | 4 +- 47 files changed, 777 insertions(+), 463 deletions(-) diff --git a/lean_client/chain/src/config.rs b/lean_client/chain/src/config.rs index 537d13b..1d762de 100644 --- a/lean_client/chain/src/config.rs +++ b/lean_client/chain/src/config.rs @@ -3,13 +3,19 @@ pub struct BasisPoint(pub u64); impl BasisPoint { pub const MAX: u64 = 10_000; - + pub const fn new(value: u64) -> Option { - if value <= Self::MAX { Some(BasisPoint(value)) } else { None } + if value <= Self::MAX { + Some(BasisPoint(value)) + } else { + None + } + } + + #[inline] + pub fn get(&self) -> u64 { + self.0 } - - #[inline] - pub fn get(&self) -> u64 { self.0 } } #[derive(Clone, Debug)] @@ -47,4 +53,4 @@ impl ChainConfig { validator_registry_limit: 1u64 << 12, } } -} \ No newline at end of file +} diff --git a/lean_client/chain/src/lib.rs b/lean_client/chain/src/lib.rs index 12cf630..9496841 100644 --- a/lean_client/chain/src/lib.rs +++ b/lean_client/chain/src/lib.rs @@ -1,2 +1,2 @@ mod config; -pub use config::ChainConfig; \ No newline at end of file +pub use config::ChainConfig; diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 9c3537c..6779b0f 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -126,7 +126,7 @@ pub struct AggregatedAttestation { /// Bitfield indicating which validators participated in the aggregation. pub aggregation_bits: AggregationBits, /// Combined attestation data similar to the beacon chain format. - /// + /// /// Multiple validator attestations are aggregated here without the complexity of /// committee assignments. pub data: AttestationData, diff --git a/lean_client/containers/src/checkpoint.rs b/lean_client/containers/src/checkpoint.rs index e635ab1..1b36f31 100644 --- a/lean_client/containers/src/checkpoint.rs +++ b/lean_client/containers/src/checkpoint.rs @@ -1,9 +1,9 @@ use crate::{Bytes32, Slot}; -use ssz_derive::Ssz; use serde::{Deserialize, Serialize}; +use ssz_derive::Ssz; /// Represents a checkpoint in the chain's history. -/// +/// /// A checkpoint marks a specific moment in the chain. It combines a block /// identifier with a slot number. Checkpoints are used for justification and /// finalization. @@ -45,4 +45,4 @@ mod tests { }; assert_eq!(cp1, cp2); } -} \ No newline at end of file +} diff --git a/lean_client/containers/src/config.rs b/lean_client/containers/src/config.rs index 83bf459..fed2b7e 100644 --- a/lean_client/containers/src/config.rs +++ b/lean_client/containers/src/config.rs @@ -1,5 +1,5 @@ -use ssz_derive::Ssz; use serde::{Deserialize, Serialize}; +use ssz_derive::Ssz; use std::fs::File; use std::io::BufReader; use std::path::Path; @@ -26,4 +26,4 @@ impl GenesisConfig { let config = serde_yaml::from_reader(reader)?; Ok(config) } -} \ No newline at end of file +} diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index c73a9f9..f0590ca 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -22,8 +22,8 @@ pub use slot::Slot; pub use state::State; pub use status::Status; pub use types::{ - Bytes32, HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, Validators, - Uint64, ValidatorIndex, + Bytes32, HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, + Uint64, ValidatorIndex, Validators, }; pub use types::Bytes32 as Root; diff --git a/lean_client/containers/src/slot.rs b/lean_client/containers/src/slot.rs index d845ec3..17f5439 100644 --- a/lean_client/containers/src/slot.rs +++ b/lean_client/containers/src/slot.rs @@ -1,5 +1,5 @@ -use ssz_derive::Ssz; use serde::{Deserialize, Serialize}; +use ssz_derive::Ssz; use std::cmp::Ordering; #[derive(Clone, Copy, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] @@ -37,15 +37,18 @@ impl Slot { /// /// Panics if this slot is earlier than the finalized slot. pub fn is_justifiable_after(self, finalized: Slot) -> bool { - assert!(self >= finalized, "Candidate slot must not be before finalized slot"); + assert!( + self >= finalized, + "Candidate slot must not be before finalized slot" + ); let delta = self.0 - finalized.0; - + // Rule 1: The first 5 slots after finalization are always justifiable. // Examples: delta = 0, 1, 2, 3, 4, 5 if delta <= 5 { return true; } - + // Rule 2: Slots at perfect square distances are justifiable. // Examples: delta = 1, 4, 9, 16, 25, 36, 49, 64, ... // Check: integer square root squared equals delta @@ -53,7 +56,7 @@ impl Slot { if sqrt * sqrt == delta { return true; } - + // Rule 3: Slots at pronic number distances are justifiable. // Pronic numbers have the form n(n+1): 2, 6, 12, 20, 30, 42, 56, ... // Mathematical insight: For pronic delta = n(n+1), we have: @@ -64,7 +67,7 @@ impl Slot { if test_sqrt * test_sqrt == test && test_sqrt % 2 == 1 { return true; } - + false } } @@ -89,32 +92,32 @@ mod tests { fn test_is_justifiable_perfect_squares() { let finalized = Slot(0); // Rule 2: Perfect square distances - assert!(Slot(1).is_justifiable_after(finalized)); // delta = 1 = 1^2 - assert!(Slot(4).is_justifiable_after(finalized)); // delta = 4 = 2^2 - assert!(Slot(9).is_justifiable_after(finalized)); // delta = 9 = 3^2 - assert!(Slot(16).is_justifiable_after(finalized)); // delta = 16 = 4^2 - assert!(Slot(25).is_justifiable_after(finalized)); // delta = 25 = 5^2 - assert!(Slot(36).is_justifiable_after(finalized)); // delta = 36 = 6^2 + assert!(Slot(1).is_justifiable_after(finalized)); // delta = 1 = 1^2 + assert!(Slot(4).is_justifiable_after(finalized)); // delta = 4 = 2^2 + assert!(Slot(9).is_justifiable_after(finalized)); // delta = 9 = 3^2 + assert!(Slot(16).is_justifiable_after(finalized)); // delta = 16 = 4^2 + assert!(Slot(25).is_justifiable_after(finalized)); // delta = 25 = 5^2 + assert!(Slot(36).is_justifiable_after(finalized)); // delta = 36 = 6^2 } #[test] fn test_is_justifiable_pronic() { let finalized = Slot(0); // Rule 3: Pronic numbers (n(n+1)) - assert!(Slot(2).is_justifiable_after(finalized)); // delta = 2 = 1*2 - assert!(Slot(6).is_justifiable_after(finalized)); // delta = 6 = 2*3 - assert!(Slot(12).is_justifiable_after(finalized)); // delta = 12 = 3*4 - assert!(Slot(20).is_justifiable_after(finalized)); // delta = 20 = 4*5 - assert!(Slot(30).is_justifiable_after(finalized)); // delta = 30 = 5*6 - assert!(Slot(42).is_justifiable_after(finalized)); // delta = 42 = 6*7 + assert!(Slot(2).is_justifiable_after(finalized)); // delta = 2 = 1*2 + assert!(Slot(6).is_justifiable_after(finalized)); // delta = 6 = 2*3 + assert!(Slot(12).is_justifiable_after(finalized)); // delta = 12 = 3*4 + assert!(Slot(20).is_justifiable_after(finalized)); // delta = 20 = 4*5 + assert!(Slot(30).is_justifiable_after(finalized)); // delta = 30 = 5*6 + assert!(Slot(42).is_justifiable_after(finalized)); // delta = 42 = 6*7 } #[test] fn test_is_not_justifiable() { let finalized = Slot(0); // Not justifiable: not in first 5, not perfect square, not pronic - assert!(!Slot(7).is_justifiable_after(finalized)); // delta = 7 - assert!(!Slot(8).is_justifiable_after(finalized)); // delta = 8 + assert!(!Slot(7).is_justifiable_after(finalized)); // delta = 7 + assert!(!Slot(8).is_justifiable_after(finalized)); // delta = 8 assert!(!Slot(10).is_justifiable_after(finalized)); // delta = 10 assert!(!Slot(11).is_justifiable_after(finalized)); // delta = 11 } @@ -126,4 +129,4 @@ mod tests { let candidate = Slot(50); candidate.is_justifiable_after(finalized); } -} \ No newline at end of file +} diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 5e3dd52..5056fb7 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -1,5 +1,11 @@ +use crate::attestation::AggregatedAttestations; +use crate::block::BlockSignatures; use crate::validator::Validator; -use crate::{block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, Attestation, Attestations, Bytes32, Checkpoint, Config, Signature, SignedAttestation, Slot, Uint64, ValidatorIndex}; +use crate::{ + block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, + Attestation, Attestations, Bytes32, Checkpoint, Config, Signature, SignedAttestation, Slot, + Uint64, ValidatorIndex, +}; use crate::{ HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, Validators, }; @@ -8,8 +14,6 @@ use ssz::{PersistentList as List, PersistentList}; use ssz_derive::Ssz; use std::collections::BTreeMap; use typenum::U4096; -use crate::attestation::AggregatedAttestations; -use crate::block::BlockSignatures; pub const VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const JUSTIFICATION_ROOTS_LIMIT: usize = 1 << 18; // 262144 @@ -305,7 +309,9 @@ impl State { let plain_attestations = aggregated_attestation.to_plain(); // For each attestatio in the vector, push to the list for attestation in plain_attestations { - unaggregated_attestations.push(attestation).map_err(|e| format!("Failed to push attestation: {:?}", e))?; + unaggregated_attestations + .push(attestation) + .map_err(|e| format!("Failed to push attestation: {:?}", e))?; } } state.process_attestations(&unaggregated_attestations) @@ -566,7 +572,15 @@ impl State { initial_attestations: Option>, available_signed_attestations: Option<&[SignedBlockWithAttestation]>, known_block_roots: Option<&std::collections::HashSet>, - ) -> Result<(Block, Self, Vec, PersistentList), String> { + ) -> Result< + ( + Block, + Self, + Vec, + PersistentList, + ), + String, + > { // Initialize empty attestation set for iterative collection let mut attestations = initial_attestations.unwrap_or_default(); let mut signatures = PersistentList::default(); @@ -703,7 +717,7 @@ mod tests { config: st.config.clone(), ..st.clone() } - .is_proposer(ValidatorIndex(0))); + .is_proposer(ValidatorIndex(0))); } #[test] diff --git a/lean_client/containers/src/status.rs b/lean_client/containers/src/status.rs index da05ba1..d68c7c3 100644 --- a/lean_client/containers/src/status.rs +++ b/lean_client/containers/src/status.rs @@ -1,6 +1,6 @@ use crate::Checkpoint; -use ssz_derive::Ssz; use serde::{Deserialize, Serialize}; +use ssz_derive::Ssz; #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct Status { diff --git a/lean_client/containers/src/types.rs b/lean_client/containers/src/types.rs index bb2a64b..7d9aa4d 100644 --- a/lean_client/containers/src/types.rs +++ b/lean_client/containers/src/types.rs @@ -38,8 +38,8 @@ impl fmt::Display for Bytes32 { } // Type-level constants for SSZ collection limits -use typenum::{Prod, U4, U1000, U4096, U262144, U1073741824}; use crate::validator::Validator; +use typenum::{Prod, U1000, U1073741824, U262144, U4, U4096}; // 2^18, 4096 * 262144 /// Type-level number for 4000 bytes (signature size) = 4 * 1000 diff --git a/lean_client/containers/tests/debug_deserialize.rs b/lean_client/containers/tests/debug_deserialize.rs index 0b5a6de..1d28df1 100644 --- a/lean_client/containers/tests/debug_deserialize.rs +++ b/lean_client/containers/tests/debug_deserialize.rs @@ -4,45 +4,52 @@ use std::fs; #[test] fn debug_deserialize_state() { let json_content = fs::read_to_string( - "../tests/test_vectors/test_blocks/test_process_first_block_after_genesis.json" - ).expect("Failed to read test vector file"); - + "../tests/test_vectors/test_blocks/test_process_first_block_after_genesis.json", + ) + .expect("Failed to read test vector file"); + // Try to deserialize just to see where it fails let result: Result = serde_json::from_str(&json_content); - + match result { Ok(value) => { println!("✓ JSON is valid"); - + // Try to extract just the pre state if let Some(tests) = value.as_object() { if let Some((test_name, test_case)) = tests.iter().next() { println!("✓ Found test: {}", test_name); - + if let Some(pre) = test_case.get("pre") { println!("✓ Found pre state"); - + // Try deserializing field by field if let Some(pre_obj) = pre.as_object() { for (field_name, field_value) in pre_obj.iter() { println!("\nTrying to deserialize field: {}", field_name); - println!("Field value type: {}", match field_value { - serde_json::Value::Null => "null", - serde_json::Value::Bool(_) => "bool", - serde_json::Value::Number(_) => "number", - serde_json::Value::String(_) => "string", - serde_json::Value::Array(_) => "array", - serde_json::Value::Object(_) => "object", - }); - + println!( + "Field value type: {}", + match field_value { + serde_json::Value::Null => "null", + serde_json::Value::Bool(_) => "bool", + serde_json::Value::Number(_) => "number", + serde_json::Value::String(_) => "string", + serde_json::Value::Array(_) => "array", + serde_json::Value::Object(_) => "object", + } + ); + if field_value.is_object() { if let Some(obj) = field_value.as_object() { - println!("Object keys: {:?}", obj.keys().collect::>()); + println!( + "Object keys: {:?}", + obj.keys().collect::>() + ); } } } } - + // Now try to deserialize the whole state let state_result: Result = serde_json::from_value(pre.clone()); match state_result { diff --git a/lean_client/containers/tests/main.rs b/lean_client/containers/tests/main.rs index 4d48535..f951ffe 100644 --- a/lean_client/containers/tests/main.rs +++ b/lean_client/containers/tests/main.rs @@ -1,4 +1,4 @@ // tests/lib - Test entry point mod debug_deserialize; +mod test_vectors; mod unit_tests; -mod test_vectors; \ No newline at end of file diff --git a/lean_client/containers/tests/test_vectors/block_processing.rs b/lean_client/containers/tests/test_vectors/block_processing.rs index caec865..5bbc997 100644 --- a/lean_client/containers/tests/test_vectors/block_processing.rs +++ b/lean_client/containers/tests/test_vectors/block_processing.rs @@ -13,8 +13,7 @@ fn test_process_first_block_after_genesis() { #[cfg(feature = "devnet1")] fn test_blocks_with_gaps() { let test_path = "../tests/test_vectors/test_blocks/test_blocks_with_gaps.json"; - TestRunner::run_block_processing_test(test_path) - .expect("test_blocks_with_gaps failed"); + TestRunner::run_block_processing_test(test_path).expect("test_blocks_with_gaps failed"); } #[test] @@ -29,16 +28,14 @@ fn test_linear_chain_multiple_blocks() { #[cfg(feature = "devnet1")] fn test_block_extends_deep_chain() { let test_path = "../tests/test_vectors/test_blocks/test_block_extends_deep_chain.json"; - TestRunner::run_block_processing_test(test_path) - .expect("test_block_extends_deep_chain failed"); + TestRunner::run_block_processing_test(test_path).expect("test_block_extends_deep_chain failed"); } #[test] #[cfg(feature = "devnet1")] fn test_empty_blocks() { let test_path = "../tests/test_vectors/test_blocks/test_empty_blocks.json"; - TestRunner::run_block_processing_test(test_path) - .expect("test_empty_blocks failed"); + TestRunner::run_block_processing_test(test_path).expect("test_empty_blocks failed"); } #[test] diff --git a/lean_client/containers/tests/test_vectors/genesis.rs b/lean_client/containers/tests/test_vectors/genesis.rs index 0b1d3d3..92acf25 100644 --- a/lean_client/containers/tests/test_vectors/genesis.rs +++ b/lean_client/containers/tests/test_vectors/genesis.rs @@ -4,20 +4,17 @@ use super::runner::TestRunner; #[test] fn test_genesis_default_configuration() { let test_path = "../tests/test_vectors/test_genesis/test_genesis_default_configuration.json"; - TestRunner::run_genesis_test(test_path) - .expect("test_genesis_default_configuration failed"); + TestRunner::run_genesis_test(test_path).expect("test_genesis_default_configuration failed"); } #[test] fn test_genesis_custom_time() { let test_path = "../tests/test_vectors/test_genesis/test_genesis_custom_time.json"; - TestRunner::run_genesis_test(test_path) - .expect("test_genesis_custom_time failed"); + TestRunner::run_genesis_test(test_path).expect("test_genesis_custom_time failed"); } #[test] fn test_genesis_custom_validator_set() { let test_path = "../tests/test_vectors/test_genesis/test_genesis_custom_validator_set.json"; - TestRunner::run_genesis_test(test_path) - .expect("test_genesis_custom_validator_set failed"); + TestRunner::run_genesis_test(test_path).expect("test_genesis_custom_validator_set failed"); } diff --git a/lean_client/containers/tests/test_vectors/mod.rs b/lean_client/containers/tests/test_vectors/mod.rs index 8859847..acdc055 100644 --- a/lean_client/containers/tests/test_vectors/mod.rs +++ b/lean_client/containers/tests/test_vectors/mod.rs @@ -1,15 +1,13 @@ // Test vector modules -pub mod runner; pub mod block_processing; pub mod genesis; +pub mod runner; pub mod verify_signatures; +use containers::{block::Block, block::SignedBlockWithAttestation, state::State, Slot}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::collections::HashMap; -use containers::{ - Slot, block::Block, block::SignedBlockWithAttestation, state::State -}; /// Custom deserializer that handles both plain values and {"data": T} wrapper format fn deserialize_flexible<'de, D, T>(deserializer: D) -> Result @@ -18,21 +16,22 @@ where T: serde::de::DeserializeOwned, { use serde::de::Error; - + // Deserialize as a generic Value first to inspect the structure let value = Value::deserialize(deserializer)?; - + // Check if it's an object with a "data" field if let Value::Object(ref map) = value { if map.contains_key("data") && map.len() == 1 { // Extract just the data field if let Some(data_value) = map.get("data") { - return serde_json::from_value(data_value.clone()) - .map_err(|e| D::Error::custom(format!("Failed to deserialize from data wrapper: {}", e))); + return serde_json::from_value(data_value.clone()).map_err(|e| { + D::Error::custom(format!("Failed to deserialize from data wrapper: {}", e)) + }); } } } - + // Otherwise, deserialize as a plain value serde_json::from_value(value) .map_err(|e| D::Error::custom(format!("Failed to deserialize plain value: {}", e))) diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index bf23138..0ed8ac5 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -6,14 +6,19 @@ use std::path::Path; pub struct TestRunner; impl TestRunner { - pub fn run_sequential_block_processing_tests>(path: P) -> Result<(), Box> { + pub fn run_sequential_block_processing_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; println!("Running test: {}", test_name); @@ -78,20 +83,22 @@ impl TestRunner { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } // Only check validator count if specified in post-state if let Some(expected_count) = post.validator_count { let num_validators = state.validators.len_u64(); - + if num_validators as usize != expected_count { return Err(format!( "Post-state validator count mismatch: expected {}, got {}", expected_count, num_validators - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); println!(" Validator count: {}", num_validators); @@ -100,38 +107,48 @@ impl TestRunner { println!(" Final slot: {:?}", state.slot); } } - + println!("\n✓✓✓ PASS: All blocks processed successfully with matching roots ✓✓✓"); } - + Ok(()) } - pub fn run_single_block_with_slot_gap_tests>(path: P) -> Result<(), Box> { + pub fn run_single_block_with_slot_gap_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; - + // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("Running test: {}", test_name); println!("Description: {}", test_case.info.description); if let Some(ref blocks) = test_case.blocks { let mut state = test_case.pre.clone(); - + for (idx, block) in blocks.iter().enumerate() { - println!("\nProcessing block {}: slot {:?} (gap from slot {:?})", idx + 1, block.slot, state.slot); - + println!( + "\nProcessing block {}: slot {:?} (gap from slot {:?})", + idx + 1, + block.slot, + state.slot + ); + // Advance state to the block's slot (this handles the slot gap) let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { return Err(format!( @@ -141,19 +158,19 @@ impl TestRunner { computed_parent_root ).into()); } - + println!(" ✓ Parent root matches: {:?}", computed_parent_root); - + // Process the block header let result = state_after_slots.process_block_header(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the vector if block.state_root != computed_state_root { return Err(format!( @@ -163,59 +180,69 @@ impl TestRunner { computed_state_root ).into()); } - + println!(" ✓ State root matches: {:?}", computed_state_root); - println!(" ✓ Block {} processed successfully (with {} empty slots)", idx + 1, block.slot.0 - test_case.pre.slot.0 - idx as u64); + println!( + " ✓ Block {} processed successfully (with {} empty slots)", + idx + 1, + block.slot.0 - test_case.pre.slot.0 - idx as u64 + ); } Err(e) => { return Err(format!("Block {} processing failed: {:?}", idx + 1, e).into()); } } } - + // Verify post-state conditions if let Some(post) = test_case.post { if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); } - + println!("\n✓✓✓ PASS: Block with slot gap processed successfully ✓✓✓"); } - + Ok(()) } - pub fn run_single_empty_block_tests>(path: P) -> Result<(), Box> { + pub fn run_single_empty_block_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; - + // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("Running test: {}", test_name); println!("Description: {}", test_case.info.description); if let Some(ref blocks) = test_case.blocks { let mut state = test_case.pre.clone(); - + // Should be exactly one block if blocks.len() != 1 { return Err(format!("Expected 1 block, found {}", blocks.len()).into()); } - + let block = &blocks[0]; println!("\nProcessing single empty block at slot {:?}", block.slot); - + // Verify it's an empty block (no attestations) let attestation_count = { let mut count = 0u64; @@ -227,18 +254,22 @@ impl TestRunner { } count }; - + if attestation_count > 0 { - return Err(format!("Expected empty block, but found {} attestations", attestation_count).into()); + return Err(format!( + "Expected empty block, but found {} attestations", + attestation_count + ) + .into()); } println!(" ✓ Confirmed: Block has no attestations (empty block)"); - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { return Err(format!( @@ -247,19 +278,19 @@ impl TestRunner { computed_parent_root ).into()); } - + println!(" ✓ Parent root matches: {:?}", computed_parent_root); - + // Process the block header let result = state_after_slots.process_block_header(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the vector if block.state_root != computed_state_root { return Err(format!( @@ -268,7 +299,7 @@ impl TestRunner { computed_state_root ).into()); } - + println!(" ✓ State root matches: {:?}", computed_state_root); println!(" ✓ Empty block processed successfully"); } @@ -276,38 +307,44 @@ impl TestRunner { return Err(format!("Block processing failed: {:?}", e).into()); } } - + // Verify post-state conditions if let Some(post) = test_case.post { if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); } - + println!("\n✓✓✓ PASS: Single empty block processed successfully ✓✓✓"); } - + Ok(()) } /// Generic test runner for block processing test vectors /// Handles all test vectors from test_blocks directory - pub fn run_block_processing_test>(path: P) -> Result<(), Box> { + pub fn run_block_processing_test>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); // Check if this is an invalid/exception test @@ -327,7 +364,7 @@ impl TestRunner { } let mut state = test_case.pre.clone(); - + for (idx, block) in blocks.iter().enumerate() { // Check if this is a gap (missed slots) let gap_size = if idx == 0 { @@ -335,19 +372,24 @@ impl TestRunner { } else { block.slot.0 - state.slot.0 - 1 }; - + if gap_size > 0 { - println!(" Block {}: slot {} (gap: {} empty slots)", idx + 1, block.slot.0, gap_size); + println!( + " Block {}: slot {} (gap: {} empty slots)", + idx + 1, + block.slot.0, + gap_size + ); } else { println!(" Block {}: slot {}", idx + 1, block.slot.0); } - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { println!(" \x1b[31m✗ FAIL: Parent root mismatch\x1b[0m"); @@ -355,7 +397,7 @@ impl TestRunner { println!(" Got: {:?}\n", computed_parent_root); return Err(format!("Block {} parent_root mismatch", idx + 1).into()); } - + // Check if block is empty (no attestations) let attestation_count = { let mut count = 0u64; @@ -367,17 +409,17 @@ impl TestRunner { } count }; - + // Process the full block (header + operations) let result = state_after_slots.process_block(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the block if block.state_root != computed_state_root { println!(" \x1b[31m✗ FAIL: State root mismatch\x1b[0m"); @@ -385,7 +427,7 @@ impl TestRunner { println!(" Got: {:?}\n", computed_state_root); return Err(format!("Block {} state_root mismatch", idx + 1).into()); } - + if attestation_count > 0 { println!(" ✓ Processed with {} attestation(s)", attestation_count); } else { @@ -399,13 +441,13 @@ impl TestRunner { } } } - + // Verify post-state conditions Self::verify_post_state(&state, &test_case)?; - + println!("\n\x1b[32m✓ PASS\x1b[0m\n"); } - + Ok(()) } @@ -413,50 +455,64 @@ impl TestRunner { /// Handles test vectors from test_genesis directory pub fn run_genesis_test>(path: P) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the TestVectorFile structure let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); let state = &test_case.pre; - + let num_validators = state.validators.len_u64(); - println!(" Genesis time: {}, slot: {}, validators: {}", state.config.genesis_time, state.slot.0, num_validators); - + println!( + " Genesis time: {}, slot: {}, validators: {}", + state.config.genesis_time, state.slot.0, num_validators + ); + // Verify it's at genesis (slot 0) if state.slot.0 != 0 { return Err(format!("Expected genesis at slot 0, got slot {}", state.slot.0).into()); } - + // Verify checkpoint initialization if state.latest_justified.slot.0 != 0 { - return Err(format!("Expected latest_justified at slot 0, got {}", state.latest_justified.slot.0).into()); + return Err(format!( + "Expected latest_justified at slot 0, got {}", + state.latest_justified.slot.0 + ) + .into()); } - + if state.latest_finalized.slot.0 != 0 { - return Err(format!("Expected latest_finalized at slot 0, got {}", state.latest_finalized.slot.0).into()); + return Err(format!( + "Expected latest_finalized at slot 0, got {}", + state.latest_finalized.slot.0 + ) + .into()); } - + // Verify empty historical data let has_history = state.historical_block_hashes.get(0).is_ok(); if has_history { return Err("Expected empty historical block hashes at genesis".into()); } - + println!(" ✓ Genesis state validated"); - + // Verify post-state if present if test_case.post.is_some() { Self::verify_post_state(state, &test_case)?; } - + println!("\n\x1b[32m✓ PASS\x1b[0m\n"); - + Ok(()) } @@ -473,10 +529,10 @@ impl TestRunner { for (idx, block) in blocks.iter().enumerate() { println!(" Block {}: slot {}", idx + 1, block.slot.0); - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Try to process the full block (header + body) - we expect this to fail let result = state_after_slots.process_block(block); @@ -484,14 +540,16 @@ impl TestRunner { Ok(new_state) => { // Block processing succeeded, now validate state root let computed_state_root = hash_tree_root(&new_state); - + if block.state_root != computed_state_root { error_occurred = true; println!(" ✓ Correctly rejected: Invalid block state root"); break; // Stop at first error } else { println!(" \x1b[31m✗ FAIL: Block processed successfully - but should have failed!\x1b[0m\n"); - return Err("Expected block processing to fail, but it succeeded".into()); + return Err( + "Expected block processing to fail, but it succeeded".into() + ); } } Err(e) => { @@ -501,91 +559,110 @@ impl TestRunner { } } } - + if !error_occurred { return Err("Expected an exception but all blocks processed successfully".into()); } } - + Ok(()) } /// Helper: Verify genesis state only (no blocks) fn verify_genesis_state(test_case: TestCase) -> Result<(), Box> { let state = &test_case.pre; - + // Verify post-state if present Self::verify_post_state(state, &test_case)?; - + Ok(()) } /// Helper: Verify post-state conditions - fn verify_post_state(state: &State, test_case: &TestCase) -> Result<(), Box> { + fn verify_post_state( + state: &State, + test_case: &TestCase, + ) -> Result<(), Box> { if let Some(ref post) = test_case.post { // Verify slot if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + // Verify validator count if specified if let Some(expected_count) = post.validator_count { let num_validators = state.validators.len_u64(); - + if num_validators as usize != expected_count { return Err(format!( "Post-state validator count mismatch: expected {}, got {}", expected_count, num_validators - ).into()); + ) + .into()); } - println!(" ✓ Post-state verified: slot {}, {} validators", state.slot.0, num_validators); + println!( + " ✓ Post-state verified: slot {}, {} validators", + state.slot.0, num_validators + ); } else { println!(" ✓ Post-state verified: slot {}", state.slot.0); } } - + Ok(()) } /// Test runner for verify_signatures test vectors /// Tests XMSS signature verification on SignedBlockWithAttestation #[cfg(feature = "devnet1")] - pub fn run_verify_signatures_test>(path: P) -> Result<(), Box> { + pub fn run_verify_signatures_test>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the VerifySignaturesTestVectorFile structure let test_file: VerifySignaturesTestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); - + let anchor_state = test_case.anchor_state; let signed_block = test_case.signed_block_with_attestation; - + // Print some debug info about what we're verifying println!(" Block slot: {}", signed_block.message.block.slot.0); - println!(" Proposer index: {}", signed_block.message.block.proposer_index.0); - + println!( + " Proposer index: {}", + signed_block.message.block.proposer_index.0 + ); + let attestation_count = signed_block.message.block.body.attestations.len_u64(); println!(" Attestations in block: {}", attestation_count); - println!(" Proposer attestation validator: {}", signed_block.message.proposer_attestation.validator_id.0); - + println!( + " Proposer attestation validator: {}", + signed_block.message.proposer_attestation.validator_id.0 + ); + let signature_count = signed_block.signature.len_u64(); println!(" Signatures: {}", signature_count); - + // Check if we expect this test to fail if let Some(ref exception) = test_case.expect_exception { println!(" Expecting exception: {}", exception); - + // Verify signatures - we expect this to fail (return false) let result = signed_block.verify_signatures(anchor_state); - + if result { println!(" \x1b[31m✗ FAIL: Signatures verified successfully but should have failed!\x1b[0m\n"); return Err("Expected signature verification to fail, but it succeeded".into()); @@ -596,7 +673,7 @@ impl TestRunner { } else { // Valid test case - signatures should verify successfully let result = signed_block.verify_signatures(anchor_state); - + if result { println!(" ✓ All signatures verified successfully"); println!("\n\x1b[32m✓ PASS\x1b[0m\n"); @@ -605,8 +682,7 @@ impl TestRunner { return Err("Signature verification failed".into()); } } - + Ok(()) } - } diff --git a/lean_client/containers/tests/test_vectors/verify_signatures.rs b/lean_client/containers/tests/test_vectors/verify_signatures.rs index cfc3301..13692f7 100644 --- a/lean_client/containers/tests/test_vectors/verify_signatures.rs +++ b/lean_client/containers/tests/test_vectors/verify_signatures.rs @@ -18,8 +18,7 @@ use super::runner::TestRunner; #[cfg(feature = "devnet1")] fn test_proposer_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json"; - TestRunner::run_verify_signatures_test(test_path) - .expect("test_proposer_signature failed"); + TestRunner::run_verify_signatures_test(test_path).expect("test_proposer_signature failed"); } #[test] @@ -40,8 +39,7 @@ fn test_proposer_and_attester_signatures() { #[ignore = "Requires xmss-verify feature for actual signature validation. Run with: cargo test --features xmss-verify"] fn test_invalid_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json"; - TestRunner::run_verify_signatures_test(test_path) - .expect("test_invalid_signature failed"); + TestRunner::run_verify_signatures_test(test_path).expect("test_invalid_signature failed"); } #[test] diff --git a/lean_client/containers/tests/unit_tests/attestation_aggregation.rs b/lean_client/containers/tests/unit_tests/attestation_aggregation.rs index 285aa46..72d48b4 100644 --- a/lean_client/containers/tests/unit_tests/attestation_aggregation.rs +++ b/lean_client/containers/tests/unit_tests/attestation_aggregation.rs @@ -1,10 +1,12 @@ #[cfg(feature = "devnet2")] #[cfg(test)] mod tests { - use containers::attestation::{AggregatedAttestation, AggregationBits, Attestation, AttestationData}; - use containers::{Bytes32, Uint64}; + use containers::attestation::{ + AggregatedAttestation, AggregationBits, Attestation, AttestationData, + }; use containers::checkpoint::Checkpoint; use containers::slot::Slot; + use containers::{Bytes32, Uint64}; #[test] fn test_aggregated_attestation_structure() { @@ -21,17 +23,22 @@ mod tests { source: Checkpoint { root: Bytes32::default(), slot: Slot(2), - } + }, }; let bits = AggregationBits::from_validator_indices(&vec![2, 7]); let agg = AggregatedAttestation { aggregation_bits: bits.clone(), - data: att_data.clone() + data: att_data.clone(), }; let indices = agg.aggregation_bits.to_validator_indices(); - assert_eq!(indices.into_iter().collect::>(), vec![2, 7].into_iter().collect()); + assert_eq!( + indices + .into_iter() + .collect::>(), + vec![2, 7].into_iter().collect() + ); assert_eq!(agg.data, att_data); } @@ -50,7 +57,7 @@ mod tests { source: Checkpoint { root: Bytes32::default(), slot: Slot(2), - } + }, }; let att_data2 = AttestationData { slot: Slot(6), @@ -65,7 +72,7 @@ mod tests { source: Checkpoint { root: Bytes32::default(), slot: Slot(3), - } + }, }; let attestations = vec![ @@ -88,7 +95,12 @@ mod tests { let agg1 = aggregated.iter().find(|agg| agg.data == att_data1).unwrap(); let validator_ids1 = agg1.aggregation_bits.to_validator_indices(); - assert_eq!(validator_ids1.into_iter().collect::>(), vec![1, 3].into_iter().collect()); + assert_eq!( + validator_ids1 + .into_iter() + .collect::>(), + vec![1, 3].into_iter().collect() + ); let agg2 = aggregated.iter().find(|agg| agg.data == att_data2).unwrap(); let validator_ids2 = agg2.aggregation_bits.to_validator_indices(); @@ -116,7 +128,7 @@ mod tests { source: Checkpoint { root: Bytes32::default(), slot: Slot(2), - } + }, }; let attestations = vec![Attestation { diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 26fa0a5..1a648b8 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -1,7 +1,15 @@ -use containers::{Attestation, Attestations, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators, AggregatedAttestation, Signature}; -use ssz::{PersistentList}; -use typenum::U4096; use containers::block::BlockSignatures; +use containers::{ + block::{hash_tree_root, Block, BlockBody, BlockHeader}, + checkpoint::Checkpoint, + slot::Slot, + state::State, + types::{Bytes32, ValidatorIndex}, + AggregatedAttestation, Attestation, Attestations, BlockWithAttestation, Config, Signature, + SignedBlockWithAttestation, Validators, +}; +use ssz::PersistentList; +use typenum::U4096; pub const DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const TEST_VALIDATOR_COUNT: usize = 4; // Actual validator count used in tests @@ -10,7 +18,11 @@ pub const TEST_VALIDATOR_COUNT: usize = 4; // Actual validator count used in tes const _: [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT] = [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT]; -pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Option) -> SignedBlockWithAttestation { +pub fn create_block( + slot: u64, + parent_header: &mut BlockHeader, + attestations: Option, +) -> SignedBlockWithAttestation { #[cfg(feature = "devnet1")] let body = BlockBody { attestations: attestations.unwrap_or_else(PersistentList::default), @@ -19,23 +31,26 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op let body = BlockBody { attestations: { let attestations_vec = attestations.unwrap_or_default(); - + // Convert PersistentList into a Vec - let attestations_vec: Vec = attestations_vec.into_iter().cloned().collect(); + let attestations_vec: Vec = + attestations_vec.into_iter().cloned().collect(); let aggregated: Vec = AggregatedAttestation::aggregate_by_data(&attestations_vec); - let aggregated: Vec = AggregatedAttestation::aggregate_by_data(&attestations_vec); // Create a new empty PersistentList - let mut persistent_list: PersistentList = PersistentList::default(); + let mut persistent_list: PersistentList = + PersistentList::default(); // Push each aggregated attestation for agg in aggregated { - persistent_list.push(agg).expect("PersistentList capacity exceeded"); + persistent_list + .push(agg) + .expect("PersistentList capacity exceeded"); } persistent_list @@ -43,7 +58,6 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op // other BlockBody fields... }; - let block_message = Block { slot: Slot(slot), proposer_index: ValidatorIndex(slot % 10), @@ -70,11 +84,10 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op signature: BlockSignatures { attestation_signatures: PersistentList::default(), proposer_signature: Signature::default(), - } + }, }; - - return_value + return_value } pub fn create_attestations(indices: &[usize]) -> Vec { @@ -109,8 +122,11 @@ pub fn base_state(config: Config) -> State { } pub fn base_state_with_validators(config: Config, num_validators: usize) -> State { - use containers::{HistoricalBlockHashes, JustificationRoots, JustifiedSlots, JustificationsValidators, validator::Validator, Uint64}; - + use containers::{ + validator::Validator, HistoricalBlockHashes, JustificationRoots, JustificationsValidators, + JustifiedSlots, Uint64, + }; + // Create validators list with the specified number of validators let mut validators = Validators::default(); for i in 0..num_validators { @@ -120,7 +136,7 @@ pub fn base_state_with_validators(config: Config, num_validators: usize) -> Stat }; validators.push(validator).expect("within limit"); } - + State { config, slot: Slot(0), @@ -136,7 +152,5 @@ pub fn base_state_with_validators(config: Config, num_validators: usize) -> Stat } pub fn sample_config() -> Config { - Config { - genesis_time: 0, - } -} \ No newline at end of file + Config { genesis_time: 0 } +} diff --git a/lean_client/containers/tests/unit_tests/mod.rs b/lean_client/containers/tests/unit_tests/mod.rs index b9f442f..1bef390 100644 --- a/lean_client/containers/tests/unit_tests/mod.rs +++ b/lean_client/containers/tests/unit_tests/mod.rs @@ -1,7 +1,7 @@ // tests/unit_tests/mod.rs +mod attestation_aggregation; mod common; mod state_basic; mod state_justifications; mod state_process; mod state_transition; -mod attestation_aggregation; diff --git a/lean_client/containers/tests/unit_tests/state_basic.rs b/lean_client/containers/tests/unit_tests/state_basic.rs index 5fa16e1..085384a 100644 --- a/lean_client/containers/tests/unit_tests/state_basic.rs +++ b/lean_client/containers/tests/unit_tests/state_basic.rs @@ -1,5 +1,10 @@ // tests/state_basic.rs -use containers::{block::{BlockBody, hash_tree_root}, state::State, types::Uint64, ValidatorIndex}; +use containers::{ + block::{hash_tree_root, BlockBody}, + state::State, + types::Uint64, + ValidatorIndex, +}; use pretty_assertions::assert_eq; #[path = "common.rs"] @@ -14,8 +19,13 @@ fn test_generate_genesis() { assert_eq!(state.config, config); assert_eq!(state.slot.0, 0); - let empty_body = BlockBody { attestations: ssz::PersistentList::default() }; - assert_eq!(state.latest_block_header.body_root, hash_tree_root(&empty_body)); + let empty_body = BlockBody { + attestations: ssz::PersistentList::default(), + }; + assert_eq!( + state.latest_block_header.body_root, + hash_tree_root(&empty_body) + ); // Check that collections are empty by trying to get the first element assert!(state.historical_block_hashes.get(0).is_err()); @@ -41,7 +51,9 @@ fn test_slot_justifiability_rules() { #[test] fn test_hash_tree_root() { - let body = BlockBody { attestations: ssz::PersistentList::default() }; + let body = BlockBody { + attestations: ssz::PersistentList::default(), + }; let block = containers::block::Block { slot: containers::slot::Slot(1), proposer_index: ValidatorIndex(0), @@ -52,4 +64,4 @@ fn test_hash_tree_root() { let root = hash_tree_root(&block); assert_ne!(root, containers::types::Bytes32(ssz::H256::zero())); -} \ No newline at end of file +} diff --git a/lean_client/containers/tests/unit_tests/state_justifications.rs b/lean_client/containers/tests/unit_tests/state_justifications.rs index 9a7b0cc..afdd220 100644 --- a/lean_client/containers/tests/unit_tests/state_justifications.rs +++ b/lean_client/containers/tests/unit_tests/state_justifications.rs @@ -1,18 +1,12 @@ // tests/state_justifications.rs -use containers::{ - state::State, - types::Bytes32, - Config -}; +use containers::{state::State, types::Bytes32, Config}; use pretty_assertions::assert_eq; use rstest::{fixture, rstest}; use ssz::PersistentList as List; #[path = "common.rs"] mod common; -use common::{ - base_state, create_attestations, sample_config, TEST_VALIDATOR_COUNT, -}; +use common::{base_state, create_attestations, sample_config, TEST_VALIDATOR_COUNT}; #[fixture] fn config() -> Config { @@ -49,7 +43,7 @@ fn test_get_justifications_single_root() { let mut roots_list = List::default(); roots_list.push(root1).unwrap(); state.justifications_roots = roots_list; - + // Convert Vec to BitList let mut bitlist = ssz::BitList::with_length(TEST_VALIDATOR_COUNT); for (i, &val) in votes1.iter().enumerate() { @@ -88,7 +82,7 @@ fn test_get_justifications_multiple_roots() { roots_list.push(root2).unwrap(); roots_list.push(root3).unwrap(); state.justifications_roots = roots_list; - + // Convert Vec to BitList let mut bitlist = ssz::BitList::with_length(all_votes.len()); for (i, &val) in all_votes.iter().enumerate() { @@ -113,16 +107,20 @@ fn test_with_justifications_empty() { let mut initial_state = base_state(config.clone()); let mut roots_list = List::default(); - roots_list.push(Bytes32(ssz::H256::from_slice(&[1u8;32]))).unwrap(); + roots_list + .push(Bytes32(ssz::H256::from_slice(&[1u8; 32]))) + .unwrap(); initial_state.justifications_roots = roots_list; - + let mut bitlist = ssz::BitList::with_length(TEST_VALIDATOR_COUNT); for i in 0..TEST_VALIDATOR_COUNT { bitlist.set(i, true); } initial_state.justifications_validators = bitlist; - let new_state = initial_state.clone().with_justifications(std::collections::BTreeMap::new()); + let new_state = initial_state + .clone() + .with_justifications(std::collections::BTreeMap::new()); assert!(new_state.justifications_roots.get(0).is_err()); assert!(new_state.justifications_validators.get(0).is_none()); @@ -149,11 +147,15 @@ fn test_with_justifications_deterministic_order() { // Expected roots in sorted order (root1 < root2) assert_eq!(new_state.justifications_roots.get(0).ok(), Some(&root1)); assert_eq!(new_state.justifications_roots.get(1).ok(), Some(&root2)); - + // Verify the bitlist contains the concatenated votes let expected_validators = [votes1, votes2].concat(); for (i, &expected_val) in expected_validators.iter().enumerate() { - let actual_val = new_state.justifications_validators.get(i).map(|b| *b).unwrap_or(false); + let actual_val = new_state + .justifications_validators + .get(i) + .map(|b| *b) + .unwrap_or(false); assert_eq!(actual_val, expected_val); } } diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index afc1887..5df98cf 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -1,6 +1,6 @@ // tests/state_process.rs use containers::{ - block::{Block, BlockBody, hash_tree_root}, + block::{hash_tree_root, Block, BlockBody}, checkpoint::Checkpoint, slot::Slot, state::State, @@ -26,15 +26,24 @@ pub fn genesis_state() -> State { fn test_process_slot() { let genesis_state = genesis_state(); - assert_eq!(genesis_state.latest_block_header.state_root, Bytes32(ssz::H256::zero())); + assert_eq!( + genesis_state.latest_block_header.state_root, + Bytes32(ssz::H256::zero()) + ); let state_after_slot = genesis_state.process_slot(); let expected_root = hash_tree_root(&genesis_state); - assert_eq!(state_after_slot.latest_block_header.state_root, expected_root); + assert_eq!( + state_after_slot.latest_block_header.state_root, + expected_root + ); let state_after_second_slot = state_after_slot.process_slot(); - assert_eq!(state_after_second_slot.latest_block_header.state_root, expected_root); + assert_eq!( + state_after_second_slot.latest_block_header.state_root, + expected_root + ); } #[test] @@ -45,7 +54,10 @@ fn test_process_slots() { let new_state = genesis_state.process_slots(target_slot).unwrap(); assert_eq!(new_state.slot, target_slot); - assert_eq!(new_state.latest_block_header.state_root, hash_tree_root(&genesis_state)); + assert_eq!( + new_state.latest_block_header.state_root, + hash_tree_root(&genesis_state) + ); } #[test] @@ -68,11 +80,21 @@ fn test_process_block_header_valid() { assert_eq!(new_state.latest_finalized.root, genesis_header_root); assert_eq!(new_state.latest_justified.root, genesis_header_root); - assert_eq!(new_state.historical_block_hashes.get(0).ok(), Some(&genesis_header_root)); - let justified_slot_0 = new_state.justified_slots.get(0).map(|b| *b).unwrap_or(false); + assert_eq!( + new_state.historical_block_hashes.get(0).ok(), + Some(&genesis_header_root) + ); + let justified_slot_0 = new_state + .justified_slots + .get(0) + .map(|b| *b) + .unwrap_or(false); assert_eq!(justified_slot_0, true); assert_eq!(new_state.latest_block_header.slot, Slot(1)); - assert_eq!(new_state.latest_block_header.state_root, Bytes32(ssz::H256::zero())); + assert_eq!( + new_state.latest_block_header.state_root, + Bytes32(ssz::H256::zero()) + ); } #[rstest] @@ -95,7 +117,9 @@ fn test_process_block_header_invalid( proposer_index: ValidatorIndex(bad_proposer), parent_root: bad_parent_root.unwrap_or(parent_root), state_root: Bytes32(ssz::H256::zero()), - body: BlockBody { attestations: List::default() }, + body: BlockBody { + attestations: List::default(), + }, }; let result = state_at_slot_1.process_block_header(&block); @@ -115,13 +139,17 @@ fn test_process_attestations_justification_and_finalization() { let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); let block1 = create_block(1, &mut state_at_slot_1.latest_block_header, None); // Use process_block_header and process_operations separately to avoid state root validation - let state_after_header1 = state_at_slot_1.process_block_header(&block1.message.block).unwrap(); + let state_after_header1 = state_at_slot_1 + .process_block_header(&block1.message.block) + .unwrap(); state = state_after_header1.process_attestations(&block1.message.block.body.attestations); // Process slot 4 and block let mut state_at_slot_4 = state.process_slots(Slot(4)).unwrap(); let block4 = create_block(4, &mut state_at_slot_4.latest_block_header, None); - let state_after_header4 = state_at_slot_4.process_block_header(&block4.message.block).unwrap(); + let state_after_header4 = state_at_slot_4 + .process_block_header(&block4.message.block) + .unwrap(); state = state_after_header4.process_attestations(&block4.message.block.body.attestations); // Advance to slot 5 @@ -151,15 +179,21 @@ fn test_process_attestations_justification_and_finalization() { // Convert Vec to PersistentList let mut attestations_list: List<_, U4096> = List::default(); - for a in attestations_for_4 { - attestations_list.push(a).unwrap(); + for a in attestations_for_4 { + attestations_list.push(a).unwrap(); } let new_state = state.process_attestations(&attestations_list); assert_eq!(new_state.latest_justified, checkpoint4); - let justified_slot_4 = new_state.justified_slots.get(4).map(|b| *b).unwrap_or(false); + let justified_slot_4 = new_state + .justified_slots + .get(4) + .map(|b| *b) + .unwrap_or(false); assert_eq!(justified_slot_4, true); assert_eq!(new_state.latest_finalized, genesis_checkpoint); - assert!(!new_state.get_justifications().contains_key(&checkpoint4.root)); -} \ No newline at end of file + assert!(!new_state + .get_justifications() + .contains_key(&checkpoint4.root)); +} diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index 9fe6abb..7725210 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -167,7 +167,7 @@ fn test_state_transition_devnet2() { } state_after_header.process_attestations(&unaggregated_attestations) }; - + // Ensure the state root matches the expected state let block_with_correct_root = Block { state_root: hash_tree_root(&expected_state), diff --git a/lean_client/env-config/src/lib.rs b/lean_client/env-config/src/lib.rs index 972005d..109ac2d 100644 --- a/lean_client/env-config/src/lib.rs +++ b/lean_client/env-config/src/lib.rs @@ -1 +1 @@ -// Empty on purpose \ No newline at end of file +// Empty on purpose diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index fa9aa89..9f3837d 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -34,7 +34,6 @@ pub fn on_attestation( #[cfg(feature = "devnet1")] let target_slot = signed_attestation.message.data.target.slot; - #[cfg(feature = "devnet2")] let validator_id = ValidatorIndex(signed_attestation.validator_id); #[cfg(feature = "devnet2")] @@ -80,9 +79,7 @@ pub fn on_attestation( if store .latest_known_attestations .get(&validator_id) - .map_or(true, |existing| { - existing.message.slot < attestation_slot - }) + .map_or(true, |existing| existing.message.slot < attestation_slot) { store .latest_known_attestations @@ -114,14 +111,12 @@ pub fn on_attestation( .latest_new_attestations .insert(validator_id, signed_attestation); } - + #[cfg(feature = "devnet2")] if store .latest_new_attestations .get(&validator_id) - .map_or(true, |existing| { - existing.message.slot < attestation_slot - }) + .map_or(true, |existing| existing.message.slot < attestation_slot) { store .latest_new_attestations @@ -246,7 +241,8 @@ fn process_block_internal( .zip(attestation_signatures) { let validator_ids: Vec = aggregated_attestation - .aggregation_bits.0 + .aggregation_bits + .0 .iter() .enumerate() .filter(|(_, bit)| **bit) diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 3296d06..8165443 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -1,7 +1,6 @@ use containers::{ - attestation::SignedAttestation, - block::SignedBlockWithAttestation, checkpoint::Checkpoint, config::Config, state::State, - Bytes32, Root, Slot, ValidatorIndex, + attestation::SignedAttestation, block::SignedBlockWithAttestation, checkpoint::Checkpoint, + config::Config, state::State, Bytes32, Root, Slot, ValidatorIndex, }; use ssz::SszHash; use std::collections::HashMap; @@ -185,7 +184,8 @@ pub fn update_safe_target(store: &mut Store) { let min_score = (n_validators * 2 + 2) / 3; let root = store.latest_justified.root; - store.safe_target = get_fork_choice_head(store, root, &store.latest_new_attestations, min_score); + store.safe_target = + get_fork_choice_head(store, root, &store.latest_new_attestations, min_score); } pub fn accept_new_attestations(store: &mut Store) { diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index 4f8521f..bb32273 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -4,8 +4,11 @@ use fork_choice::{ }; use containers::{ - attestation::{Attestation, AttestationData, SignedAttestation, Signature}, - block::{hash_tree_root, Block, BlockBody, BlockHeader, BlockWithAttestation, SignedBlockWithAttestation}, + attestation::{Attestation, AttestationData, Signature, SignedAttestation}, + block::{ + hash_tree_root, Block, BlockBody, BlockHeader, BlockWithAttestation, + SignedBlockWithAttestation, + }, checkpoint::Checkpoint, config::Config, state::State, @@ -305,7 +308,9 @@ fn convert_test_anchor_block(test_block: &TestAnchorBlock) -> SignedBlockWithAtt } #[cfg(feature = "devnet1")] -fn convert_test_block(test_block_with_att: &TestBlockWithAttestation) -> SignedBlockWithAttestation { +fn convert_test_block( + test_block_with_att: &TestBlockWithAttestation, +) -> SignedBlockWithAttestation { let test_block = &test_block_with_att.block; let mut attestations = ssz::PersistentList::default(); @@ -419,7 +424,7 @@ fn verify_checks( Some(c) => c, None => return Ok(()), }; - + if let Some(expected_slot) = checks.head_slot { let actual_slot = store.blocks[&store.head].message.block.slot.0; if actual_slot != expected_slot { @@ -533,7 +538,8 @@ fn run_single_test(_test_name: &str, test: TestVector) -> Result<(), String> { // Advance time to the block's slot to ensure attestations are processable // SECONDS_PER_SLOT is 4 (not 12) - let block_time = store.config.genesis_time + (signed_block.message.block.slot.0 * 4); + let block_time = + store.config.genesis_time + (signed_block.message.block.slot.0 * 4); on_tick(&mut store, block_time, false); on_block(&mut store, signed_block)?; diff --git a/lean_client/fork_choice/tests/unit_tests.rs b/lean_client/fork_choice/tests/unit_tests.rs index b4490da..480ba3f 100644 --- a/lean_client/fork_choice/tests/unit_tests.rs +++ b/lean_client/fork_choice/tests/unit_tests.rs @@ -1,6 +1,6 @@ mod unit_tests { pub mod common; + pub mod fork_choice; pub mod time; pub mod votes; - pub mod fork_choice; } diff --git a/lean_client/fork_choice/tests/unit_tests/common.rs b/lean_client/fork_choice/tests/unit_tests/common.rs index 9539c34..dc1a762 100644 --- a/lean_client/fork_choice/tests/unit_tests/common.rs +++ b/lean_client/fork_choice/tests/unit_tests/common.rs @@ -1,4 +1,3 @@ -use fork_choice::store::{get_forkchoice_store, Store}; use containers::{ attestation::Attestation, block::{Block, BlockBody, BlockWithAttestation, SignedBlockWithAttestation}, @@ -7,19 +6,16 @@ use containers::{ validator::Validator, Bytes32, Slot, Uint64, ValidatorIndex, }; +use fork_choice::store::{get_forkchoice_store, Store}; use ssz::SszHash; pub fn create_test_store() -> Store { - let config = Config { - genesis_time: 1000, - }; - - let validators = vec![ - Validator::default(); 10 - ]; - + let config = Config { genesis_time: 1000 }; + + let validators = vec![Validator::default(); 10]; + let state = State::generate_genesis_with_validators(Uint64(1000), validators); - + let block = Block { slot: Slot(0), proposer_index: ValidatorIndex(0), @@ -27,7 +23,7 @@ pub fn create_test_store() -> Store { state_root: Bytes32(state.hash_tree_root()), body: BlockBody::default(), }; - + let block_with_attestation = BlockWithAttestation { block: block.clone(), proposer_attestation: Attestation::default(), diff --git a/lean_client/fork_choice/tests/unit_tests/fork_choice.rs b/lean_client/fork_choice/tests/unit_tests/fork_choice.rs index fc1e7e6..d2b3833 100644 --- a/lean_client/fork_choice/tests/unit_tests/fork_choice.rs +++ b/lean_client/fork_choice/tests/unit_tests/fork_choice.rs @@ -1,12 +1,12 @@ use super::common::create_test_store; -use fork_choice::store::{get_proposal_head, get_vote_target}; use containers::Slot; +use fork_choice::store::{get_proposal_head, get_vote_target}; #[test] fn test_get_proposal_head_basic() { let mut store = create_test_store(); let head = get_proposal_head(&mut store, Slot(0)); - + assert_eq!(head, store.head); } @@ -14,9 +14,9 @@ fn test_get_proposal_head_basic() { fn test_get_proposal_head_advances_time() { let mut store = create_test_store(); let initial_time = store.time; - + get_proposal_head(&mut store, Slot(5)); - + assert!(store.time >= initial_time); } diff --git a/lean_client/fork_choice/tests/unit_tests/time.rs b/lean_client/fork_choice/tests/unit_tests/time.rs index 03260c7..ff99491 100644 --- a/lean_client/fork_choice/tests/unit_tests/time.rs +++ b/lean_client/fork_choice/tests/unit_tests/time.rs @@ -1,7 +1,7 @@ use super::common::create_test_store; +use containers::{Slot, Uint64}; use fork_choice::handlers::on_tick; use fork_choice::store::{tick_interval, INTERVALS_PER_SLOT, SECONDS_PER_SLOT}; -use containers::{Slot, Uint64}; #[test] fn test_on_tick_basic() { @@ -31,7 +31,7 @@ fn test_on_tick_already_current() { let initial_time = store.time; let current_target = store.config.genesis_time + initial_time; - // Try to advance to current time + // Try to advance to current time on_tick(&mut store, current_target, true); // Should not change significantly @@ -86,7 +86,7 @@ fn test_tick_interval_sequence() { #[test] fn test_tick_interval_actions_by_phase() { let mut store = create_test_store(); - + // Reset store time to 0 relative to genesis for clean testing store.time = 0; @@ -101,11 +101,10 @@ fn test_tick_interval_actions_by_phase() { } } - #[test] fn test_slot_time_calculations() { let genesis_time = 1000; - + // Slot 0 let slot_0_time = genesis_time + (0 * SECONDS_PER_SLOT); assert_eq!(slot_0_time, genesis_time); diff --git a/lean_client/fork_choice/tests/unit_tests/votes.rs b/lean_client/fork_choice/tests/unit_tests/votes.rs index 4a1b688..d6c2ad4 100644 --- a/lean_client/fork_choice/tests/unit_tests/votes.rs +++ b/lean_client/fork_choice/tests/unit_tests/votes.rs @@ -1,22 +1,35 @@ use super::common::create_test_store; -use fork_choice::handlers::on_attestation; -use fork_choice::store::{accept_new_attestations, INTERVALS_PER_SLOT}; use containers::{ - attestation::{Attestation, AttestationData, SignedAttestation, Signature}, + attestation::{Attestation, AttestationData, Signature, SignedAttestation}, checkpoint::Checkpoint, Bytes32, Slot, Uint64, ValidatorIndex, }; +use fork_choice::handlers::on_attestation; +use fork_choice::store::{accept_new_attestations, INTERVALS_PER_SLOT}; #[cfg(feature = "devnet1")] -fn create_signed_attestation(validator_id: u64, slot: Slot, head_root: Bytes32) -> SignedAttestation { +fn create_signed_attestation( + validator_id: u64, + slot: Slot, + head_root: Bytes32, +) -> SignedAttestation { SignedAttestation { message: Attestation { validator_id: Uint64(validator_id), data: AttestationData { slot, - head: Checkpoint { root: head_root, slot }, - target: Checkpoint { root: head_root, slot }, - source: Checkpoint { root: Bytes32::default(), slot: Slot(0) }, + head: Checkpoint { + root: head_root, + slot, + }, + target: Checkpoint { + root: head_root, + slot, + }, + source: Checkpoint { + root: Bytes32::default(), + slot: Slot(0), + }, }, }, signature: Signature::default(), @@ -33,54 +46,59 @@ fn test_accept_new_attestations() { let val2 = ValidatorIndex(2); let val3 = ValidatorIndex(3); - store.latest_known_attestations.insert( - val1, - create_signed_attestation(1, Slot(0), store.head), - ); + store + .latest_known_attestations + .insert(val1, create_signed_attestation(1, Slot(0), store.head)); // Val1 updates their attestation to Slot 1 - store.latest_new_attestations.insert( - val1, - create_signed_attestation(1, Slot(1), store.head), - ); + store + .latest_new_attestations + .insert(val1, create_signed_attestation(1, Slot(1), store.head)); // Val2 casts a new attestation for Slot 1 - store.latest_new_attestations.insert( - val2, - create_signed_attestation(2, Slot(1), store.head), - ); + store + .latest_new_attestations + .insert(val2, create_signed_attestation(2, Slot(1), store.head)); // Val3 casts a new attestation for Slot 2 - store.latest_new_attestations.insert( - val3, - create_signed_attestation(3, Slot(2), store.head), - ); + store + .latest_new_attestations + .insert(val3, create_signed_attestation(3, Slot(2), store.head)); accept_new_attestations(&mut store); assert_eq!(store.latest_new_attestations.len(), 0); assert_eq!(store.latest_known_attestations.len(), 3); - assert_eq!(store.latest_known_attestations[&val1].message.data.slot, Slot(1)); - assert_eq!(store.latest_known_attestations[&val2].message.data.slot, Slot(1)); - assert_eq!(store.latest_known_attestations[&val3].message.data.slot, Slot(2)); + assert_eq!( + store.latest_known_attestations[&val1].message.data.slot, + Slot(1) + ); + assert_eq!( + store.latest_known_attestations[&val2].message.data.slot, + Slot(1) + ); + assert_eq!( + store.latest_known_attestations[&val3].message.data.slot, + Slot(2) + ); } #[test] #[cfg(feature = "devnet1")] fn test_accept_new_attestations_multiple() { let mut store = create_test_store(); - + for i in 0..5 { store.latest_new_attestations.insert( ValidatorIndex(i), create_signed_attestation(i, Slot(i), store.head), ); } - + assert_eq!(store.latest_new_attestations.len(), 5); assert_eq!(store.latest_known_attestations.len(), 0); - + accept_new_attestations(&mut store); - + assert_eq!(store.latest_new_attestations.len(), 0); assert_eq!(store.latest_known_attestations.len(), 5); } @@ -89,9 +107,9 @@ fn test_accept_new_attestations_multiple() { fn test_accept_new_attestations_empty() { let mut store = create_test_store(); let initial_known = store.latest_known_attestations.len(); - + accept_new_attestations(&mut store); - + assert_eq!(store.latest_new_attestations.len(), 0); assert_eq!(store.latest_known_attestations.len(), initial_known); } @@ -107,29 +125,55 @@ fn test_on_attestation_lifecycle() { // 1. Attestation from network (gossip) let signed_attestation_gossip = create_signed_attestation(1, slot_0, store.head); - on_attestation(&mut store, signed_attestation_gossip.clone(), false).expect("Gossip attestation valid"); - + on_attestation(&mut store, signed_attestation_gossip.clone(), false) + .expect("Gossip attestation valid"); + // Should be in new_attestations, not known_attestations assert!(store.latest_new_attestations.contains_key(&validator_idx)); assert!(!store.latest_known_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, slot_0); + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + slot_0 + ); // 2. Same attestation included in a block on_attestation(&mut store, signed_attestation_gossip, true).expect("Block attestation valid"); - + assert!(store.latest_known_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, slot_0); + assert_eq!( + store.latest_known_attestations[&validator_idx] + .message + .data + .slot, + slot_0 + ); // 3. Newer attestation from network store.time = 1 * INTERVALS_PER_SLOT; // Advance time let signed_attestation_next = create_signed_attestation(1, slot_1, store.head); - on_attestation(&mut store, signed_attestation_next, false).expect("Next gossip attestation valid"); + on_attestation(&mut store, signed_attestation_next, false) + .expect("Next gossip attestation valid"); // Should update new_attestations - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, slot_1); + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + slot_1 + ); // Known attestations should still be at slot 0 until accepted - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, slot_0); + assert_eq!( + store.latest_known_attestations[&validator_idx] + .message + .data + .slot, + slot_0 + ); } #[test] @@ -137,7 +181,7 @@ fn test_on_attestation_lifecycle() { fn test_on_attestation_future_slot() { let mut store = create_test_store(); let future_slot = Slot(100); // Far in the future - + let signed_attestation = create_signed_attestation(1, future_slot, store.head); let result = on_attestation(&mut store, signed_attestation, false); @@ -149,21 +193,33 @@ fn test_on_attestation_future_slot() { fn test_on_attestation_update_vote() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); - + // First attestation at slot 0 let signed_attestation1 = create_signed_attestation(1, Slot(0), store.head); - + on_attestation(&mut store, signed_attestation1, false).expect("First attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(0)); - + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(0) + ); + // Advance time to allow slot 1 store.time = 1 * INTERVALS_PER_SLOT; - + // Second attestation at slot 1 let signed_attestation2 = create_signed_attestation(1, Slot(1), store.head); - + on_attestation(&mut store, signed_attestation2, false).expect("Second attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(1)); + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(1) + ); } #[test] @@ -171,22 +227,35 @@ fn test_on_attestation_update_vote() { fn test_on_attestation_ignore_old_vote() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); - + // Advance time store.time = 2 * INTERVALS_PER_SLOT; - + // Newer attestation first let signed_attestation_new = create_signed_attestation(1, Slot(2), store.head); - + on_attestation(&mut store, signed_attestation_new, false).expect("New attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(2)); - + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(2) + ); + // Older attestation second let signed_attestation_old = create_signed_attestation(1, Slot(1), store.head); - - on_attestation(&mut store, signed_attestation_old, false).expect("Old attestation processed but ignored"); + + on_attestation(&mut store, signed_attestation_old, false) + .expect("Old attestation processed but ignored"); // Should still be slot 2 - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(2)); + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(2) + ); } #[test] @@ -194,18 +263,18 @@ fn test_on_attestation_ignore_old_vote() { fn test_on_attestation_from_block_supersedes_new() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); - + // First, add attestation via gossip let signed_attestation1 = create_signed_attestation(1, Slot(0), store.head); on_attestation(&mut store, signed_attestation1, false).expect("Gossip attestation valid"); - + assert!(store.latest_new_attestations.contains_key(&validator_idx)); assert!(!store.latest_known_attestations.contains_key(&validator_idx)); - + // Then, add same attestation via block (on-chain) let signed_attestation2 = create_signed_attestation(1, Slot(0), store.head); on_attestation(&mut store, signed_attestation2, true).expect("Block attestation valid"); - + // Should move from new to known assert!(!store.latest_new_attestations.contains_key(&validator_idx)); assert!(store.latest_known_attestations.contains_key(&validator_idx)); @@ -216,19 +285,31 @@ fn test_on_attestation_from_block_supersedes_new() { fn test_on_attestation_newer_from_block_removes_older_new() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); - + // Add older attestation via gossip let signed_attestation_gossip = create_signed_attestation(1, Slot(0), store.head); on_attestation(&mut store, signed_attestation_gossip, false).expect("Gossip attestation valid"); - - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(0)); - + + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(0) + ); + // Add newer attestation via block (on-chain) store.time = 1 * INTERVALS_PER_SLOT; let signed_attestation_block = create_signed_attestation(1, Slot(1), store.head); on_attestation(&mut store, signed_attestation_block, true).expect("Block attestation valid"); - + // New attestation should be removed (superseded by newer on-chain one) assert!(!store.latest_new_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, Slot(1)); + assert_eq!( + store.latest_known_attestations[&validator_idx] + .message + .data + .slot, + Slot(1) + ); } diff --git a/lean_client/networking/src/gossipsub/config.rs b/lean_client/networking/src/gossipsub/config.rs index 05488c2..67061bc 100644 --- a/lean_client/networking/src/gossipsub/config.rs +++ b/lean_client/networking/src/gossipsub/config.rs @@ -15,9 +15,9 @@ impl GossipsubConfig { pub fn new() -> Self { let justification_lookback_slots: u64 = 3; let seconds_per_slot: u64 = 12; - + let seen_ttl_secs = seconds_per_slot * justification_lookback_slots * 2; - + let config = ConfigBuilder::default() // leanSpec: heartbeat_interval_secs = 0.7 .heartbeat_interval(Duration::from_millis(700)) diff --git a/lean_client/networking/src/gossipsub/message.rs b/lean_client/networking/src/gossipsub/message.rs index b578e75..4ac1ae1 100644 --- a/lean_client/networking/src/gossipsub/message.rs +++ b/lean_client/networking/src/gossipsub/message.rs @@ -1,8 +1,8 @@ use crate::gossipsub::topic::GossipsubKind; use crate::gossipsub::topic::GossipsubTopic; +use containers::SignedAttestation; use containers::SignedBlockWithAttestation; use containers::ssz::SszReadDefault; -use containers::{SignedAttestation}; use libp2p::gossipsub::TopicHash; pub enum GossipsubMessage { @@ -14,7 +14,8 @@ impl GossipsubMessage { pub fn decode(topic: &TopicHash, data: &[u8]) -> Result { match GossipsubTopic::decode(topic)?.kind { GossipsubKind::Block => Ok(Self::Block( - SignedBlockWithAttestation::from_ssz_default(data).map_err(|e| format!("{:?}", e))?, + SignedBlockWithAttestation::from_ssz_default(data) + .map_err(|e| format!("{:?}", e))?, )), GossipsubKind::Attestation => Ok(Self::Attestation( SignedAttestation::from_ssz_default(data).map_err(|e| format!("{:?}", e))?, diff --git a/lean_client/networking/src/gossipsub/tests/config.rs b/lean_client/networking/src/gossipsub/tests/config.rs index e788d81..4fa245d 100644 --- a/lean_client/networking/src/gossipsub/tests/config.rs +++ b/lean_client/networking/src/gossipsub/tests/config.rs @@ -1,5 +1,5 @@ use crate::gossipsub::config::GossipsubConfig; -use crate::gossipsub::topic::{get_topics, GossipsubKind}; +use crate::gossipsub::topic::{GossipsubKind, get_topics}; #[test] fn test_default_parameters() { @@ -24,8 +24,14 @@ fn test_default_parameters() { assert_eq!(config.config.gossip_lazy(), 6); // d_lazy = 6 assert_eq!(config.config.history_length(), 6); // mcache_len = 6 assert_eq!(config.config.history_gossip(), 3); // mcache_gossip = 3 - assert_eq!(config.config.fanout_ttl(), std::time::Duration::from_secs(60)); // fanout_ttl_secs = 60 - assert_eq!(config.config.heartbeat_interval(), std::time::Duration::from_millis(700)); // heartbeat_interval_secs = 0.7 + assert_eq!( + config.config.fanout_ttl(), + std::time::Duration::from_secs(60) + ); // fanout_ttl_secs = 60 + assert_eq!( + config.config.heartbeat_interval(), + std::time::Duration::from_millis(700) + ); // heartbeat_interval_secs = 0.7 assert!(config.topics.is_empty()); } diff --git a/lean_client/networking/src/gossipsub/tests/message.rs b/lean_client/networking/src/gossipsub/tests/message.rs index ce062be..9fd25dd 100644 --- a/lean_client/networking/src/gossipsub/tests/message.rs +++ b/lean_client/networking/src/gossipsub/tests/message.rs @@ -1,5 +1,7 @@ use crate::gossipsub::message::GossipsubMessage; -use crate::gossipsub::topic::{ATTESTATION_TOPIC, BLOCK_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX}; +use crate::gossipsub::topic::{ + ATTESTATION_TOPIC, BLOCK_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX, +}; use libp2p::gossipsub::TopicHash; #[test] diff --git a/lean_client/networking/src/gossipsub/tests/message_id.rs b/lean_client/networking/src/gossipsub/tests/message_id.rs index 4eb3302..17a0b51 100644 --- a/lean_client/networking/src/gossipsub/tests/message_id.rs +++ b/lean_client/networking/src/gossipsub/tests/message_id.rs @@ -1,5 +1,7 @@ use crate::gossipsub::config::compute_message_id; -use crate::gossipsub::topic::{ATTESTATION_TOPIC, BLOCK_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX}; +use crate::gossipsub::topic::{ + ATTESTATION_TOPIC, BLOCK_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX, +}; use crate::types::MESSAGE_DOMAIN_VALID_SNAPPY; use libp2p::gossipsub::{Message, TopicHash}; use sha2::{Digest, Sha256}; @@ -134,7 +136,7 @@ fn test_message_id_uses_valid_snappy_domain() { let topic_bytes = topic.as_bytes(); let topic_len = topic_bytes.len() as u64; - + let mut digest_input = Vec::new(); digest_input.extend_from_slice(MESSAGE_DOMAIN_VALID_SNAPPY); diff --git a/lean_client/networking/src/gossipsub/tests/mod.rs b/lean_client/networking/src/gossipsub/tests/mod.rs index 351a897..15f330a 100644 --- a/lean_client/networking/src/gossipsub/tests/mod.rs +++ b/lean_client/networking/src/gossipsub/tests/mod.rs @@ -1,4 +1,4 @@ mod config; -mod message_id; mod message; -mod topic; \ No newline at end of file +mod message_id; +mod topic; diff --git a/lean_client/networking/src/gossipsub/tests/topic.rs b/lean_client/networking/src/gossipsub/tests/topic.rs index cdd09df..7e3d70b 100644 --- a/lean_client/networking/src/gossipsub/tests/topic.rs +++ b/lean_client/networking/src/gossipsub/tests/topic.rs @@ -1,6 +1,6 @@ use crate::gossipsub::topic::{ - get_topics, GossipsubKind, GossipsubTopic, ATTESTATION_TOPIC, BLOCK_TOPIC, - SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX, + ATTESTATION_TOPIC, BLOCK_TOPIC, GossipsubKind, GossipsubTopic, SSZ_SNAPPY_ENCODING_POSTFIX, + TOPIC_PREFIX, get_topics, }; use libp2p::gossipsub::TopicHash; diff --git a/lean_client/networking/src/gossipsub/topic.rs b/lean_client/networking/src/gossipsub/topic.rs index 9a4e7b5..09fcd33 100644 --- a/lean_client/networking/src/gossipsub/topic.rs +++ b/lean_client/networking/src/gossipsub/topic.rs @@ -45,9 +45,7 @@ impl GossipsubTopic { let parts: Vec<&str> = topic.as_str().trim_start_matches('/').split('/').collect(); if parts.len() != 4 { - return Err(format!( - "Invalid topic part count: {topic:?}" - )); + return Err(format!("Invalid topic part count: {topic:?}")); } Ok(parts) @@ -78,10 +76,7 @@ impl std::fmt::Display for GossipsubTopic { write!( f, "/{}/{}/{}/{}", - TOPIC_PREFIX, - self.fork, - self.kind, - SSZ_SNAPPY_ENCODING_POSTFIX + TOPIC_PREFIX, self.fork, self.kind, SSZ_SNAPPY_ENCODING_POSTFIX ) } } @@ -106,10 +101,7 @@ impl From for TopicHash { }; TopicHash::from_raw(format!( "/{}/{}/{}/{}", - TOPIC_PREFIX, - val.fork, - kind_str, - SSZ_SNAPPY_ENCODING_POSTFIX + TOPIC_PREFIX, val.fork, kind_str, SSZ_SNAPPY_ENCODING_POSTFIX )) } } diff --git a/lean_client/networking/src/network/mod.rs b/lean_client/networking/src/network/mod.rs index 7609d9a..1900ed8 100644 --- a/lean_client/networking/src/network/mod.rs +++ b/lean_client/networking/src/network/mod.rs @@ -2,4 +2,4 @@ mod behaviour; mod service; pub use behaviour::{LeanNetworkBehaviour, LeanNetworkBehaviourEvent}; -pub use service::{NetworkServiceConfig, NetworkEvent, NetworkService}; +pub use service::{NetworkEvent, NetworkService, NetworkServiceConfig}; diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 93e749c..8ae5729 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -341,8 +341,8 @@ where } fn handle_request_response_event(&mut self, event: ReqRespMessage) -> Option { - use libp2p::request_response::{Event, Message}; use crate::req_resp::LeanResponse; + use libp2p::request_response::{Event, Message}; match event { Event::Message { peer, message, .. } => match message { @@ -360,16 +360,24 @@ where tokio::spawn(async move { for block in blocks { let slot = block.message.block.slot.0; - if let Err(e) = chain_sink.send( - ChainMessage::ProcessBlock { + if let Err(e) = chain_sink + .send(ChainMessage::ProcessBlock { signed_block_with_attestation: block, is_trusted: false, should_gossip: false, // Don't re-gossip requested blocks - } - ).await { - warn!(slot = slot, ?e, "Failed to send requested block to chain"); + }) + .await + { + warn!( + slot = slot, + ?e, + "Failed to send requested block to chain" + ); } else { - debug!(slot = slot, "Queued requested block for processing"); + debug!( + slot = slot, + "Queued requested block for processing" + ); } } }); @@ -382,7 +390,9 @@ where } } } - Message::Request { request, channel, .. } => { + Message::Request { + request, channel, .. + } => { use crate::req_resp::{LeanRequest, LeanResponse}; let response = match request { @@ -398,10 +408,12 @@ where } }; - if let Err(e) = self.swarm + if let Err(e) = self + .swarm .behaviour_mut() .req_resp - .send_response(channel, response) { + .send_response(channel, response) + { warn!(peer = %peer, ?e, "Failed to send response"); } } @@ -528,7 +540,7 @@ where let slot = signed_attestation.message.data.slot.0; #[cfg(feature = "devnet2")] let slot = signed_attestation.message.slot.0; - + match signed_attestation.to_ssz() { Ok(bytes) => { if let Err(err) = self.publish_to_topic(GossipsubKind::Attestation, bytes) { diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index 51d705e..bd6c414 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -2,8 +2,8 @@ use std::io; use std::io::{Read, Write}; use async_trait::async_trait; -use containers::{Bytes32, SignedBlockWithAttestation, Status}; use containers::ssz::{SszReadDefault, SszWrite}; +use containers::{Bytes32, SignedBlockWithAttestation, Status}; use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use libp2p::request_response::{ Behaviour as RequestResponse, Codec, Config, Event, ProtocolSupport, @@ -46,8 +46,9 @@ impl LeanCodec { fn compress(data: &[u8]) -> io::Result> { let mut encoder = FrameEncoder::new(Vec::new()); encoder.write_all(data)?; - encoder.into_inner() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Snappy framing failed: {e}"))) + encoder.into_inner().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("Snappy framing failed: {e}")) + }) } /// Decompress data using Snappy framing format (required for req/resp protocol) @@ -60,8 +61,9 @@ impl LeanCodec { fn encode_request(request: &LeanRequest) -> io::Result> { let ssz_bytes = match request { - LeanRequest::Status(status) => status.to_ssz() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")))?, + LeanRequest::Status(status) => status.to_ssz().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) + })?, LeanRequest::BlocksByRoot(roots) => { let mut bytes = Vec::new(); for root in roots { @@ -77,12 +79,16 @@ impl LeanCodec { if data.is_empty() { return Ok(LeanRequest::Status(Status::default())); } - + let ssz_bytes = Self::decompress(data)?; - + if protocol.contains("status") { - let status = Status::from_ssz_default(&ssz_bytes) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ decode Status failed: {e:?}")))?; + let status = Status::from_ssz_default(&ssz_bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("SSZ decode Status failed: {e:?}"), + ) + })?; Ok(LeanRequest::Status(status)) } else if protocol.contains("blocks_by_root") { let mut roots = Vec::new(); @@ -96,35 +102,44 @@ impl LeanCodec { if roots.len() > MAX_REQUEST_BLOCKS { return Err(io::Error::new( io::ErrorKind::InvalidData, - format!("Too many block roots requested: {} > {}", roots.len(), MAX_REQUEST_BLOCKS), + format!( + "Too many block roots requested: {} > {}", + roots.len(), + MAX_REQUEST_BLOCKS + ), )); } Ok(LeanRequest::BlocksByRoot(roots)) } else { - Err(io::Error::new(io::ErrorKind::Other, format!("Unknown protocol: {protocol}"))) + Err(io::Error::new( + io::ErrorKind::Other, + format!("Unknown protocol: {protocol}"), + )) } } fn encode_response(response: &LeanResponse) -> io::Result> { let ssz_bytes = match response { - LeanResponse::Status(status) => status.to_ssz() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")))?, + LeanResponse::Status(status) => status.to_ssz().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) + })?, LeanResponse::BlocksByRoot(blocks) => { let mut bytes = Vec::new(); for block in blocks { - let block_bytes = block.to_ssz() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")))?; + let block_bytes = block.to_ssz().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) + })?; bytes.extend_from_slice(&block_bytes); } bytes } LeanResponse::Empty => Vec::new(), }; - + if ssz_bytes.is_empty() { return Ok(Vec::new()); } - + Self::compress(&ssz_bytes) } @@ -132,22 +147,33 @@ impl LeanCodec { if data.is_empty() { return Ok(LeanResponse::Empty); } - + let ssz_bytes = Self::decompress(data)?; - + if protocol.contains("status") { - let status = Status::from_ssz_default(&ssz_bytes) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ decode Status failed: {e:?}")))?; + let status = Status::from_ssz_default(&ssz_bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("SSZ decode Status failed: {e:?}"), + ) + })?; Ok(LeanResponse::Status(status)) } else if protocol.contains("blocks_by_root") { if ssz_bytes.is_empty() { return Ok(LeanResponse::BlocksByRoot(Vec::new())); } - let block = SignedBlockWithAttestation::from_ssz_default(&ssz_bytes) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ decode Block failed: {e:?}")))?; + let block = SignedBlockWithAttestation::from_ssz_default(&ssz_bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("SSZ decode Block failed: {e:?}"), + ) + })?; Ok(LeanResponse::BlocksByRoot(vec![block])) } else { - Err(io::Error::new(io::ErrorKind::Other, format!("Unknown protocol: {protocol}"))) + Err(io::Error::new( + io::ErrorKind::Other, + format!("Unknown protocol: {protocol}"), + )) } } } diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index 028a883..bbe7cba 100644 --- a/lean_client/networking/src/types.rs +++ b/lean_client/networking/src/types.rs @@ -70,7 +70,9 @@ pub enum ChainMessage { } impl ChainMessage { - pub fn block_with_attestation(signed_block_with_attestation: SignedBlockWithAttestation) -> Self { + pub fn block_with_attestation( + signed_block_with_attestation: SignedBlockWithAttestation, + ) -> Self { ChainMessage::ProcessBlock { signed_block_with_attestation, is_trusted: false, @@ -90,16 +92,35 @@ impl ChainMessage { impl Display for ChainMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ChainMessage::ProcessBlock { signed_block_with_attestation, .. } => { - write!(f, "ProcessBlockWithAttestation(slot={})", signed_block_with_attestation.message.block.slot.0) + ChainMessage::ProcessBlock { + signed_block_with_attestation, + .. + } => { + write!( + f, + "ProcessBlockWithAttestation(slot={})", + signed_block_with_attestation.message.block.slot.0 + ) } #[cfg(feature = "devnet1")] - ChainMessage::ProcessAttestation { signed_attestation, .. } => { - write!(f, "ProcessAttestation(slot={})", signed_attestation.message.data.slot.0) + ChainMessage::ProcessAttestation { + signed_attestation, .. + } => { + write!( + f, + "ProcessAttestation(slot={})", + signed_attestation.message.data.slot.0 + ) } #[cfg(feature = "devnet2")] - ChainMessage::ProcessAttestation { signed_attestation, .. } => { - write!(f, "ProcessAttestation(slot={})", signed_attestation.message.slot.0) + ChainMessage::ProcessAttestation { + signed_attestation, .. + } => { + write!( + f, + "ProcessAttestation(slot={})", + signed_attestation.message.slot.0 + ) } } } diff --git a/lean_client/validator/src/keys.rs b/lean_client/validator/src/keys.rs index cae38f1..7680102 100644 --- a/lean_client/validator/src/keys.rs +++ b/lean_client/validator/src/keys.rs @@ -1,8 +1,8 @@ +use containers::attestation::U3112; +use containers::ssz::ByteVector; +use containers::Signature; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use containers::Signature; -use containers::ssz::ByteVector; -use containers::attestation::U3112; use tracing::info; #[cfg(feature = "xmss-signing")] @@ -42,7 +42,9 @@ impl KeyManager { /// Load a secret key for a specific validator index pub fn load_key(&mut self, validator_index: u64) -> Result<(), Box> { - let sk_path = self.keys_dir.join(format!("validator_{}_sk.ssz", validator_index)); + let sk_path = self + .keys_dir + .join(format!("validator_{}_sk.ssz", validator_index)); if !sk_path.exists() { return Err(format!("Secret key file not found: {:?}", sk_path).into()); @@ -69,20 +71,20 @@ impl KeyManager { ) -> Result> { #[cfg(feature = "xmss-signing")] { - let key_bytes = self.keys + let key_bytes = self + .keys .get(&validator_index) .ok_or_else(|| format!("No key loaded for validator {}", validator_index))?; - type SecretKey = ::SecretKey; + type SecretKey = + ::SecretKey; let secret_key = SecretKey::from_bytes(key_bytes) .map_err(|e| format!("Failed to deserialize secret key: {:?}", e))?; - let leansig_signature = SIGTopLevelTargetSumLifetime32Dim64Base8::sign( - &secret_key, - epoch, - message, - ).map_err(|e| format!("Failed to sign message: {:?}", e))?; + let leansig_signature = + SIGTopLevelTargetSumLifetime32Dim64Base8::sign(&secret_key, epoch, message) + .map_err(|e| format!("Failed to sign message: {:?}", e))?; let sig_bytes = leansig_signature.to_bytes(); @@ -90,7 +92,8 @@ impl KeyManager { return Err(format!( "Invalid signature size: expected 3112, got {}", sig_bytes.len() - ).into()); + ) + .into()); } // Convert to ByteVector using unsafe pointer copy (same pattern as PublicKey) @@ -105,7 +108,7 @@ impl KeyManager { #[cfg(not(feature = "xmss-signing"))] { - let _ = (epoch, message); // Suppress unused warnings + let _ = (epoch, message); // Suppress unused warnings warn!( validator = validator_index, "XMSS signing disabled - using zero signature" diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 2c65fa7..752cda8 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use std::path::Path; -use containers::attestation::{AggregatedAttestations}; +use containers::attestation::AggregatedAttestations; #[cfg(feature = "devnet2")] -use containers::attestation::{NaiveAggregatedSignature}; +use containers::attestation::NaiveAggregatedSignature; use containers::block::BlockSignatures; use containers::{ attestation::{Attestation, AttestationData, Signature, SignedAttestation}, From 12f2def95193c7670fde806d3fa48089fc1bcf66 Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Sun, 18 Jan 2026 15:18:36 +0200 Subject: [PATCH 17/24] Implemented fixed-point attestation collection and signature aggregation --- lean_client/containers/src/attestation.rs | 98 +++++++- lean_client/containers/src/state.rs | 282 +++++++++++++++++++++- lean_client/fork_choice/src/store.rs | 77 ++++++ lean_client/validator/src/lib.rs | 57 +++-- 4 files changed, 476 insertions(+), 38 deletions(-) diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 291a8f0..5946417 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -67,30 +67,98 @@ impl MultisigAggregatedSignature { self.0.as_bytes().is_empty() } + /// Aggregate individual XMSS signatures into a single proof. + /// + /// Uses lean-multisig zkVM to combine multiple signatures into a compact proof. + /// + /// # Arguments + /// * `public_keys` - Public keys of the signers + /// * `signatures` - Individual XMSS signatures to aggregate + /// * `message` - The 32-byte message that was signed (as 8 field elements) + /// * `epoch` - The epoch/slot in which signatures were created + /// + /// # Returns + /// Aggregated signature proof, or error if aggregation fails. + pub fn aggregate( + public_keys: &[lean_multisig::XmssPublicKey], + signatures: &[lean_multisig::XmssSignature], + message: [lean_multisig::F; 8], + epoch: u64, + ) -> Result { + if public_keys.is_empty() { + return Err(AggregationError::EmptyInput); + } + if public_keys.len() != signatures.len() { + return Err(AggregationError::MismatchedLengths); + } + + let proof_bytes = lean_multisig::xmss_aggregate_signatures( + public_keys, + signatures, + message, + epoch, + ).map_err(|_| AggregationError::AggregationFailed)?; + + Ok(Self::new(proof_bytes)) + } + /// Verify the aggregated signature proof against the given public keys and message. - /// This uses the lean-multisig zkVM to verify that the aggregated proof is valid + /// + /// Uses lean-multisig zkVM to verify that the aggregated proof is valid /// for all the given public keys signing the same message at the given epoch. /// + /// # Returns /// `Ok(())` if the proof is valid, `Err` with the proof error otherwise. - #[cfg(feature = "xmss-verify")] - pub fn verify_aggregated_payload( + pub fn verify( &self, public_keys: &[lean_multisig::XmssPublicKey], message: [lean_multisig::F; 8], epoch: u64, - ) -> Result<(), lean_multisig::ProofError> { + ) -> Result<(), AggregationError> { lean_multisig::xmss_verify_aggregated_signatures( public_keys, message, self.0.as_bytes(), epoch, - ) + ).map_err(|_| AggregationError::VerificationFailed) } - /// Stub verification when xmss-verify feature is disabled. + /// Verify the aggregated payload against validators and message. + /// + /// This is a convenience method that extracts public keys from validators + /// and converts the message bytes to the field element format expected by lean-multisig. + /// + /// # Arguments + /// * `validators` - Slice of validator references to extract public keys from + /// * `message` - 32-byte message (typically attestation data root) + /// * `epoch` - Epoch/slot for proof verification + /// + /// # Returns + /// `Ok(())` if verification succeeds, `Err` otherwise. + pub fn verify_aggregated_payload( + &self, + validators: &[&crate::validator::Validator], + message: &[u8; 32], + epoch: u64, + ) -> Result<(), AggregationError> { + // NOTE: This stub matches Python leanSpec behavior (test_mode=True). + // Python also uses test_mode=True with TODO: "Remove test_mode once leanVM + // supports correct signature encoding." + // See: src/lean_spec/subspecs/xmss/aggregation.py + // + // Once leanVM/lean-multisig supports proper signature encoding: + // 1. Extract public keys from validators + // 2. Convert message bytes to field element format + // 3. Call lean_multisig::xmss_verify_aggregated_signatures + let _ = (validators, message, epoch); + + Ok(()) + } + + /// Stub verification when lean-multisig is not available. /// Always returns Ok(()) for testing without cryptographic verification. - #[cfg(not(feature = "xmss-verify"))] - pub fn verify_aggregated_payload( + #[cfg(not(feature = "devnet2"))] + pub fn verify_stub( &self, _public_keys: &[P], _message: M, @@ -100,6 +168,20 @@ impl MultisigAggregatedSignature { } } +/// Error types for signature aggregation operations. +#[cfg(feature = "devnet2")] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AggregationError { + /// No signatures provided for aggregation. + EmptyInput, + /// Public keys and signatures arrays have different lengths. + MismatchedLengths, + /// Aggregation failed in lean-multisig. + AggregationFailed, + /// Verification of aggregated proof failed. + VerificationFailed, +} + /// Bitlist representing validator participation in an attestation. /// Limit is VALIDATOR_REGISTRY_LIMIT (4096). #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 2c09215..fed6e08 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -695,14 +695,280 @@ impl State { #[cfg(feature = "devnet2")] pub fn build_block( &self, - _slot: Slot, - _proposer_index: ValidatorIndex, - _parent_root: Bytes32, - _initial_attestations: Option>, - _available_signed_attestations: Option<&[SignedAttestation]>, - _known_block_roots: Option<&std::collections::HashSet>, - ) -> Result<(Block, Self, Vec, BlockSignatures), String> { - Err("build_block is not implemented for devnet2".to_string()) + slot: Slot, + proposer_index: ValidatorIndex, + parent_root: Bytes32, + initial_attestations: Option>, + available_attestations: Option>, + known_block_roots: Option<&std::collections::HashSet>, + gossip_signatures: Option<&std::collections::HashMap>, + aggregated_payloads: Option<&std::collections::HashMap>>, + ) -> Result<(Block, Self, Vec, Vec), String> { + use crate::attestation::{AggregatedAttestation, SignatureKey}; + + // Initialize attestation set + let mut attestations = initial_attestations.unwrap_or_default(); + + // Advance state to target slot + let pre_state = self.process_slots(slot)?; + + // Fixed-point attestation collection loop + // Iteratively add valid attestations until no new ones can be added + loop { + // Create candidate block with current attestation set + let aggregated = AggregatedAttestation::aggregate_by_data(&attestations); + let mut attestations_list = AggregatedAttestations::default(); + for att in &aggregated { + attestations_list + .push(att.clone()) + .map_err(|e| format!("Failed to push attestation: {:?}", e))?; + } + + let candidate_block = Block { + slot, + proposer_index, + parent_root, + state_root: Bytes32(ssz::H256::zero()), + body: BlockBody { + attestations: attestations_list, + }, + }; + + // Apply state transition to get the post-block state + let post_state = pre_state.process_block(&candidate_block)?; + + // If no available attestations pool, skip fixed-point iteration + let available = match &available_attestations { + Some(avail) => avail, + None => { + // No fixed-point: compute signatures and return + let (aggregated_attestations, aggregated_proofs) = self.compute_aggregated_signatures( + &attestations, + gossip_signatures, + aggregated_payloads, + )?; + + let mut final_attestations_list = AggregatedAttestations::default(); + for att in &aggregated_attestations { + final_attestations_list + .push(att.clone()) + .map_err(|e| format!("Failed to push attestation: {:?}", e))?; + } + + let final_block = Block { + slot, + proposer_index, + parent_root, + state_root: hash_tree_root(&post_state), + body: BlockBody { + attestations: final_attestations_list, + }, + }; + + return Ok((final_block, post_state, aggregated_attestations, aggregated_proofs)); + } + }; + + // Find new valid attestations from available pool + let mut new_attestations: Vec = Vec::new(); + let current_data_roots: std::collections::HashSet<_> = attestations + .iter() + .map(|a| a.data.data_root_bytes()) + .collect(); + + for attestation in available { + // Skip if already included + if current_data_roots.contains(&attestation.data.data_root_bytes()) { + continue; + } + + // Validate attestation against post-state + // Source must match post-state's justified checkpoint + if attestation.data.source != post_state.latest_justified { + continue; + } + + // Target must be after source + if attestation.data.target.slot <= attestation.data.source.slot { + continue; + } + + // Target block must be known (if known_block_roots provided) + if let Some(known_roots) = known_block_roots { + if !known_roots.contains(&attestation.data.target.root) { + continue; + } + } + + // Check if we have a signature for this attestation + let data_root = attestation.data.data_root_bytes(); + let sig_key = SignatureKey::new(attestation.validator_id.0, data_root); + let has_gossip_sig = gossip_signatures.map_or(false, |gs| gs.contains_key(&sig_key)); + let has_block_proof = aggregated_payloads.map_or(false, |ap| ap.contains_key(&sig_key)); + + if has_gossip_sig || has_block_proof { + new_attestations.push(attestation.clone()); + } + } + + // Fixed point reached: no new attestations found + if new_attestations.is_empty() { + // Compute aggregated signatures + let (aggregated_attestations, aggregated_proofs) = self.compute_aggregated_signatures( + &attestations, + gossip_signatures, + aggregated_payloads, + )?; + + let mut final_attestations_list = AggregatedAttestations::default(); + for att in &aggregated_attestations { + final_attestations_list + .push(att.clone()) + .map_err(|e| format!("Failed to push attestation: {:?}", e))?; + } + + let final_block = Block { + slot, + proposer_index, + parent_root, + state_root: hash_tree_root(&post_state), + body: BlockBody { + attestations: final_attestations_list, + }, + }; + + return Ok((final_block, post_state, aggregated_attestations, aggregated_proofs)); + } + + // Add new attestations and continue iteration + attestations.extend(new_attestations); + } + } + + /// Compute aggregated signatures for a set of attestations using a two-phase algorithm. + /// + /// # Phase 1: Gossip Collection + /// For each attestation group, attempt to collect individual XMSS signatures from + /// the gossip network. These are fresh signatures that validators broadcast. + /// + /// # Phase 2: Fallback to Block Proofs + /// For validators not covered by gossip, fall back to previously-seen aggregated + /// proofs from blocks using a greedy set-cover approach. + /// + /// # Returns + /// Tuple of (aggregated attestations, corresponding proofs) + #[cfg(feature = "devnet2")] + pub fn compute_aggregated_signatures( + &self, + attestations: &[Attestation], + gossip_signatures: Option<&std::collections::HashMap>, + aggregated_payloads: Option<&std::collections::HashMap>>, + ) -> Result<(Vec, Vec), String> { + use crate::attestation::{AggregatedAttestation, AggregationBits, SignatureKey}; + use std::collections::HashSet; + + let mut results: Vec<(AggregatedAttestation, crate::MultisigAggregatedSignature)> = Vec::new(); + + // Group individual attestations by data + for aggregated in AggregatedAttestation::aggregate_by_data(attestations) { + let data = &aggregated.data; + let data_root = data.data_root_bytes(); + let validator_ids = aggregated.aggregation_bits.to_validator_indices(); + + // Phase 1: Gossip Collection + // Try to collect individual signatures from gossip network + let mut gossip_ids: Vec = Vec::new(); + let mut gossip_sigs_collected: Vec = Vec::new(); + let mut remaining: HashSet = HashSet::new(); + + if let Some(gossip_sigs) = gossip_signatures { + for vid in &validator_ids { + let key = SignatureKey::new(*vid, data_root); + if let Some(sig) = gossip_sigs.get(&key) { + gossip_ids.push(*vid); + gossip_sigs_collected.push(sig.clone()); + } else { + remaining.insert(*vid); + } + } + } else { + // No gossip data: all validators need fallback + remaining = validator_ids.iter().copied().collect(); + } + + // If we collected any gossip signatures, create an aggregated proof + if !gossip_ids.is_empty() { + let participants = AggregationBits::from_validator_indices(&gossip_ids); + + // TODO: Actually aggregate signatures using lean-multisig when we have + // the proper public key and signature conversion + // For now, create empty proof placeholder + let proof = crate::MultisigAggregatedSignature::new(Vec::new()); + + results.push(( + AggregatedAttestation { + aggregation_bits: participants, + data: data.clone(), + }, + proof, + )); + } + + // Phase 2: Fallback to block proofs using greedy set-cover + // Goal: Cover remaining validators with minimum number of proofs + while !remaining.is_empty() { + let payloads = match aggregated_payloads { + Some(p) => p, + None => break, + }; + + // Pick any remaining validator to find candidate proofs + let target_id = *remaining.iter().next().unwrap(); + let key = SignatureKey::new(target_id, data_root); + + let candidates = match payloads.get(&key) { + Some(proofs) if !proofs.is_empty() => proofs, + _ => break, // No proofs found for this validator + }; + + // Greedy selection: find proof covering most remaining validators + // We need to check each candidate to see which validators it covers + // For now, we use a simplified approach: each proof in aggregated_payloads + // is assumed to cover a set of validators that we track + + // TODO: Extract actual participants from proof metadata + // For now, assume each proof covers the validators it's indexed by + // Use simplified single-validator coverage + let best_proof = &candidates[0]; + let covered_validators = vec![target_id]; + + // Create attestation with this proof's participants + let participants = AggregationBits::from_validator_indices(&covered_validators); + results.push(( + AggregatedAttestation { + aggregation_bits: participants, + data: data.clone(), + }, + best_proof.clone(), + )); + + // Remove covered validators from remaining + for vid in &covered_validators { + remaining.remove(vid); + } + } + } + + // Handle empty case + if results.is_empty() { + return Ok((Vec::new(), Vec::new())); + } + + // Unzip results into parallel lists + let (aggregated_attestations, aggregated_proofs): (Vec<_>, Vec<_>) = + results.into_iter().unzip(); + + Ok((aggregated_attestations, aggregated_proofs)) } } diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 5fa6b9b..96d5700 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -273,3 +273,80 @@ pub fn get_proposal_head(store: &mut Store, slot: Slot) -> Root { accept_new_attestations(store); store.head } + +/// Produce a block and aggregated signature proofs for the target slot. +/// +/// The proposer returns the block and `MultisigAggregatedSignature` proofs aligned +/// with `block.body.attestations` so it can craft `SignedBlockWithAttestation`. +/// +/// # Algorithm Overview +/// 1. **Get Proposal Head**: Retrieve current chain head as parent +/// 2. **Collect Attestations**: Convert known attestations to plain attestations +/// 3. **Build Block**: Use State.build_block with signature caches +/// 4. **Store Block**: Insert block and post-state into Store +/// +/// # Arguments +/// * `store` - Mutable reference to the fork choice store +/// * `slot` - Target slot number for block production +/// * `validator_index` - Index of validator authorized to propose this block +/// +/// # Returns +/// Tuple of (block root, finalized Block, attestation signature proofs) +#[cfg(feature = "devnet2")] +pub fn produce_block_with_signatures( + store: &mut Store, + slot: Slot, + validator_index: ValidatorIndex, +) -> Result<(Root, containers::block::Block, Vec), String> { + use containers::Attestation; + + // Get parent block head + let head_root = get_proposal_head(store, slot); + let head_state = store.states.get(&head_root) + .ok_or_else(|| "Head state not found".to_string())? + .clone(); + + // Validate proposer authorization for this slot + let num_validators = head_state.validators.len_u64(); + let expected_proposer = slot.0 % num_validators; + if validator_index.0 != expected_proposer { + return Err(format!( + "Validator {} is not the proposer for slot {} (expected {})", + validator_index.0, slot.0, expected_proposer + )); + } + + // Convert AttestationData to Attestation objects for build_block + let available_attestations: Vec = store.latest_known_attestations + .iter() + .map(|(validator_idx, signed_att)| { + Attestation { + validator_id: containers::Uint64(validator_idx.0), + data: signed_att.message.clone(), + } + }) + .collect(); + + // Get known block roots for attestation validation + let known_block_roots: std::collections::HashSet = store.blocks.keys().copied().collect(); + + // Build block with fixed-point attestation collection and signature aggregation + let (final_block, final_post_state, _aggregated_attestations, signatures) = head_state.build_block( + slot, + validator_index, + head_root, + None, // initial_attestations - start with empty, let fixed-point collect + Some(available_attestations), + Some(&known_block_roots), + Some(&store.gossip_signatures), + Some(&store.aggregated_payloads), + )?; + + // Compute block root + let block_root = Bytes32(final_block.hash_tree_root()); + + // Store block and state + store.states.insert(block_root, final_post_state); + + Ok((block_root, final_block, signatures)) +} diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 93e9429..0e9e5a7 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -6,6 +6,8 @@ use containers::attestation::{AggregatedAttestations}; #[cfg(feature = "devnet2")] use containers::attestation::MultisigAggregatedSignature; use containers::block::BlockSignatures; +#[cfg(feature = "devnet2")] +use containers::ssz; use containers::{ attestation::{Attestation, AttestationData, Signature, SignedAttestation}, block::{hash_tree_root, BlockWithAttestation, SignedBlockWithAttestation}, @@ -219,6 +221,8 @@ impl ValidatorService { Some(valid_attestations), None, None, + None, + None, )?; #[cfg(feature = "devnet2")] let (block, _post_state, _collected_atts, sigs) = { @@ -236,30 +240,23 @@ impl ValidatorService { Some(valid_attestations), None, None, + None, + None, )? }; - // Collect signatures from the attestations we included #[cfg(not(feature = "devnet2"))] let mut signatures = sigs; - #[cfg(feature = "devnet2")] - let mut signatures = sigs.attestation_signatures; + #[cfg(not(feature = "devnet2"))] for signed_att in &valid_signed_attestations { - #[cfg(not(feature = "devnet2"))] signatures .push(signed_att.signature.clone()) .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; - #[cfg(feature = "devnet2")] - { - // TODO: Use real lean-multisig aggregation once we have individual signatures - // For now, use an empty proof placeholder that verify_aggregated_payload will accept - let aggregated_sig = MultisigAggregatedSignature::default(); - signatures - .push(aggregated_sig) - .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; - } } + #[cfg(feature = "devnet2")] + let signatures = sigs; + info!( slot = block.slot.0, proposer = block.proposer_index.0, @@ -270,6 +267,11 @@ impl ValidatorService { ); // Sign the proposer attestation + #[cfg(not(feature = "devnet2"))] + let proposer_signature: Signature; + #[cfg(feature = "devnet2")] + let proposer_signature: Signature; + if let Some(ref key_manager) = self.key_manager { // Sign proposer attestation with XMSS let message = hash_tree_root(&proposer_attestation); @@ -278,16 +280,15 @@ impl ValidatorService { match key_manager.sign(proposer_index.0, epoch, &message.0.into()) { Ok(sig) => { #[cfg(not(feature = "devnet2"))] - signatures - .push(sig) - .map_err(|e| format!("Failed to add proposer signature: {:?}", e))?; - #[cfg(feature = "devnet2")] { - // TODO: Use real lean-multisig aggregation for proposer attestation - let aggregated_sig = MultisigAggregatedSignature::default(); signatures - .push(aggregated_sig) + .push(sig.clone()) .map_err(|e| format!("Failed to add proposer signature: {:?}", e))?; + proposer_signature = sig; + } + #[cfg(feature = "devnet2")] + { + proposer_signature = sig; } info!(proposer = proposer_index.0, "Signed proposer attestation"); } @@ -298,8 +299,20 @@ impl ValidatorService { } else { // No key manager - use zero signature warn!("Building block with zero signature (no key manager)"); + proposer_signature = Signature::default(); } + // Convert signatures to PersistentList for BlockSignatures + #[cfg(feature = "devnet2")] + let attestation_signatures = { + let mut list = ssz::PersistentList::default(); + for sig in signatures { + list.push(sig) + .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; + } + list + }; + let signed_block = SignedBlockWithAttestation { message: BlockWithAttestation { block, @@ -309,8 +322,8 @@ impl ValidatorService { signature: signatures, #[cfg(feature = "devnet2")] signature: BlockSignatures { - attestation_signatures: signatures, - proposer_signature: Signature::default(), + attestation_signatures, + proposer_signature, }, }; From da32620615f58e01ea6d69d746b66d5cf3a1dc51 Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Sun, 18 Jan 2026 15:31:16 +0200 Subject: [PATCH 18/24] added AggregatedSignatureProof type for signature aggregation --- lean_client/containers/src/attestation.rs | 37 ++++++++++++- lean_client/containers/src/lib.rs | 2 +- lean_client/containers/src/state.rs | 65 +++++++++++++++-------- lean_client/fork_choice/src/handlers.rs | 10 +++- lean_client/fork_choice/src/store.rs | 6 +-- lean_client/validator/src/lib.rs | 5 +- 6 files changed, 92 insertions(+), 33 deletions(-) diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 5946417..fc776d9 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -144,8 +144,6 @@ impl MultisigAggregatedSignature { // NOTE: This stub matches Python leanSpec behavior (test_mode=True). // Python also uses test_mode=True with TODO: "Remove test_mode once leanVM // supports correct signature encoding." - // See: src/lean_spec/subspecs/xmss/aggregation.py - // // Once leanVM/lean-multisig supports proper signature encoding: // 1. Extract public keys from validators // 2. Convert message bytes to field element format @@ -182,6 +180,41 @@ pub enum AggregationError { VerificationFailed, } +/// Aggregated signature proof with participant tracking. +/// +/// This type combines the participant bitfield with the proof bytes, +/// matches Python's `AggregatedSignatureProof` container structure. +/// Used in `aggregated_payloads` to track which validators are covered by each proof. +#[cfg(feature = "devnet2")] +#[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] +pub struct AggregatedSignatureProof { + /// Bitfield indicating which validators' signatures are included. + pub participants: AggregationBits, + /// The raw aggregated proof bytes from lean-multisig. + pub proof_data: MultisigAggregatedSignature, +} + +#[cfg(feature = "devnet2")] +impl AggregatedSignatureProof { + /// Create a new AggregatedSignatureProof. + pub fn new(participants: AggregationBits, proof_data: MultisigAggregatedSignature) -> Self { + Self { participants, proof_data } + } + + + pub fn from_aggregation(participant_ids: &[u64], proof: MultisigAggregatedSignature) -> Self { + Self { + participants: AggregationBits::from_validator_indices(participant_ids), + proof_data: proof, + } + } + + /// Get the validator indices covered by this proof. + pub fn get_participant_indices(&self) -> Vec { + self.participants.to_validator_indices() + } +} + /// Bitlist representing validator participation in an attestation. /// Limit is VALIDATOR_REGISTRY_LIMIT (4096). #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index 4945252..f8fd48a 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -15,7 +15,7 @@ pub use attestation::{ }; #[cfg(feature = "devnet2")] -pub use attestation::MultisigAggregatedSignature; +pub use attestation::{AggregatedSignatureProof, MultisigAggregatedSignature}; pub use block::{ Block, BlockBody, BlockHeader, BlockWithAttestation, SignedBlock, SignedBlockWithAttestation, }; diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index fed6e08..27550f0 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -702,8 +702,8 @@ impl State { available_attestations: Option>, known_block_roots: Option<&std::collections::HashSet>, gossip_signatures: Option<&std::collections::HashMap>, - aggregated_payloads: Option<&std::collections::HashMap>>, - ) -> Result<(Block, Self, Vec, Vec), String> { + aggregated_payloads: Option<&std::collections::HashMap>>, + ) -> Result<(Block, Self, Vec, Vec), String> { use crate::attestation::{AggregatedAttestation, SignatureKey}; // Initialize attestation set @@ -862,12 +862,12 @@ impl State { &self, attestations: &[Attestation], gossip_signatures: Option<&std::collections::HashMap>, - aggregated_payloads: Option<&std::collections::HashMap>>, - ) -> Result<(Vec, Vec), String> { + aggregated_payloads: Option<&std::collections::HashMap>>, + ) -> Result<(Vec, Vec), String> { use crate::attestation::{AggregatedAttestation, AggregationBits, SignatureKey}; use std::collections::HashSet; - let mut results: Vec<(AggregatedAttestation, crate::MultisigAggregatedSignature)> = Vec::new(); + let mut results: Vec<(AggregatedAttestation, crate::AggregatedSignatureProof)> = Vec::new(); // Group individual attestations by data for aggregated in AggregatedAttestation::aggregate_by_data(attestations) { @@ -878,7 +878,7 @@ impl State { // Phase 1: Gossip Collection // Try to collect individual signatures from gossip network let mut gossip_ids: Vec = Vec::new(); - let mut gossip_sigs_collected: Vec = Vec::new(); + let mut _gossip_sigs_collected: Vec = Vec::new(); let mut remaining: HashSet = HashSet::new(); if let Some(gossip_sigs) = gossip_signatures { @@ -886,7 +886,7 @@ impl State { let key = SignatureKey::new(*vid, data_root); if let Some(sig) = gossip_sigs.get(&key) { gossip_ids.push(*vid); - gossip_sigs_collected.push(sig.clone()); + _gossip_sigs_collected.push(sig.clone()); } else { remaining.insert(*vid); } @@ -897,13 +897,18 @@ impl State { } // If we collected any gossip signatures, create an aggregated proof + // NOTE: This matches Python leanSpec behavior (test_mode=True). + // Python also uses test_mode=True with TODO: "Remove test_mode once leanVM + // supports correct signature encoding." + // Once lean-multisig is fully integrated, this will call: + // MultisigAggregatedSignature::aggregate(public_keys, signatures, message, epoch) if !gossip_ids.is_empty() { let participants = AggregationBits::from_validator_indices(&gossip_ids); - // TODO: Actually aggregate signatures using lean-multisig when we have - // the proper public key and signature conversion - // For now, create empty proof placeholder - let proof = crate::MultisigAggregatedSignature::new(Vec::new()); + // Create proof placeholder (matches Python test_mode behavior) + // TODO: Call actual aggregation when lean-multisig supports proper encoding + let proof_data = crate::MultisigAggregatedSignature::new(Vec::new()); + let proof = crate::AggregatedSignatureProof::new(participants.clone(), proof_data); results.push(( AggregatedAttestation { @@ -932,18 +937,32 @@ impl State { }; // Greedy selection: find proof covering most remaining validators - // We need to check each candidate to see which validators it covers - // For now, we use a simplified approach: each proof in aggregated_payloads - // is assumed to cover a set of validators that we track - - // TODO: Extract actual participants from proof metadata - // For now, assume each proof covers the validators it's indexed by - // Use simplified single-validator coverage - let best_proof = &candidates[0]; - let covered_validators = vec![target_id]; - - // Create attestation with this proof's participants + // For each candidate proof, compute intersection with remaining validators + let (best_proof, covered_set) = candidates + .iter() + .map(|proof| { + let proof_validators: HashSet = proof + .get_participant_indices() + .into_iter() + .collect(); + let intersection: HashSet = remaining + .intersection(&proof_validators) + .copied() + .collect(); + (proof, intersection) + }) + .max_by_key(|(_, intersection)| intersection.len()) + .expect("candidates is non-empty"); + + // Guard: If best proof has zero overlap, stop + if covered_set.is_empty() { + break; + } + + // Record proof with its actual participants (from the proof itself) + let covered_validators: Vec = best_proof.get_participant_indices(); let participants = AggregationBits::from_validator_indices(&covered_validators); + results.push(( AggregatedAttestation { aggregation_bits: participants, @@ -953,7 +972,7 @@ impl State { )); // Remove covered validators from remaining - for vid in &covered_validators { + for vid in &covered_set { remaining.remove(vid); } } diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index a4d24c4..aa82c31 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -256,7 +256,13 @@ fn process_block_internal( let data_root = aggregated_attestation.data.data_root_bytes(); // Get the corresponding proof from attestation_signatures - if let Ok(proof) = signatures.attestation_signatures.get(att_idx as u64) { + if let Ok(proof_data) = signatures.attestation_signatures.get(att_idx as u64) { + // Create AggregatedSignatureProof with participants and proof data + let aggregated_proof = containers::AggregatedSignatureProof::new( + aggregated_attestation.aggregation_bits.clone(), + proof_data.clone(), + ); + // Store proof for each validator in the aggregation for (bit_idx, bit) in aggregated_attestation.aggregation_bits.0.iter().enumerate() { if *bit { @@ -265,7 +271,7 @@ fn process_block_internal( store.aggregated_payloads .entry(sig_key) .or_insert_with(Vec::new) - .push(proof.clone()); + .push(aggregated_proof.clone()); } } } diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 96d5700..763f880 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -4,7 +4,7 @@ use containers::{ Bytes32, Root, Slot, ValidatorIndex, }; #[cfg(feature = "devnet2")] -use containers::{MultisigAggregatedSignature, Signature, SignatureKey}; +use containers::{AggregatedSignatureProof, Signature, SignatureKey}; use ssz::SszHash; use std::collections::HashMap; pub type Interval = u64; @@ -30,7 +30,7 @@ pub struct Store { pub gossip_signatures: HashMap, #[cfg(feature = "devnet2")] - pub aggregated_payloads: HashMap>, + pub aggregated_payloads: HashMap>, } pub fn get_forkchoice_store( @@ -297,7 +297,7 @@ pub fn produce_block_with_signatures( store: &mut Store, slot: Slot, validator_index: ValidatorIndex, -) -> Result<(Root, containers::block::Block, Vec), String> { +) -> Result<(Root, containers::block::Block, Vec), String> { use containers::Attestation; // Get parent block head diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 0e9e5a7..8a44c14 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -303,11 +303,12 @@ impl ValidatorService { } // Convert signatures to PersistentList for BlockSignatures + // Extract proof_data from AggregatedSignatureProof for wire format #[cfg(feature = "devnet2")] let attestation_signatures = { let mut list = ssz::PersistentList::default(); - for sig in signatures { - list.push(sig) + for proof in signatures { + list.push(proof.proof_data) .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; } list From ceb4407431d0a65d7ccaaad2c0e85139e9f35d6c Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Sun, 18 Jan 2026 16:07:38 +0200 Subject: [PATCH 19/24] Removed feature flags and devnet1 code --- lean_client/Cargo.toml | 4 +- lean_client/containers/Cargo.toml | 4 +- lean_client/containers/src/attestation.rs | 26 - lean_client/containers/src/block.rs | 103 +- lean_client/containers/src/lib.rs | 1 - lean_client/containers/src/serde_helpers.rs | 138 +-- lean_client/containers/src/state.rs | 412 +------- .../tests/test_vectors/block_processing.rs | 25 +- .../containers/tests/test_vectors/mod.rs | 9 +- .../containers/tests/test_vectors/runner.rs | 9 +- .../tests/test_vectors/verify_signatures.rs | 12 +- .../unit_tests/attestation_aggregation.rs | 1 - .../containers/tests/unit_tests/common.rs | 28 +- .../containers/tests/unit_tests/mod.rs | 6 +- .../tests/unit_tests/state_basic.rs | 9 +- .../tests/unit_tests/state_justifications.rs | 8 +- .../tests/unit_tests/state_process.rs | 17 +- .../tests/unit_tests/state_transition.rs | 13 +- lean_client/env-config/Cargo.toml | 4 - lean_client/fork_choice/Cargo.toml | 2 - lean_client/fork_choice/src/handlers.rs | 227 ++--- lean_client/fork_choice/src/store.rs | 9 - .../tests/fork_choice_test_vectors.rs | 884 +----------------- .../fork_choice/tests/unit_tests/votes.rs | 253 +---- lean_client/networking/Cargo.toml | 2 - lean_client/networking/src/network/service.rs | 6 - lean_client/networking/src/types.rs | 5 - lean_client/src/main.rs | 32 - lean_client/validator/Cargo.toml | 2 - lean_client/validator/src/lib.rs | 92 +- 30 files changed, 197 insertions(+), 2146 deletions(-) diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index 9e72c43..492d348 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -52,10 +52,8 @@ version = "0.1.0" edition = "2021" [features] -default = ["devnet2", "xmss-signing"] +default = ["xmss-signing"] xmss-signing = ["validator/xmss-signing"] -devnet1 = ["containers/devnet1", "fork-choice/devnet1", "networking/devnet1", "validator/devnet1"] -devnet2 = ["containers/devnet2", "fork-choice/devnet2", "networking/devnet2", "validator/devnet2"] [dependencies] chain = { path = "./chain" } diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index fff5b70..892dd4e 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -6,8 +6,6 @@ edition = "2021" [features] xmss-verify = ["leansig"] default = [] -devnet1 = ["env-config/devnet1"] -devnet2 = ["env-config/devnet2", "dep:lean-multisig"] [lib] name = "containers" @@ -24,7 +22,7 @@ serde_yaml = "0.9" hex = "0.4.3" sha2 = "0.10" leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "main", optional = true } -lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig", branch = "main", optional = true } +lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig", branch = "main" } [dev-dependencies] rstest = "0.18" diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index fc776d9..fb42916 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -25,15 +25,10 @@ pub type Attestations = ssz::PersistentList; pub type AggregatedAttestations = ssz::PersistentList; -#[cfg(not(feature = "devnet2"))] -pub type AttestationSignatures = ssz::PersistentList; - -#[cfg(feature = "devnet2")] pub type AttestationSignatures = ssz::PersistentList; /// Legacy naive aggregated signature type (list of individual XMSS signatures). /// Kept for backwards compatibility but no longer used in wire format. -#[cfg(feature = "devnet2")] pub type NaiveAggregatedSignature = ssz::PersistentList; /// Aggregated signature proof from lean-multisig zkVM. @@ -41,7 +36,6 @@ pub type NaiveAggregatedSignature = ssz::PersistentList; /// This is a variable-length byte list (up to 1 MiB) containing the serialized /// proof bytes from `xmss_aggregate_signatures()`. The `#[ssz(transparent)]` /// attribute makes this type serialize directly as a ByteList for SSZ wire format. -#[cfg(feature = "devnet2")] #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] #[ssz(transparent)] pub struct MultisigAggregatedSignature( @@ -50,7 +44,6 @@ pub struct MultisigAggregatedSignature( pub ssz::ByteList, ); -#[cfg(feature = "devnet2")] impl MultisigAggregatedSignature { /// Create a new MultisigAggregatedSignature from proof bytes. pub fn new(proof: Vec) -> Self { @@ -152,22 +145,9 @@ impl MultisigAggregatedSignature { Ok(()) } - - /// Stub verification when lean-multisig is not available. - /// Always returns Ok(()) for testing without cryptographic verification. - #[cfg(not(feature = "devnet2"))] - pub fn verify_stub( - &self, - _public_keys: &[P], - _message: M, - _epoch: u64, - ) -> Result<(), ()> { - Ok(()) - } } /// Error types for signature aggregation operations. -#[cfg(feature = "devnet2")] #[derive(Debug, Clone, PartialEq, Eq)] pub enum AggregationError { /// No signatures provided for aggregation. @@ -185,7 +165,6 @@ pub enum AggregationError { /// This type combines the participant bitfield with the proof bytes, /// matches Python's `AggregatedSignatureProof` container structure. /// Used in `aggregated_payloads` to track which validators are covered by each proof. -#[cfg(feature = "devnet2")] #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] pub struct AggregatedSignatureProof { /// Bitfield indicating which validators' signatures are included. @@ -194,7 +173,6 @@ pub struct AggregatedSignatureProof { pub proof_data: MultisigAggregatedSignature, } -#[cfg(feature = "devnet2")] impl AggregatedSignatureProof { /// Create a new AggregatedSignatureProof. pub fn new(participants: AggregationBits, proof_data: MultisigAggregatedSignature) -> Self { @@ -320,12 +298,8 @@ pub struct Attestation { /// Validator attestation bundled with its signature. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct SignedAttestation { - #[cfg(feature = "devnet2")] pub validator_id: u64, - #[cfg(feature = "devnet2")] pub message: AttestationData, - #[cfg(not(feature = "devnet2"))] - pub message: Attestation, pub signature: Signature, } diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index fdf3ca6..b49a11b 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -1,13 +1,10 @@ -use crate::{Attestation, Attestations, Bytes32, Signature, Slot, State, ValidatorIndex}; +use crate::{Attestation, Bytes32, Signature, Slot, State, ValidatorIndex}; use serde::{Deserialize, Serialize}; use ssz_derive::Ssz; #[cfg(feature = "xmss-verify")] use leansig::signature::generalized_xmss::instantiations_poseidon::lifetime_2_to_the_20::target_sum::SIGTargetSumLifetime20W2NoOff; -use ssz::{PersistentList, SszHash}; -use typenum::U4096; use crate::attestation::{AggregatedAttestations, AttestationSignatures}; -use crate::validator::BlsPublicKey; /// The body of a block, containing payload data. /// @@ -15,11 +12,7 @@ use crate::validator::BlsPublicKey; /// separately in BlockSignatures to match the spec architecture. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct BlockBody { - #[cfg(feature = "devnet2")] pub attestations: AggregatedAttestations, - #[cfg(not(feature = "devnet2"))] - #[serde(with = "crate::serde_helpers")] - pub attestations: Attestations, } #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] @@ -67,10 +60,6 @@ pub struct SignedBlockWithAttestation { /// Aggregated signature payload for the block. /// /// Signatures remain in attestation order followed by the proposer signature. - #[cfg(not(feature = "devnet2"))] - #[serde(with = "crate::serde_helpers::block_signatures")] - pub signature: PersistentList, - #[cfg(feature = "devnet2")] pub signature: BlockSignatures, } @@ -128,99 +117,9 @@ impl SignedBlockWithAttestation { /// /// - Spec: /// - XMSS Library: - #[cfg(not(feature = "devnet2"))] - pub fn verify_signatures(&self, parent_state: State) -> bool { - // Unpack the signed block components - let block = &self.message.block; - let signatures = &self.signature; - - // Combine all attestations that need verification - // - // This creates a single list containing both: - // 1. Block body attestations (from other validators) - // 2. Proposer attestation (from the block producer) - let mut all_attestations: Vec = Vec::new(); - - // Collect block body attestations - let mut i: u64 = 0; - loop { - match block.body.attestations.get(i) { - Ok(a) => all_attestations.push(a.clone()), - Err(_) => break, - } - i += 1; - } - - // Append proposer attestation - all_attestations.push(self.message.proposer_attestation.clone()); - - // Collect signatures into a Vec - let mut signatures_vec: Vec = Vec::new(); - let mut j: u64 = 0; - loop { - match signatures.get(j) { - Ok(s) => signatures_vec.push(s.clone()), - Err(_) => break, - } - j += 1; - } - - // Verify signature count matches attestation count - // - // Each attestation must have exactly one corresponding signature. - // - // The ordering must be preserved: - // 1. Block body attestations, - // 2. The proposer attestation. - assert_eq!( - signatures_vec.len(), - all_attestations.len(), - "Number of signatures does not match number of attestations" - ); - - let validators = &parent_state.validators; - let num_validators = validators.len_u64(); - - // Verify each attestation signature - for (attestation, signature) in all_attestations.iter().zip(signatures_vec.iter()) { - // Ensure validator exists in the active set - assert!( - attestation.validator_id.0 < num_validators, - "Validator index out of range" - ); - - let validator = validators - .get(attestation.validator_id.0) - .expect("validator must exist"); - - // Verify the XMSS signature - // - // This cryptographically proves that: - // - The validator possesses the secret key for their public key - // - The attestation has not been tampered with - // - The signature was created at the correct epoch (slot) - - let message_bytes: [u8; 32] = hash_tree_root(attestation).0.into(); - - assert!( - verify_xmss_signature( - validator.pubkey.0.as_bytes(), - attestation.data.slot, - &message_bytes, - &signature, - ), - "Attestation signature verification failed" - ); - } - - true - } - - /// Verifies all attestation signatures using lean-multisig aggregated proofs. /// Each attestation has a single `MultisigAggregatedSignature` proof that covers /// all participating validators. - #[cfg(feature = "devnet2")] pub fn verify_signatures(&self, parent_state: State) -> bool { // Unpack the signed block components let block = &self.message.block; diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index f8fd48a..7fcb675 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -14,7 +14,6 @@ pub use attestation::{ Attestations, Signature, SignatureKey, SignedAggregatedAttestation, SignedAttestation, }; -#[cfg(feature = "devnet2")] pub use attestation::{AggregatedSignatureProof, MultisigAggregatedSignature}; pub use block::{ Block, BlockBody, BlockHeader, BlockWithAttestation, SignedBlock, SignedBlockWithAttestation, diff --git a/lean_client/containers/src/serde_helpers.rs b/lean_client/containers/src/serde_helpers.rs index 55298da..7295ac5 100644 --- a/lean_client/containers/src/serde_helpers.rs +++ b/lean_client/containers/src/serde_helpers.rs @@ -183,142 +183,8 @@ pub mod signature { } } -/// Custom deserializer for BlockSignatures that handles the {"data": [sig, ...]} format -/// where each signature can be either hex string or structured XMSS format -pub mod block_signatures { - use super::*; - use crate::block::BlockSignatures; - use crate::Signature; - use serde_json::Value; - use ssz::PersistentList; - use typenum::U4096; - - /// Structured XMSS signature format from test vectors - #[derive(Deserialize, Clone)] - struct XmssSignature { - path: XmssPath, - rho: DataWrapper>, - hashes: DataWrapper>>>, - } - - #[derive(Deserialize, Clone)] - struct XmssPath { - siblings: DataWrapper>>>, - } - - fn parse_single_signature(value: &Value) -> Result { - // Check if it's a hex string (normal format) - if let Value::String(hex_str) = value { - let hex_str = hex_str.trim_start_matches("0x"); - let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid hex string: {}", e))?; - - return Signature::try_from(bytes.as_slice()) - .map_err(|_| "Invalid signature length".to_string()); - } - - // Otherwise, parse as structured XMSS signature - let xmss_sig: XmssSignature = serde_json::from_value(value.clone()) - .map_err(|e| format!("Failed to parse XMSS signature: {}", e))?; - - // Serialize the XMSS signature to bytes - // Format: siblings (variable length) + rho (28 bytes) + hashes (variable length) - let mut bytes = Vec::new(); - - // Write siblings - for sibling in &xmss_sig.path.siblings.data { - for val in &sibling.data { - bytes.extend_from_slice(&val.to_le_bytes()); - } - } - - // Write rho (7 u32s = 28 bytes) - for val in &xmss_sig.rho.data { - bytes.extend_from_slice(&val.to_le_bytes()); - } - - // Write hashes - for hash in &xmss_sig.hashes.data { - for val in &hash.data { - bytes.extend_from_slice(&val.to_le_bytes()); - } - } - - // Pad or truncate to 3112 bytes - bytes.resize(3112, 0); - - Signature::try_from(bytes.as_slice()).map_err(|_| "Failed to create signature".to_string()) - } - - #[cfg(not(feature = "devnet2"))] - pub fn deserialize<'de, D>( - deserializer: D, - ) -> Result, D::Error> - where - D: Deserializer<'de>, - { - use serde::de::Error; - - // Parse the {"data": [...]} wrapper - let wrapper: DataWrapper> = DataWrapper::deserialize(deserializer)?; - - let mut signatures = PersistentList::default(); - - for (idx, sig_value) in wrapper.data.into_iter().enumerate() { - let sig = parse_single_signature(&sig_value) - .map_err(|e| D::Error::custom(format!("Signature {}: {}", idx, e)))?; - signatures - .push(sig) - .map_err(|e| D::Error::custom(format!("Signature {} push failed: {:?}", idx, e)))?; - } - - Ok(signatures) - } - - #[cfg(feature = "devnet2")] - pub fn deserialize<'de, D>(_: D) -> Result - where - D: Deserializer<'de>, - { - Err(serde::de::Error::custom( - "BlockSignatures deserialization not implemented for devnet2", - )) - } - - #[cfg(not(feature = "devnet2"))] - pub fn serialize( - value: &PersistentList, - serializer: S, - ) -> Result - where - S: Serializer, - { - // Collect all signatures as hex strings - let mut sigs: Vec = Vec::new(); - let mut i = 0u64; - loop { - match value.get(i) { - Ok(sig) => { - sigs.push(format!("0x{}", hex::encode(sig.as_bytes()))); - i += 1; - } - Err(_) => break, - } - } - - let wrapper = DataWrapper { data: sigs }; - wrapper.serialize(serializer) - } - - #[cfg(feature = "devnet2")] - pub fn serialize(_value: &BlockSignatures, _serializer: S) -> Result - where - S: Serializer, - { - Err(serde::ser::Error::custom( - "BlockSignatures serialization not implemented for devnet2", - )) - } -} +// NOTE: The block_signatures module was removed as it was only used for devnet1 format. +// TODO: If BlockSignatures custom serialization is needed for devnet2, implement it here. /// Serde helper for ssz::ByteList - serializes as hex string pub mod byte_list { diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 27550f0..bdc6f57 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -1,15 +1,13 @@ use crate::validator::Validator; -use crate::{block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, Attestation, Attestations, Bytes32, Checkpoint, Config, Signature, SignedAttestation, Slot, Uint64, ValidatorIndex}; +use crate::{block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, Attestation, Bytes32, Checkpoint, Config, Signature, Slot, Uint64, ValidatorIndex}; use crate::{ HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, Validators, }; use serde::{Deserialize, Serialize}; -use ssz::{PersistentList as List, PersistentList}; +use ssz::PersistentList as List; use ssz_derive::Ssz; use std::collections::BTreeMap; -use typenum::U4096; use crate::attestation::{AggregatedAttestations, HasDuplicateData}; -use crate::block::BlockSignatures; pub const VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const JUSTIFICATION_ROOTS_LIMIT: usize = 1 << 18; // 262144 @@ -297,7 +295,6 @@ impl State { pub fn process_block(&self, block: &Block) -> Result { let state = self.process_block_header(block)?; - #[cfg(feature = "devnet2")] if block.body.attestations.has_duplicate_data() { return Err("Block contains duplicate AttestationData".to_string()); } @@ -392,45 +389,6 @@ impl State { }) } - #[cfg(not(feature = "devnet2"))] - pub fn process_attestations(&self, attestations: &Attestations) -> Self { - let mut justifications = self.get_justifications(); - let mut latest_justified = self.latest_justified.clone(); - let mut latest_finalized = self.latest_finalized.clone(); - let initial_finalized_slot = self.latest_finalized.slot; - let justified_slots = self.justified_slots.clone(); - - let mut votes_vec: Vec = Vec::new(); - let mut i: u64 = 0; - loop { - match attestations.get(i) { - Ok(v) => votes_vec.push(v.clone()), - Err(_) => break, - } - i += 1; - } - - let mut justified_slots_working = Vec::new(); - for i in 0..justified_slots.len() { - justified_slots_working.push(justified_slots.get(i).map(|b| *b).unwrap_or(false)); - } - - for attestation in votes_vec.iter() { - self.process_single_attestation( - &attestation.data, - &[attestation.validator_id.0], - &mut justifications, - &mut latest_justified, - &mut latest_finalized, - &mut justified_slots_working, - initial_finalized_slot, - ); - } - - self.finalize_attestation_processing(justifications, latest_justified, latest_finalized, justified_slots_working) - } - - #[cfg(feature = "devnet2")] pub fn process_attestations(&self, attestations: &AggregatedAttestations) -> Self { let mut justifications = self.get_justifications(); let mut latest_justified = self.latest_justified.clone(); @@ -552,7 +510,7 @@ impl State { /// Build a valid block on top of this state. /// /// Computes the post-state and creates a block with the correct state root. - /// If `available_signed_attestations` and `known_block_roots` are provided, + /// If `available_attestations` and `known_block_roots` are provided, /// performs fixed-point attestation collection: iteratively adds valid /// attestations until no more can be included. This is necessary because /// processing attestations may update the justified checkpoint, which may @@ -564,135 +522,14 @@ impl State { /// * `proposer_index` - Validator index of the proposer /// * `parent_root` - Root of the parent block (must match state after slot processing) /// * `initial_attestations` - Initial attestations to include - /// * `available_signed_attestations` - Optional pool of attestations to collect from + /// * `available_attestations` - Optional pool of attestations to collect from /// * `known_block_roots` - Optional set of known block roots for attestation validation + /// * `gossip_signatures` - Optional map of individual signatures from gossip + /// * `aggregated_payloads` - Optional map of aggregated signature proofs /// /// # Returns /// - /// Tuple of (Block, post-State, collected attestations, signatures) - #[cfg(not(feature = "devnet2"))] - pub fn build_block( - &self, - slot: Slot, - proposer_index: ValidatorIndex, - parent_root: Bytes32, - initial_attestations: Option>, - available_signed_attestations: Option<&[SignedBlockWithAttestation]>, - known_block_roots: Option<&std::collections::HashSet>, - ) -> Result<(Block, Self, Vec, PersistentList), String> { - // Initialize empty attestation set for iterative collection - let mut attestations = initial_attestations.unwrap_or_default(); - let mut signatures = PersistentList::default(); - - // Advance state to target slot - // Note: parent_root comes from fork choice and is already validated. - // We cannot validate it against the header hash here because process_slots() - // caches the state root in the header, changing its hash. - let pre_state = self.process_slots(slot)?; - - // Iteratively collect valid attestations using fixed-point algorithm - // - // Continue until no new attestations can be added to the block. - // This ensures we include the maximal valid attestation set. - loop { - // Create candidate block with current attestation set - let mut attestations_list = Attestations::default(); - for att in &attestations { - attestations_list - .push(att.clone()) - .map_err(|e| format!("Failed to push attestation: {:?}", e))?; - } - - let candidate_block = Block { - slot, - proposer_index, - parent_root, - state_root: Bytes32(ssz::H256::zero()), - body: BlockBody { - attestations: attestations_list, - }, - }; - - // Apply state transition to get the post-block state - let post_state = pre_state.process_block(&candidate_block)?; - - // No attestation source provided: done after computing post_state - if available_signed_attestations.is_none() || known_block_roots.is_none() { - // Store the post state root in the block - let final_block = Block { - slot, - proposer_index, - parent_root, - state_root: hash_tree_root(&post_state), - body: candidate_block.body, - }; - return Ok((final_block, post_state, attestations, signatures)); - } - - // Find new valid attestations matching post-state justification - let mut new_attestations = Vec::new(); - let mut new_signatures = Vec::new(); - - let available = available_signed_attestations.unwrap(); - let known_roots = known_block_roots.unwrap(); - - for signed_attestation in available { - let att = &signed_attestation.message.proposer_attestation; - let data = &att.data; - - // Skip if target block is unknown - if !known_roots.contains(&data.head.root) { - continue; - } - - // Skip if attestation source does not match post-state's latest justified - if data.source != post_state.latest_justified { - continue; - } - - // Add attestation if not already included - if !attestations.contains(att) { - new_attestations.push(att.clone()); - // Add corresponding signatures from the signed block - // Note: In the actual implementation, you'd need to properly track - // which signatures correspond to which attestations - let mut idx = 0u64; - loop { - match signed_attestation.signature.get(idx) { - Ok(sig) => { - new_signatures.push(sig.clone()); - idx += 1; - } - Err(_) => break, - } - } - } - } - - // Fixed point reached: no new attestations found - if new_attestations.is_empty() { - // Store the post state root in the block - let final_block = Block { - slot, - proposer_index, - parent_root, - state_root: hash_tree_root(&post_state), - body: candidate_block.body, - }; - return Ok((final_block, post_state, attestations, signatures)); - } - - // Add new attestations and continue iteration - attestations.extend(new_attestations); - for sig in new_signatures { - signatures - .push(sig) - .map_err(|e| format!("Failed to push signature: {:?}", e))?; - } - } - } - - #[cfg(feature = "devnet2")] + /// Tuple of (Block, post-State, collected aggregated attestations, aggregated proofs) pub fn build_block( &self, slot: Slot, @@ -845,19 +682,6 @@ impl State { } } - /// Compute aggregated signatures for a set of attestations using a two-phase algorithm. - /// - /// # Phase 1: Gossip Collection - /// For each attestation group, attempt to collect individual XMSS signatures from - /// the gossip network. These are fresh signatures that validators broadcast. - /// - /// # Phase 2: Fallback to Block Proofs - /// For validators not covered by gossip, fall back to previously-seen aggregated - /// proofs from blocks using a greedy set-cover approach. - /// - /// # Returns - /// Tuple of (aggregated attestations, corresponding proofs) - #[cfg(feature = "devnet2")] pub fn compute_aggregated_signatures( &self, attestations: &[Attestation], @@ -991,226 +815,4 @@ impl State { } } -#[cfg(all(test, not(feature = "devnet2")))] -mod tests { - use super::*; - #[test] - fn proposer_round_robin() { - let st = State::generate_genesis(Uint64(0), Uint64(4)); - assert!(State { - config: st.config.clone(), - ..st.clone() - } - .is_proposer(ValidatorIndex(0))); - } - - #[test] - fn slot_justifiability_rules() { - use crate::slot::Slot; - assert!(Slot(1).is_justifiable_after(Slot(0))); - assert!(Slot(9).is_justifiable_after(Slot(0))); // perfect square - assert!(Slot(6).is_justifiable_after(Slot(0))); // pronic (2*3) - } - - #[test] - fn test_hash_tree_root() { - let body = BlockBody { - attestations: List::default(), - }; - let block = Block { - slot: Slot(1), - proposer_index: ValidatorIndex(0), - parent_root: Bytes32(ssz::H256::zero()), - state_root: Bytes32(ssz::H256::zero()), - body, - }; - - let root = hash_tree_root(&block); - assert_ne!(root, Bytes32(ssz::H256::zero())); - } - - #[test] - fn test_process_slots() { - let genesis_state = State::generate_genesis(Uint64(0), Uint64(10)); - let target_slot = Slot(5); - - let new_state = genesis_state.process_slots(target_slot).unwrap(); - - assert_eq!(new_state.slot, target_slot); - let genesis_state_for_hash = genesis_state.clone(); //this is sooooo bad - assert_eq!( - new_state.latest_block_header.state_root, - hash_tree_root(&genesis_state_for_hash) - ); - } - - #[test] - #[cfg(feature = "devnet1")] - fn test_build_block() { - // Create genesis state with validators - let genesis_state = State::generate_genesis(Uint64(0), Uint64(4)); - - // Compute expected parent root after slot processing - let pre_state = genesis_state.process_slots(Slot(1)).unwrap(); - let expected_parent_root = hash_tree_root(&pre_state.latest_block_header); - - // Test 1: Build a simple block without attestations - let result = genesis_state.build_block( - Slot(1), - ValidatorIndex(1), - expected_parent_root, - None, - None, - None, - ); - - assert!(result.is_ok(), "Building simple block should succeed"); - let (block, post_state, attestations, signatures) = result.unwrap(); - - // Verify block properties - assert_eq!(block.slot, Slot(1)); - assert_eq!(block.proposer_index, ValidatorIndex(1)); - assert_eq!(block.parent_root, expected_parent_root); - assert_ne!( - block.state_root, - Bytes32(ssz::H256::zero()), - "State root should be computed" - ); - // Verify attestations and signatures are empty - assert_eq!(attestations.len(), 0); - // Check signatures by trying to get first element - assert!(signatures.get(0).is_err(), "Signatures should be empty"); - - // Verify post-state has advanced - assert_eq!(post_state.slot, Slot(1)); - // Note: The post-state's latest_block_header.state_root is zero because it will be - // filled in during the next slot processing - assert_eq!( - block.parent_root, expected_parent_root, - "Parent root should match" - ); - - // Test 2: Build block with initial attestations - let attestation = Attestation { - validator_id: Uint64(0), - data: crate::AttestationData { - slot: Slot(1), - head: Checkpoint { - root: expected_parent_root, - slot: Slot(0), - }, - target: Checkpoint { - root: expected_parent_root, - slot: Slot(1), - }, - source: Checkpoint { - root: expected_parent_root, - slot: Slot(0), - }, - }, - }; - - let result = genesis_state.build_block( - Slot(1), - ValidatorIndex(1), - expected_parent_root, - Some(vec![attestation.clone()]), - None, - None, - ); - - assert!( - result.is_ok(), - "Building block with attestations should succeed" - ); - let (block, _post_state, attestations, _signatures) = result.unwrap(); - - // Verify attestation was included - assert_eq!(attestations.len(), 1); - assert_eq!(attestations[0].validator_id, Uint64(0)); - // Check that attestation list has one element - assert!( - block.body.attestations.get(0).is_ok(), - "Block should contain attestation" - ); - assert!( - block.body.attestations.get(1).is_err(), - "Block should have only one attestation" - ); - } - - #[test] - #[cfg(feature = "devnet1")] - fn test_build_block_advances_state() { - // Create genesis state - let genesis_state = State::generate_genesis(Uint64(0), Uint64(10)); - - // Compute parent root after advancing to target slot - let pre_state = genesis_state.process_slots(Slot(5)).unwrap(); - let parent_root = hash_tree_root(&pre_state.latest_block_header); - - // Build block at slot 5 - // Proposer for slot 5 with 10 validators is (5 % 10) = 5 - let result = - genesis_state.build_block(Slot(5), ValidatorIndex(5), parent_root, None, None, None); - - assert!(result.is_ok()); - let (block, post_state, _, _) = result.unwrap(); - - // Verify state advanced through slots - assert_eq!(post_state.slot, Slot(5)); - assert_eq!(block.slot, Slot(5)); - - // Verify block can be applied to genesis state - let transition_result = genesis_state.state_transition_with_validation( - SignedBlockWithAttestation { - message: crate::BlockWithAttestation { - block: block.clone(), - proposer_attestation: Attestation::default(), - }, - signature: PersistentList::default(), - }, - true, // signatures are considered valid (not validating, just marking as valid) - true, - ); - - assert!( - transition_result.is_ok(), - "Built block should be valid for state transition" - ); - } - - #[test] - #[cfg(feature = "devnet1")] - fn test_build_block_state_root_matches() { - // Create genesis state - let genesis_state = State::generate_genesis(Uint64(0), Uint64(3)); - - // Compute parent root after advancing to target slot - let pre_state = genesis_state.process_slots(Slot(1)).unwrap(); - let parent_root = hash_tree_root(&pre_state.latest_block_header); - - // Build a block - // Proposer for slot 1 with 3 validators is (1 % 3) = 1 - let result = - genesis_state.build_block(Slot(1), ValidatorIndex(1), parent_root, None, None, None); - - assert!(result.is_ok()); - let (block, post_state, _, _) = result.unwrap(); - - // Verify the state root in block matches the computed post-state - let computed_state_root = hash_tree_root(&post_state); - assert_eq!( - block.state_root, computed_state_root, - "Block state root should match computed post-state root" - ); - - // Verify it's not zero - assert_ne!( - block.state_root, - Bytes32(ssz::H256::zero()), - "State root should not be zero" - ); - } -} diff --git a/lean_client/containers/tests/test_vectors/block_processing.rs b/lean_client/containers/tests/test_vectors/block_processing.rs index caec865..61c196b 100644 --- a/lean_client/containers/tests/test_vectors/block_processing.rs +++ b/lean_client/containers/tests/test_vectors/block_processing.rs @@ -1,8 +1,21 @@ // Integration test: All block processing test vectors +// +// TODO: Update these tests for devnet2 format: +// - test_process_first_block_after_genesis +// - test_blocks_with_gaps +// - test_linear_chain_multiple_blocks +// - test_block_extends_deep_chain +// - test_empty_blocks +// - test_empty_blocks_with_missed_slots +// - test_block_at_large_slot_number +// - test_block_with_invalid_parent_root +// - test_block_with_invalid_proposer +// - test_block_with_invalid_state_root + use super::runner::TestRunner; +/* #[test] -#[cfg(feature = "devnet1")] fn test_process_first_block_after_genesis() { let test_path = "../tests/test_vectors/test_blocks/test_process_first_block_after_genesis.json"; TestRunner::run_block_processing_test(test_path) @@ -10,7 +23,6 @@ fn test_process_first_block_after_genesis() { } #[test] -#[cfg(feature = "devnet1")] fn test_blocks_with_gaps() { let test_path = "../tests/test_vectors/test_blocks/test_blocks_with_gaps.json"; TestRunner::run_block_processing_test(test_path) @@ -18,7 +30,6 @@ fn test_blocks_with_gaps() { } #[test] -#[cfg(feature = "devnet1")] fn test_linear_chain_multiple_blocks() { let test_path = "../tests/test_vectors/test_blocks/test_linear_chain_multiple_blocks.json"; TestRunner::run_block_processing_test(test_path) @@ -26,7 +37,6 @@ fn test_linear_chain_multiple_blocks() { } #[test] -#[cfg(feature = "devnet1")] fn test_block_extends_deep_chain() { let test_path = "../tests/test_vectors/test_blocks/test_block_extends_deep_chain.json"; TestRunner::run_block_processing_test(test_path) @@ -34,7 +44,6 @@ fn test_block_extends_deep_chain() { } #[test] -#[cfg(feature = "devnet1")] fn test_empty_blocks() { let test_path = "../tests/test_vectors/test_blocks/test_empty_blocks.json"; TestRunner::run_block_processing_test(test_path) @@ -42,7 +51,6 @@ fn test_empty_blocks() { } #[test] -#[cfg(feature = "devnet1")] fn test_empty_blocks_with_missed_slots() { let test_path = "../tests/test_vectors/test_blocks/test_empty_blocks_with_missed_slots.json"; TestRunner::run_block_processing_test(test_path) @@ -50,7 +58,6 @@ fn test_empty_blocks_with_missed_slots() { } #[test] -#[cfg(feature = "devnet1")] fn test_block_at_large_slot_number() { let test_path = "../tests/test_vectors/test_blocks/test_block_at_large_slot_number.json"; TestRunner::run_block_processing_test(test_path) @@ -60,7 +67,6 @@ fn test_block_at_large_slot_number() { // Invalid block tests (expecting failures) #[test] -#[cfg(feature = "devnet1")] fn test_block_with_invalid_parent_root() { let test_path = "../tests/test_vectors/test_blocks/test_block_with_invalid_parent_root.json"; TestRunner::run_block_processing_test(test_path) @@ -68,7 +74,6 @@ fn test_block_with_invalid_parent_root() { } #[test] -#[cfg(feature = "devnet1")] fn test_block_with_invalid_proposer() { let test_path = "../tests/test_vectors/test_blocks/test_block_with_invalid_proposer.json"; TestRunner::run_block_processing_test(test_path) @@ -76,9 +81,9 @@ fn test_block_with_invalid_proposer() { } #[test] -#[cfg(feature = "devnet1")] fn test_block_with_invalid_state_root() { let test_path = "../tests/test_vectors/test_blocks/test_block_with_invalid_state_root.json"; TestRunner::run_block_processing_test(test_path) .expect("test_block_with_invalid_state_root failed"); } +*/ diff --git a/lean_client/containers/tests/test_vectors/mod.rs b/lean_client/containers/tests/test_vectors/mod.rs index f29ca74..106ac3a 100644 --- a/lean_client/containers/tests/test_vectors/mod.rs +++ b/lean_client/containers/tests/test_vectors/mod.rs @@ -1,5 +1,10 @@ -//! Test vector modules for devnet1 format -#![cfg(not(feature = "devnet2"))] +//! Test vector modules +//! +//! TODO: Update these test vectors for devnet2 format: +//! - Update runner.rs to use AggregatedAttestations +//! - Update block_processing.rs tests +//! - Update genesis.rs tests +//! - Update verify_signatures.rs to use BlockSignatures structure // Test vector modules pub mod runner; diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index bf23138..19a8ef1 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -550,9 +550,11 @@ impl TestRunner { Ok(()) } - /// Test runner for verify_signatures test vectors - /// Tests XMSS signature verification on SignedBlockWithAttestation - #[cfg(feature = "devnet1")] + // Test runner for verify_signatures test vectors + // Tests XMSS signature verification on SignedBlockWithAttestation + // + // TODO: Update for devnet2 format - needs BlockSignatures structure + /* pub fn run_verify_signatures_test>(path: P) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; @@ -608,5 +610,6 @@ impl TestRunner { Ok(()) } + */ } diff --git a/lean_client/containers/tests/test_vectors/verify_signatures.rs b/lean_client/containers/tests/test_vectors/verify_signatures.rs index cfc3301..cd6a14a 100644 --- a/lean_client/containers/tests/test_vectors/verify_signatures.rs +++ b/lean_client/containers/tests/test_vectors/verify_signatures.rs @@ -1,6 +1,12 @@ // Integration test: verify_signatures test vectors // Tests XMSS signature verification on SignedBlockWithAttestation // +// TODO: Update these tests for devnet2 format: +// - test_proposer_signature +// - test_proposer_and_attester_signatures +// - test_invalid_signature +// - test_mixed_valid_invalid_signatures +// // NOTE: Without the `xmss-verify` feature, signature verification only checks // structure (attestation count matches signature count, validator indices valid). // Full cryptographic verification requires `--features xmss-verify`. @@ -14,8 +20,8 @@ use super::runner::TestRunner; // These tests verify that properly signed blocks pass verification. // Without xmss-verify feature, they pass because structural validation succeeds. +/* #[test] -#[cfg(feature = "devnet1")] fn test_proposer_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json"; TestRunner::run_verify_signatures_test(test_path) @@ -23,7 +29,6 @@ fn test_proposer_signature() { } #[test] -#[cfg(feature = "devnet1")] fn test_proposer_and_attester_signatures() { let test_path = "../tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_and_attester_signatures.json"; TestRunner::run_verify_signatures_test(test_path) @@ -36,7 +41,6 @@ fn test_proposer_and_attester_signatures() { // Run with `cargo test --features xmss-verify` to enable full signature verification. #[test] -#[cfg(feature = "devnet1")] #[ignore = "Requires xmss-verify feature for actual signature validation. Run with: cargo test --features xmss-verify"] fn test_invalid_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json"; @@ -45,10 +49,10 @@ fn test_invalid_signature() { } #[test] -#[cfg(feature = "devnet1")] #[ignore = "Requires xmss-verify feature for actual signature validation. Run with: cargo test --features xmss-verify"] fn test_mixed_valid_invalid_signatures() { let test_path = "../tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_mixed_valid_invalid_signatures.json"; TestRunner::run_verify_signatures_test(test_path) .expect("test_mixed_valid_invalid_signatures failed"); } +*/ diff --git a/lean_client/containers/tests/unit_tests/attestation_aggregation.rs b/lean_client/containers/tests/unit_tests/attestation_aggregation.rs index 285aa46..f66f6b2 100644 --- a/lean_client/containers/tests/unit_tests/attestation_aggregation.rs +++ b/lean_client/containers/tests/unit_tests/attestation_aggregation.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "devnet2")] #[cfg(test)] mod tests { use containers::attestation::{AggregatedAttestation, AggregationBits, Attestation, AttestationData}; diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 9d618d6..611cff3 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -1,5 +1,9 @@ -//! Common test utilities for devnet1 format -#![cfg(not(feature = "devnet2"))] +//! Common test utilities +//! +//! TODO: Update these test utilities for devnet2 format: +//! - Update create_block to use AggregatedAttestations instead of Attestations +//! - Update SignedBlockWithAttestation to use BlockSignatures structure +//! - Update process_attestations calls to use new signature format use containers::{Attestation, Attestations, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators, AggregatedAttestation, Signature}; use ssz::{PersistentList}; @@ -14,11 +18,6 @@ const _: [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT] = [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT]; pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Option) -> SignedBlockWithAttestation { - #[cfg(feature = "devnet1")] - let body = BlockBody { - attestations: attestations.unwrap_or_else(PersistentList::default), - }; - #[cfg(feature = "devnet2")] let body = BlockBody { attestations: { let attestations_vec = attestations.unwrap_or_default(); @@ -26,10 +25,6 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op // Convert PersistentList into a Vec let attestations_vec: Vec = attestations_vec.into_iter().cloned().collect(); - let aggregated: Vec = - AggregatedAttestation::aggregate_by_data(&attestations_vec); - - let aggregated: Vec = AggregatedAttestation::aggregate_by_data(&attestations_vec); @@ -43,7 +38,6 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op persistent_list }, - // other BlockBody fields... }; @@ -55,16 +49,6 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op body: body, }; - #[cfg(feature = "devnet1")] - let return_value = SignedBlockWithAttestation { - message: BlockWithAttestation { - block: block_message, - proposer_attestation: Attestation::default(), - }, - signature: PersistentList::default(), - }; - - #[cfg(feature = "devnet2")] let return_value = SignedBlockWithAttestation { message: BlockWithAttestation { block: block_message, diff --git a/lean_client/containers/tests/unit_tests/mod.rs b/lean_client/containers/tests/unit_tests/mod.rs index c02c404..cb5fc61 100644 --- a/lean_client/containers/tests/unit_tests/mod.rs +++ b/lean_client/containers/tests/unit_tests/mod.rs @@ -5,10 +5,8 @@ mod state_basic; mod state_justifications; mod attestation_aggregation; -// Modules that are only compatible with devnet1 format -#[cfg(not(feature = "devnet2"))] +// TODO: Update these modules for devnet2 data structures +// (SignedAttestation now uses AttestationData directly, BlockSignatures changed, etc.) mod common; -#[cfg(not(feature = "devnet2"))] mod state_process; -#[cfg(not(feature = "devnet2"))] mod state_transition; diff --git a/lean_client/containers/tests/unit_tests/state_basic.rs b/lean_client/containers/tests/unit_tests/state_basic.rs index d83fc91..d815e55 100644 --- a/lean_client/containers/tests/unit_tests/state_basic.rs +++ b/lean_client/containers/tests/unit_tests/state_basic.rs @@ -1,5 +1,10 @@ -//! State basic tests for devnet1 format -#![cfg(not(feature = "devnet2"))] +//! State basic tests +//! +//! TODO: Update these tests for devnet2 format: +//! - test_generate_genesis +//! - test_proposer_round_robin +//! - test_slot_justifiability_rules +//! - test_hash_tree_root // tests/state_basic.rs use containers::{block::{BlockBody, hash_tree_root}, state::State, types::Uint64, ValidatorIndex}; diff --git a/lean_client/containers/tests/unit_tests/state_justifications.rs b/lean_client/containers/tests/unit_tests/state_justifications.rs index 74dce9e..bb9f469 100644 --- a/lean_client/containers/tests/unit_tests/state_justifications.rs +++ b/lean_client/containers/tests/unit_tests/state_justifications.rs @@ -1,5 +1,9 @@ -//! State justifications tests for devnet1 format -#![cfg(not(feature = "devnet2"))] +//! State justifications tests +//! +//! TODO: Update these tests for devnet2 format: +//! - test_get_justifications_empty +//! - test_get_justifications_single_root +//! - test_get_justifications_multiple_roots // tests/state_justifications.rs use containers::{ diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index 459c853..4ad95ef 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -1,5 +1,12 @@ -//! State process tests for devnet1 format -#![cfg(not(feature = "devnet2"))] +//! State process tests +//! +//! TODO: Update these tests for devnet2 format: +//! - test_process_slot +//! - test_process_slots +//! - test_process_slots_backwards +//! - test_process_block_header_valid +//! - test_process_block_header_invalid +//! - test_process_attestations_justification_and_finalization // tests/state_process.rs use containers::{ @@ -109,7 +116,8 @@ fn test_process_block_header_invalid( } // This test verifies that attestations correctly justify and finalize slots -#[cfg(feature = "devnet1")] +// TODO: Update for devnet2 - needs AggregatedAttestations format +/* #[test] fn test_process_attestations_justification_and_finalization() { let mut state = genesis_state(); @@ -165,4 +173,5 @@ fn test_process_attestations_justification_and_finalization() { assert_eq!(justified_slot_4, true); assert_eq!(new_state.latest_finalized, genesis_checkpoint); assert!(!new_state.get_justifications().contains_key(&checkpoint4.root)); -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index 53ec667..789b999 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -1,5 +1,9 @@ -//! State transition tests for devnet1 format -#![cfg(not(feature = "devnet2"))] +//! State transition tests +//! +//! TODO: Update these tests for devnet2 format: +//! - test_state_transition_full +//! - test_state_transition_invalid_signatures +//! - test_state_transition_bad_state_root // tests/state_transition.rs use containers::{ @@ -88,7 +92,8 @@ fn test_state_transition_invalid_signatures() { assert_eq!(result.unwrap_err(), "Block signatures must be valid"); } -#[cfg(feature = "devnet1")] +// TODO: Update for devnet2 - needs BlockSignatures structure +/* #[test] fn test_state_transition_bad_state_root() { let state = genesis_state(); @@ -112,8 +117,8 @@ fn test_state_transition_bad_state_root() { assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Invalid block state root"); } +*/ -#[cfg(feature = "devnet2")] #[test] fn test_state_transition_devnet2() { let state = genesis_state(); diff --git a/lean_client/env-config/Cargo.toml b/lean_client/env-config/Cargo.toml index 4b761e5..bd3e737 100644 --- a/lean_client/env-config/Cargo.toml +++ b/lean_client/env-config/Cargo.toml @@ -5,8 +5,4 @@ edition.workspace = true authors.workspace = true license.workspace = true -[features] -devnet1 = [] -devnet2 = [] - [dependencies] diff --git a/lean_client/fork_choice/Cargo.toml b/lean_client/fork_choice/Cargo.toml index badc834..d08f647 100644 --- a/lean_client/fork_choice/Cargo.toml +++ b/lean_client/fork_choice/Cargo.toml @@ -5,8 +5,6 @@ edition = "2021" [features] default = [] -devnet1 = ["containers/devnet1", "env-config/devnet1"] -devnet2 = ["containers/devnet2", "env-config/devnet2"] [dependencies] env-config = { path = "../env-config", default-features = false } diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index aa82c31..d1935ff 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -2,7 +2,6 @@ use crate::store::*; use containers::{ attestation::SignedAttestation, block::SignedBlockWithAttestation, Bytes32, ValidatorIndex, }; -#[cfg(feature = "devnet2")] use containers::SignatureKey; use ssz::SszHash; @@ -27,23 +26,9 @@ pub fn on_attestation( signed_attestation: SignedAttestation, is_from_block: bool, ) -> Result<(), String> { - #[cfg(not(feature = "devnet2"))] - let validator_id = ValidatorIndex(signed_attestation.message.validator_id.0); - #[cfg(not(feature = "devnet2"))] - let attestation_slot = signed_attestation.message.data.slot; - #[cfg(not(feature = "devnet2"))] - let source_slot = signed_attestation.message.data.source.slot; - #[cfg(not(feature = "devnet2"))] - let target_slot = signed_attestation.message.data.target.slot; - - - #[cfg(feature = "devnet2")] let validator_id = ValidatorIndex(signed_attestation.validator_id); - #[cfg(feature = "devnet2")] let attestation_slot = signed_attestation.message.slot; - #[cfg(feature = "devnet2")] let source_slot = signed_attestation.message.source.slot; - #[cfg(feature = "devnet2")] let target_slot = signed_attestation.message.target.slot; // Validate attestation is not from future @@ -65,20 +50,6 @@ pub fn on_attestation( if is_from_block { // On-chain attestation processing - immediately becomes "known" - #[cfg(not(feature = "devnet2"))] - if store - .latest_known_attestations - .get(&validator_id) - .map_or(true, |existing| { - existing.message.data.slot < attestation_slot - }) - { - store - .latest_known_attestations - .insert(validator_id, signed_attestation.clone()); - } - - #[cfg(feature = "devnet2")] if store .latest_known_attestations .get(&validator_id) @@ -93,50 +64,29 @@ pub fn on_attestation( // Remove from new attestations if superseded if let Some(existing_new) = store.latest_new_attestations.get(&validator_id) { - #[cfg(not(feature = "devnet2"))] - if existing_new.message.data.slot <= attestation_slot { - store.latest_new_attestations.remove(&validator_id); - } - #[cfg(feature = "devnet2")] if existing_new.message.slot <= attestation_slot { store.latest_new_attestations.remove(&validator_id); } } } else { // Network gossip attestation processing - goes to "new" stage - #[cfg(not(feature = "devnet2"))] + // Store signature for later aggregation during block building + let data_root = signed_attestation.message.data_root_bytes(); + let sig_key = SignatureKey::new(signed_attestation.validator_id, data_root); + store.gossip_signatures.insert(sig_key, signed_attestation.signature.clone()); + + // Track attestation for fork choice if store .latest_new_attestations .get(&validator_id) .map_or(true, |existing| { - existing.message.data.slot < attestation_slot + existing.message.slot < attestation_slot }) { store .latest_new_attestations .insert(validator_id, signed_attestation); } - - #[cfg(feature = "devnet2")] - { - // Store signature for later aggregation during block building - let data_root = signed_attestation.message.data_root_bytes(); - let sig_key = SignatureKey::new(signed_attestation.validator_id, data_root); - store.gossip_signatures.insert(sig_key, signed_attestation.signature.clone()); - - // Track attestation for fork choice - if store - .latest_new_attestations - .get(&validator_id) - .map_or(true, |existing| { - existing.message.slot < attestation_slot - }) - { - store - .latest_new_attestations - .insert(validator_id, signed_attestation); - } - } } Ok(()) } @@ -201,123 +151,78 @@ fn process_block_internal( } // Process block body attestations as on-chain (is_from_block=true) - let attestations = &signed_block.message.block.body.attestations; let signatures = &signed_block.signature; - #[cfg(not(feature = "devnet2"))] - { - for i in 0.. { - match (attestations.get(i), signatures.get(i)) { - (Ok(attestation), Ok(signature)) => { - let signed_attestation = SignedAttestation { - message: attestation.clone(), - signature: signature.clone(), - }; - on_attestation(store, signed_attestation, true)?; - } - _ => break, - } - } - - // Update head BEFORE processing proposer attestation - update_head(store); - - // Process proposer attestation as gossip (is_from_block=false) - // This ensures it goes to "new" attestations and doesn't immediately affect fork choice - let num_body_attestations = attestations.len_u64(); - - // Get proposer signature or use default if not present (for tests) - use containers::attestation::Signature; - let proposer_signature = signatures - .get(num_body_attestations) - .map(|sig| sig.clone()) - .unwrap_or_else(|_| Signature::default()); - - let proposer_signed_attestation = SignedAttestation { - message: signed_block.message.proposer_attestation.clone(), - signature: proposer_signature, - }; - - // Process proposer attestation as if received via gossip (is_from_block=false) - // This ensures it goes to "new" attestations and doesn't immediately affect fork choice - on_attestation(store, proposer_signed_attestation, false)?; - - Ok(()) - } - - #[cfg(feature = "devnet2")] - { - let aggregated_attestations = &signed_block.message.block.body.attestations; - let proposer_attestation = &signed_block.message.proposer_attestation; + let aggregated_attestations = &signed_block.message.block.body.attestations; + let proposer_attestation = &signed_block.message.proposer_attestation; + + // Store aggregated proofs for future block building + // Each attestation_signature proof is indexed by (validator_id, data_root) for each participating validator + for (att_idx, aggregated_attestation) in aggregated_attestations.into_iter().enumerate() { + let data_root = aggregated_attestation.data.data_root_bytes(); - // Store aggregated proofs for future block building - // Each attestation_signature proof is indexed by (validator_id, data_root) for each participating validator - for (att_idx, aggregated_attestation) in aggregated_attestations.into_iter().enumerate() { - let data_root = aggregated_attestation.data.data_root_bytes(); + // Get the corresponding proof from attestation_signatures + if let Ok(proof_data) = signatures.attestation_signatures.get(att_idx as u64) { + // Create AggregatedSignatureProof with participants and proof data + let aggregated_proof = containers::AggregatedSignatureProof::new( + aggregated_attestation.aggregation_bits.clone(), + proof_data.clone(), + ); - // Get the corresponding proof from attestation_signatures - if let Ok(proof_data) = signatures.attestation_signatures.get(att_idx as u64) { - // Create AggregatedSignatureProof with participants and proof data - let aggregated_proof = containers::AggregatedSignatureProof::new( - aggregated_attestation.aggregation_bits.clone(), - proof_data.clone(), - ); - - // Store proof for each validator in the aggregation - for (bit_idx, bit) in aggregated_attestation.aggregation_bits.0.iter().enumerate() { - if *bit { - let validator_id = bit_idx as u64; - let sig_key = SignatureKey::new(validator_id, data_root); - store.aggregated_payloads - .entry(sig_key) - .or_insert_with(Vec::new) - .push(aggregated_proof.clone()); - } + // Store proof for each validator in the aggregation + for (bit_idx, bit) in aggregated_attestation.aggregation_bits.0.iter().enumerate() { + if *bit { + let validator_id = bit_idx as u64; + let sig_key = SignatureKey::new(validator_id, data_root); + store.aggregated_payloads + .entry(sig_key) + .or_insert_with(Vec::new) + .push(aggregated_proof.clone()); } } } + } - // Process each aggregated attestation's validators for fork choice - // Note: Signature verification is done in verify_signatures() before on_block() - for aggregated_attestation in aggregated_attestations.into_iter() { - let validator_ids: Vec = aggregated_attestation - .aggregation_bits.0 - .iter() - .enumerate() - .filter(|(_, bit)| **bit) - .map(|(index, _)| index as u64) - .collect(); - - // Each validator in the aggregation votes for this attestation data - for validator_id in validator_ids { - on_attestation( - store, - SignedAttestation { - validator_id, - message: aggregated_attestation.data.clone(), - // Use a default signature since verification already happened - signature: containers::Signature::default(), - }, - true, - )?; - } + // Process each aggregated attestation's validators for fork choice + // Note: Signature verification is done in verify_signatures() before on_block() + for aggregated_attestation in aggregated_attestations.into_iter() { + let validator_ids: Vec = aggregated_attestation + .aggregation_bits.0 + .iter() + .enumerate() + .filter(|(_, bit)| **bit) + .map(|(index, _)| index as u64) + .collect(); + + // Each validator in the aggregation votes for this attestation data + for validator_id in validator_ids { + on_attestation( + store, + SignedAttestation { + validator_id, + message: aggregated_attestation.data.clone(), + // Use a default signature since verification already happened + signature: containers::Signature::default(), + }, + true, + )?; } + } - // Update head BEFORE processing proposer attestation - update_head(store); + // Update head BEFORE processing proposer attestation + update_head(store); - let proposer_signed_attestation = SignedAttestation { - validator_id: proposer_attestation.validator_id.0, - message: proposer_attestation.data.clone(), - signature: signed_block.signature.proposer_signature, - }; + let proposer_signed_attestation = SignedAttestation { + validator_id: proposer_attestation.validator_id.0, + message: proposer_attestation.data.clone(), + signature: signed_block.signature.proposer_signature, + }; - // Process proposer attestation as if received via gossip (is_from_block=false) - // This ensures it goes to "new" attestations and doesn't immediately affect fork choice - on_attestation(store, proposer_signed_attestation, false)?; + // Process proposer attestation as if received via gossip (is_from_block=false) + // This ensures it goes to "new" attestations and doesn't immediately affect fork choice + on_attestation(store, proposer_signed_attestation, false)?; - Ok(()) - } + Ok(()) } fn process_pending_blocks(store: &mut Store, mut roots: Vec) { diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 763f880..e2cb275 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -3,7 +3,6 @@ use containers::{ block::SignedBlockWithAttestation, checkpoint::Checkpoint, config::Config, state::State, Bytes32, Root, Slot, ValidatorIndex, }; -#[cfg(feature = "devnet2")] use containers::{AggregatedSignatureProof, Signature, SignatureKey}; use ssz::SszHash; use std::collections::HashMap; @@ -26,10 +25,8 @@ pub struct Store { pub latest_new_attestations: HashMap, pub blocks_queue: HashMap>, - #[cfg(feature = "devnet2")] pub gossip_signatures: HashMap, - #[cfg(feature = "devnet2")] pub aggregated_payloads: HashMap>, } @@ -71,9 +68,7 @@ pub fn get_forkchoice_store( latest_known_attestations: HashMap::new(), latest_new_attestations: HashMap::new(), blocks_queue: HashMap::new(), - #[cfg(feature = "devnet2")] gossip_signatures: HashMap::new(), - #[cfg(feature = "devnet2")] aggregated_payloads: HashMap::new(), } } @@ -97,9 +92,6 @@ pub fn get_fork_choice_head( // stage 1: accumulate weights by walking up from each attestation's head for attestation in latest_attestations.values() { - #[cfg(not(feature = "devnet2"))] - let mut curr = attestation.message.data.head.root; - #[cfg(feature = "devnet2")] let mut curr = attestation.message.head.root; if let Some(block) = store.blocks.get(&curr) { @@ -292,7 +284,6 @@ pub fn get_proposal_head(store: &mut Store, slot: Slot) -> Root { /// /// # Returns /// Tuple of (block root, finalized Block, attestation signature proofs) -#[cfg(feature = "devnet2")] pub fn produce_block_with_signatures( store: &mut Store, slot: Slot, diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index 7c92780..c5a8da9 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -1,867 +1,17 @@ - -#![cfg(not(feature = "devnet2"))] - -use fork_choice::{ - handlers::{on_attestation, on_block, on_tick}, - store::{get_forkchoice_store, Store}, -}; - -use containers::{ - attestation::{Attestation, AttestationData, SignedAttestation, Signature}, - block::{hash_tree_root, Block, BlockBody, BlockHeader, BlockWithAttestation, SignedBlockWithAttestation}, - checkpoint::Checkpoint, - config::Config, - state::State, - Bytes32, Slot, Uint64, ValidatorIndex, -}; - -use serde::Deserialize; -use ssz::{PersistentList, SszHash}; -use std::collections::HashMap; -use std::panic::AssertUnwindSafe; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestVectorFile { - #[serde(flatten)] - tests: HashMap, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestVector { - #[allow(dead_code)] - network: String, - anchor_state: TestAnchorState, - anchor_block: TestAnchorBlock, - steps: Vec, - #[serde(rename = "_info")] - info: TestInfo, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestAnchorState { - config: TestConfig, - slot: u64, - latest_block_header: TestBlockHeader, - latest_justified: TestCheckpoint, - latest_finalized: TestCheckpoint, - #[serde(default)] - historical_block_hashes: TestDataWrapper, - #[serde(default)] - justified_slots: TestDataWrapper, - validators: TestDataWrapper, - #[serde(default)] - justifications_roots: TestDataWrapper, - #[serde(default)] - justifications_validators: TestDataWrapper, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestConfig { - genesis_time: u64, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestBlockHeader { - slot: u64, - proposer_index: u64, - parent_root: String, - state_root: String, - body_root: String, -} - -#[derive(Debug, Deserialize)] -struct TestCheckpoint { - root: String, - slot: u64, -} - -#[derive(Debug, Deserialize, Default)] -struct TestDataWrapper { - data: Vec, -} - -#[derive(Debug, Deserialize)] -struct TestValidator { - #[allow(dead_code)] - pubkey: String, - #[allow(dead_code)] - #[serde(default)] - index: u64, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestAnchorBlock { - slot: u64, - proposer_index: u64, - parent_root: String, - state_root: String, - body: TestBlockBody, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestBlock { - slot: u64, - proposer_index: u64, - parent_root: String, - state_root: String, - body: TestBlockBody, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestBlockWithAttestation { - block: TestBlock, - proposer_attestation: TestAttestation, - #[serde(default)] - block_root_label: Option, -} - -#[derive(Debug, Deserialize)] -struct TestBlockBody { - attestations: TestDataWrapper, -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum TestAttestation { - #[serde(rename_all = "camelCase")] - Nested { - validator_id: u64, - data: TestAttestationData, - }, - #[serde(rename_all = "camelCase")] - Flat { - validator_id: u64, - slot: u64, - head: TestCheckpoint, - target: TestCheckpoint, - source: TestCheckpoint, - }, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestAttestationData { - slot: u64, - head: TestCheckpoint, - target: TestCheckpoint, - source: TestCheckpoint, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestStep { - valid: bool, - #[serde(default)] - checks: Option, - #[serde(rename = "stepType")] - step_type: String, - block: Option, - attestation: Option, - tick: Option, - time: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct TestChecks { - #[serde(rename = "headSlot")] - head_slot: Option, - #[serde(rename = "headRootLabel")] - head_root_label: Option, - #[serde(rename = "attestationChecks")] - attestation_checks: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct AttestationCheck { - validator: u64, - #[allow(dead_code)] - #[serde(rename = "attestationSlot")] - attestation_slot: u64, - #[serde(rename = "targetSlot")] - target_slot: Option, - location: String, -} - -#[derive(Debug, Deserialize)] -struct TestInfo { - #[allow(dead_code)] - hash: String, - #[allow(dead_code)] - comment: String, - #[serde(rename = "testId")] - test_id: String, - #[allow(dead_code)] - description: String, - #[allow(dead_code)] - #[serde(rename = "fixtureFormat")] - fixture_format: String, -} - -fn parse_root(hex_str: &str) -> Bytes32 { - let hex = hex_str.trim_start_matches("0x"); - let mut bytes = [0u8; 32]; - - if hex.len() == 64 { - for i in 0..32 { - bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16) - .unwrap_or_else(|_| panic!("Invalid hex at position {}: {}", i, hex)); - } - } else if !hex.chars().all(|c| c == '0') { - panic!("Invalid root length: {} (expected 64 hex chars)", hex.len()); - } - - Bytes32(ssz::H256::from(bytes)) -} - -fn convert_test_checkpoint(test_cp: &TestCheckpoint) -> Checkpoint { - Checkpoint { - root: parse_root(&test_cp.root), - slot: Slot(test_cp.slot), - } -} - -fn convert_test_attestation(test_att: &TestAttestation) -> Attestation { - let (validator_id, slot, head, target, source) = match test_att { - TestAttestation::Nested { validator_id, data } => ( - *validator_id, - data.slot, - &data.head, - &data.target, - &data.source, - ), - TestAttestation::Flat { - validator_id, - slot, - head, - target, - source, - } => (*validator_id, *slot, head, target, source), - }; - - Attestation { - validator_id: Uint64(validator_id), - data: AttestationData { - slot: Slot(slot), - head: convert_test_checkpoint(head), - target: convert_test_checkpoint(target), - source: convert_test_checkpoint(source), - }, - } -} - -#[cfg(feature = "devnet1")] -fn convert_test_anchor_block(test_block: &TestAnchorBlock) -> SignedBlockWithAttestation { - let mut attestations = ssz::PersistentList::default(); - - for (i, test_att) in test_block.body.attestations.data.iter().enumerate() { - let signed_vote = convert_test_attestation(test_att); - attestations - .push(signed_vote) - .expect(&format!("Failed to add attestation {}", i)); - } - - let block = Block { - slot: Slot(test_block.slot), - proposer_index: ValidatorIndex(test_block.proposer_index), - parent_root: parse_root(&test_block.parent_root), - state_root: parse_root(&test_block.state_root), - body: BlockBody { attestations }, - }; - - // Create proposer attestation - let proposer_attestation = Attestation { - validator_id: Uint64(test_block.proposer_index), - data: AttestationData { - slot: Slot(test_block.slot), - head: Checkpoint { - root: parse_root(&test_block.parent_root), - slot: Slot(test_block.slot), - }, - target: Checkpoint { - root: parse_root(&test_block.parent_root), - slot: Slot(test_block.slot), - }, - source: Checkpoint { - root: parse_root(&test_block.parent_root), - slot: Slot(0), - }, - }, - }; - - SignedBlockWithAttestation { - message: BlockWithAttestation { - block, - proposer_attestation, - }, - signature: PersistentList::default(), - } -} - -#[cfg(feature = "devnet1")] -fn convert_test_block(test_block_with_att: &TestBlockWithAttestation) -> SignedBlockWithAttestation { - let test_block = &test_block_with_att.block; - let mut attestations = ssz::PersistentList::default(); - - for (i, test_att) in test_block.body.attestations.data.iter().enumerate() { - let signed_vote = convert_test_attestation(test_att); - attestations - .push(signed_vote) - .expect(&format!("Failed to add attestation {}", i)); - } - - let block = Block { - slot: Slot(test_block.slot), - proposer_index: ValidatorIndex(test_block.proposer_index), - parent_root: parse_root(&test_block.parent_root), - state_root: parse_root(&test_block.state_root), - body: BlockBody { attestations }, - }; - - let proposer_attestation = convert_test_attestation(&test_block_with_att.proposer_attestation); - - SignedBlockWithAttestation { - message: BlockWithAttestation { - block, - proposer_attestation, - }, - signature: PersistentList::default(), - } -} - -fn initialize_state_from_test(test_state: &TestAnchorState) -> State { - use containers::{ - HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, - }; - use ssz::PersistentList as List; - - let config = Config { - genesis_time: test_state.config.genesis_time, - }; - - let latest_block_header = BlockHeader { - slot: Slot(test_state.latest_block_header.slot), - proposer_index: ValidatorIndex(test_state.latest_block_header.proposer_index), - parent_root: parse_root(&test_state.latest_block_header.parent_root), - state_root: parse_root(&test_state.latest_block_header.state_root), - body_root: parse_root(&test_state.latest_block_header.body_root), - }; - - let mut historical_block_hashes = HistoricalBlockHashes::default(); - for hash_str in &test_state.historical_block_hashes.data { - historical_block_hashes - .push(parse_root(hash_str)) - .expect("within limit"); - } - - let mut justified_slots = JustifiedSlots::new(false, test_state.justified_slots.data.len()); - for (i, &val) in test_state.justified_slots.data.iter().enumerate() { - if val { - justified_slots.set(i, true); - } - } - - let mut justifications_roots = JustificationRoots::default(); - for root_str in &test_state.justifications_roots.data { - justifications_roots - .push(parse_root(root_str)) - .expect("within limit"); - } - - let mut justifications_validators = - JustificationsValidators::new(false, test_state.justifications_validators.data.len()); - for (i, &val) in test_state.justifications_validators.data.iter().enumerate() { - if val { - justifications_validators.set(i, true); - } - } - - let mut validators = List::default(); - for test_validator in &test_state.validators.data { - let pubkey = containers::validator::BlsPublicKey::from_hex(&test_validator.pubkey) - .expect("Failed to parse validator pubkey"); - let validator = containers::validator::Validator { - pubkey, - index: containers::Uint64(test_validator.index), - }; - validators.push(validator).expect("Failed to add validator"); - } - - State { - config, - slot: Slot(test_state.slot), - latest_block_header, - latest_justified: convert_test_checkpoint(&test_state.latest_justified), - latest_finalized: convert_test_checkpoint(&test_state.latest_finalized), - historical_block_hashes, - justified_slots, - validators, - justifications_roots, - justifications_validators, - } -} - -#[cfg(feature = "devnet1")] -fn verify_checks( - store: &Store, - checks: &Option, - block_labels: &HashMap, - step_idx: usize, -) -> Result<(), String> { - // If no checks provided, nothing to verify - let checks = match checks { - Some(c) => c, - None => return Ok(()), - }; - - if let Some(expected_slot) = checks.head_slot { - let actual_slot = store.blocks[&store.head].message.block.slot.0; - if actual_slot != expected_slot { - return Err(format!( - "Step {}: Head slot mismatch - expected {}, got {}", - step_idx, expected_slot, actual_slot - )); - } - } - - if let Some(label) = &checks.head_root_label { - let expected_root = block_labels - .get(label) - .ok_or_else(|| format!("Step {}: Block label '{}' not found", step_idx, label))?; - if &store.head != expected_root { - let actual_slot = store - .blocks - .get(&store.head) - .map(|b| b.message.block.slot.0) - .unwrap_or(0); - let expected_slot = store - .blocks - .get(expected_root) - .map(|b| b.message.block.slot.0) - .unwrap_or(0); - return Err(format!( - "Step {}: Head root mismatch for label '{}' - expected slot {}, got slot {} (known_attestations: {}, new_attestations: {})", - step_idx, label, expected_slot, actual_slot, - store.latest_known_attestations.len(), store.latest_new_attestations.len() - )); - } - } - - if let Some(att_checks) = &checks.attestation_checks { - for check in att_checks { - let validator = ValidatorIndex(check.validator); - - match check.location.as_str() { - "new" => { - if !store.latest_new_attestations.contains_key(&validator) { - return Err(format!( - "Step {}: Expected validator {} in new attestations, but not found", - step_idx, check.validator - )); - } - if let Some(target_slot) = check.target_slot { - let attestation = &store.latest_new_attestations[&validator]; - if attestation.message.data.target.slot.0 != target_slot { - return Err(format!( - "Step {}: Validator {} new attestation target slot mismatch - expected {}, got {}", - step_idx, check.validator, target_slot, attestation.message.data.target.slot.0 - )); - } - } - } - "known" => { - if !store.latest_known_attestations.contains_key(&validator) { - return Err(format!( - "Step {}: Expected validator {} in known attestations, but not found", - step_idx, check.validator - )); - } - } - _ => { - return Err(format!( - "Step {}: Unknown attestation location: {}", - step_idx, check.location - )); - } - } - } - } - - Ok(()) -} - -#[cfg(feature = "devnet1")] -fn run_single_test(_test_name: &str, test: TestVector) -> Result<(), String> { - println!(" Running: {}", test.info.test_id); - - let mut anchor_state = initialize_state_from_test(&test.anchor_state); - let anchor_block = convert_test_anchor_block(&test.anchor_block); - - let body_root = hash_tree_root(&anchor_block.message.block.body); - anchor_state.latest_block_header = BlockHeader { - slot: anchor_block.message.block.slot, - proposer_index: anchor_block.message.block.proposer_index, - parent_root: anchor_block.message.block.parent_root, - state_root: anchor_block.message.block.state_root, - body_root, - }; - - let config = Config { - genesis_time: test.anchor_state.config.genesis_time, - }; - - let mut store = get_forkchoice_store(anchor_state, anchor_block, config); - let mut block_labels: HashMap = HashMap::new(); - - for (step_idx, step) in test.steps.iter().enumerate() { - match step.step_type.as_str() { - "block" => { - let test_block = step - .block - .as_ref() - .ok_or_else(|| format!("Step {}: Missing block data", step_idx))?; - - let result = std::panic::catch_unwind(AssertUnwindSafe(|| { - let signed_block = convert_test_block(test_block); - let block_root = Bytes32(signed_block.message.block.hash_tree_root()); - - // Advance time to the block's slot to ensure attestations are processable - // SECONDS_PER_SLOT is 4 (not 12) - let block_time = store.config.genesis_time + (signed_block.message.block.slot.0 * 4); - on_tick(&mut store, block_time, false); - - on_block(&mut store, signed_block)?; - Ok(block_root) - })); - - let result = match result { - Ok(inner) => inner, - Err(e) => Err(format!("Panic: {:?}", e)), - }; - - if let Ok(block_root) = &result { - if let Some(label) = &test_block.block_root_label { - block_labels.insert(label.clone(), *block_root); - } - } - - if step.valid && result.is_err() { - return Err(format!( - "Step {}: Block should be valid but processing failed: {:?}", - step_idx, - result.err() - )); - } else if !step.valid && result.is_ok() { - return Err(format!( - "Step {}: Block should be invalid but processing succeeded", - step_idx - )); - } - - if step.valid && result.is_ok() { - verify_checks(&store, &step.checks, &block_labels, step_idx)?; - } - } - "tick" | "time" => { - let time_value = step - .tick - .or(step.time) - .ok_or_else(|| format!("Step {}: Missing tick/time data", step_idx))?; - on_tick(&mut store, time_value, false); - - if step.valid { - verify_checks(&store, &step.checks, &block_labels, step_idx)?; - } - } - "attestation" => { - let test_att = step - .attestation - .as_ref() - .ok_or_else(|| format!("Step {}: Missing attestation data", step_idx))?; - - let result = std::panic::catch_unwind(AssertUnwindSafe(|| { - let attestation = convert_test_attestation(test_att); - let signed_attestation = SignedAttestation { - message: attestation, - signature: Signature::default(), - }; - on_attestation(&mut store, signed_attestation, false) - })); - - let result = match result { - Ok(inner) => inner, - Err(e) => Err(format!("Panic: {:?}", e)), - }; - - if step.valid && result.is_err() { - return Err(format!( - "Step {}: Attestation should be valid but processing failed: {:?}", - step_idx, - result.err() - )); - } else if !step.valid && result.is_ok() { - return Err(format!( - "Step {}: Attestation should be invalid but processing succeeded", - step_idx - )); - } - - if step.valid && result.is_ok() { - verify_checks(&store, &step.checks, &block_labels, step_idx)?; - } - } - _ => { - return Err(format!( - "Step {}: Unknown step type: {}", - step_idx, step.step_type - )); - } - } - } - - Ok(()) -} - -#[cfg(feature = "devnet1")] -fn run_test_vector_file(test_path: &str) -> Result<(), String> { - let json_str = std::fs::read_to_string(test_path) - .map_err(|e| format!("Failed to read file {}: {}", test_path, e))?; - - let test_data: TestVectorFile = serde_json::from_str(&json_str) - .map_err(|e| format!("Failed to parse JSON from {}: {}", test_path, e))?; - - for (test_name, test_vector) in test_data.tests { - run_single_test(&test_name, test_vector)?; - } - - Ok(()) -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_fork_choice_head_vectors() { - let test_dir = "../tests/test_vectors/test_fork_choice/test_fork_choice_head"; - - let entries = - std::fs::read_dir(test_dir).expect(&format!("Failed to read test directory: {}", test_dir)); - - let mut test_count = 0; - let mut pass_count = 0; - let mut fail_count = 0; - - println!("\n=== Fork Choice Head Tests ==="); - - for entry in entries { - let path = entry.unwrap().path(); - if path.extension().map_or(false, |ext| ext == "json") { - test_count += 1; - println!("\nTest file: {:?}", path.file_name().unwrap()); - - match run_test_vector_file(path.to_str().unwrap()) { - Ok(_) => { - println!(" ✓ PASSED"); - pass_count += 1; - } - Err(e) => { - println!(" ✗ FAILED: {}", e); - fail_count += 1; - } - } - } - } - - println!("\n=== Summary ==="); - println!( - "Total: {}, Passed: {}, Failed: {}", - test_count, pass_count, fail_count - ); - - if fail_count > 0 { - panic!("{} test(s) failed", fail_count); - } -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_attestation_processing_vectors() { - let test_dir = "../tests/test_vectors/test_fork_choice/test_attestation_processing"; - - let entries = - std::fs::read_dir(test_dir).expect(&format!("Failed to read test directory: {}", test_dir)); - - let mut test_count = 0; - let mut pass_count = 0; - let mut fail_count = 0; - - println!("\n=== Attestation Processing Tests ==="); - - for entry in entries { - let path = entry.unwrap().path(); - if path.extension().map_or(false, |ext| ext == "json") { - test_count += 1; - println!("\nTest file: {:?}", path.file_name().unwrap()); - - match run_test_vector_file(path.to_str().unwrap()) { - Ok(_) => { - println!(" ✓ PASSED"); - pass_count += 1; - } - Err(e) => { - println!(" ✗ FAILED: {}", e); - fail_count += 1; - } - } - } - } - - println!("\n=== Summary ==="); - println!( - "Total: {}, Passed: {}, Failed: {}", - test_count, pass_count, fail_count - ); - - if fail_count > 0 { - panic!("{} test(s) failed", fail_count); - } -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_fork_choice_reorgs_vectors() { - let test_dir = "../tests/test_vectors/test_fork_choice/test_fork_choice_reorgs"; - - let entries = - std::fs::read_dir(test_dir).expect(&format!("Failed to read test directory: {}", test_dir)); - - let mut test_count = 0; - let mut pass_count = 0; - let mut fail_count = 0; - - println!("\n=== Fork Choice Reorg Tests ==="); - - for entry in entries { - let path = entry.unwrap().path(); - if path.extension().map_or(false, |ext| ext == "json") { - test_count += 1; - println!("\nTest file: {:?}", path.file_name().unwrap()); - - match run_test_vector_file(path.to_str().unwrap()) { - Ok(_) => { - println!(" ✓ PASSED"); - pass_count += 1; - } - Err(e) => { - println!(" ✗ FAILED: {}", e); - fail_count += 1; - } - } - } - } - - println!("\n=== Summary ==="); - println!( - "Total: {}, Passed: {}, Failed: {}", - test_count, pass_count, fail_count - ); - - if fail_count > 0 { - panic!("{} test(s) failed", fail_count); - } -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_attestation_target_selection_vectors() { - let test_dir = "../tests/test_vectors/test_fork_choice/test_attestation_target_selection"; - - let entries = - std::fs::read_dir(test_dir).expect(&format!("Failed to read test directory: {}", test_dir)); - - let mut test_count = 0; - let mut pass_count = 0; - let mut fail_count = 0; - - println!("\n=== Attestation Target Selection Tests ==="); - - for entry in entries { - let path = entry.unwrap().path(); - if path.extension().map_or(false, |ext| ext == "json") { - test_count += 1; - println!("\nTest file: {:?}", path.file_name().unwrap()); - - match run_test_vector_file(path.to_str().unwrap()) { - Ok(_) => { - println!(" ✓ PASSED"); - pass_count += 1; - } - Err(e) => { - println!(" ✗ FAILED: {}", e); - fail_count += 1; - } - } - } - } - - println!("\n=== Summary ==="); - println!( - "Total: {}, Passed: {}, Failed: {}", - test_count, pass_count, fail_count - ); - - if fail_count > 0 { - panic!("{} test(s) failed", fail_count); - } -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_lexicographic_tiebreaker_vectors() { - let test_dir = "../tests/test_vectors/test_fork_choice/test_lexicographic_tiebreaker"; - - let entries = - std::fs::read_dir(test_dir).expect(&format!("Failed to read test directory: {}", test_dir)); - - let mut test_count = 0; - let mut pass_count = 0; - let mut fail_count = 0; - - println!("\n=== Lexicographic Tiebreaker Tests ==="); - - for entry in entries { - let path = entry.unwrap().path(); - if path.extension().map_or(false, |ext| ext == "json") { - test_count += 1; - println!("\nTest file: {:?}", path.file_name().unwrap()); - - match run_test_vector_file(path.to_str().unwrap()) { - Ok(_) => { - println!(" ✓ PASSED"); - pass_count += 1; - } - Err(e) => { - println!(" ✗ FAILED: {}", e); - fail_count += 1; - } - } - } - } - - println!("\n=== Summary ==="); - println!( - "Total: {}, Passed: {}, Failed: {}", - test_count, pass_count, fail_count - ); - - if fail_count > 0 { - panic!("{} test(s) failed", fail_count); - } -} +// TODO: Add fork choice test vectors for devnet2 +// +// This file previously contained devnet1 test vectors that are no longer compatible +// with the devnet2 data structures (SignedAttestation now uses AttestationData directly +// instead of Attestation wrapper, BlockSignatures structure changed, etc.) +// +// Tests to implement: +// - test_genesis_state_transition +// - test_basic_slot_transition +// - test_attestation_processing +// - test_justification_and_finalization +// - test_multiple_attestations +// - test_fork_choice_with_competing_blocks +// - test_reorg_on_higher_justified +// - test_finality_prevents_reorg +// - test_attestation_from_future_slot +// - test_attestation_with_wrong_source diff --git a/lean_client/fork_choice/tests/unit_tests/votes.rs b/lean_client/fork_choice/tests/unit_tests/votes.rs index 7b94932..24b437e 100644 --- a/lean_client/fork_choice/tests/unit_tests/votes.rs +++ b/lean_client/fork_choice/tests/unit_tests/votes.rs @@ -1,237 +1,16 @@ - -#![cfg(not(feature = "devnet2"))] - -use super::common::create_test_store; -use fork_choice::handlers::on_attestation; -use fork_choice::store::{accept_new_attestations, INTERVALS_PER_SLOT}; -use containers::{ - attestation::{Attestation, AttestationData, SignedAttestation, Signature}, - checkpoint::Checkpoint, - Bytes32, Slot, Uint64, ValidatorIndex, -}; - -#[cfg(feature = "devnet1")] -fn create_signed_attestation(validator_id: u64, slot: Slot, head_root: Bytes32) -> SignedAttestation { - SignedAttestation { - message: Attestation { - validator_id: Uint64(validator_id), - data: AttestationData { - slot, - head: Checkpoint { root: head_root, slot }, - target: Checkpoint { root: head_root, slot }, - source: Checkpoint { root: Bytes32::default(), slot: Slot(0) }, - }, - }, - signature: Signature::default(), - } -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_accept_new_attestations() { - let mut store = create_test_store(); - - // Setup initial known attestations - let val1 = ValidatorIndex(1); - let val2 = ValidatorIndex(2); - let val3 = ValidatorIndex(3); - - store.latest_known_attestations.insert( - val1, - create_signed_attestation(1, Slot(0), store.head), - ); - - // Val1 updates their attestation to Slot 1 - store.latest_new_attestations.insert( - val1, - create_signed_attestation(1, Slot(1), store.head), - ); - // Val2 casts a new attestation for Slot 1 - store.latest_new_attestations.insert( - val2, - create_signed_attestation(2, Slot(1), store.head), - ); - // Val3 casts a new attestation for Slot 2 - store.latest_new_attestations.insert( - val3, - create_signed_attestation(3, Slot(2), store.head), - ); - - accept_new_attestations(&mut store); - - assert_eq!(store.latest_new_attestations.len(), 0); - assert_eq!(store.latest_known_attestations.len(), 3); - - assert_eq!(store.latest_known_attestations[&val1].message.data.slot, Slot(1)); - assert_eq!(store.latest_known_attestations[&val2].message.data.slot, Slot(1)); - assert_eq!(store.latest_known_attestations[&val3].message.data.slot, Slot(2)); -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_accept_new_attestations_multiple() { - let mut store = create_test_store(); - - for i in 0..5 { - store.latest_new_attestations.insert( - ValidatorIndex(i), - create_signed_attestation(i, Slot(i), store.head), - ); - } - - assert_eq!(store.latest_new_attestations.len(), 5); - assert_eq!(store.latest_known_attestations.len(), 0); - - accept_new_attestations(&mut store); - - assert_eq!(store.latest_new_attestations.len(), 0); - assert_eq!(store.latest_known_attestations.len(), 5); -} - -#[test] -fn test_accept_new_attestations_empty() { - let mut store = create_test_store(); - let initial_known = store.latest_known_attestations.len(); - - accept_new_attestations(&mut store); - - assert_eq!(store.latest_new_attestations.len(), 0); - assert_eq!(store.latest_known_attestations.len(), initial_known); -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_on_attestation_lifecycle() { - let mut store = create_test_store(); - let validator_idx = ValidatorIndex(1); - let slot_0 = Slot(0); - let slot_1 = Slot(1); - - // 1. Attestation from network (gossip) - let signed_attestation_gossip = create_signed_attestation(1, slot_0, store.head); - - on_attestation(&mut store, signed_attestation_gossip.clone(), false).expect("Gossip attestation valid"); - - // Should be in new_attestations, not known_attestations - assert!(store.latest_new_attestations.contains_key(&validator_idx)); - assert!(!store.latest_known_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, slot_0); - - // 2. Same attestation included in a block - on_attestation(&mut store, signed_attestation_gossip, true).expect("Block attestation valid"); - - assert!(store.latest_known_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, slot_0); - - // 3. Newer attestation from network - store.time = 1 * INTERVALS_PER_SLOT; // Advance time - let signed_attestation_next = create_signed_attestation(1, slot_1, store.head); - - on_attestation(&mut store, signed_attestation_next, false).expect("Next gossip attestation valid"); - - // Should update new_attestations - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, slot_1); - // Known attestations should still be at slot 0 until accepted - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, slot_0); -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_on_attestation_future_slot() { - let mut store = create_test_store(); - let future_slot = Slot(100); // Far in the future - - let signed_attestation = create_signed_attestation(1, future_slot, store.head); - - let result = on_attestation(&mut store, signed_attestation, false); - assert!(result.is_err()); -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_on_attestation_update_vote() { - let mut store = create_test_store(); - let validator_idx = ValidatorIndex(1); - - // First attestation at slot 0 - let signed_attestation1 = create_signed_attestation(1, Slot(0), store.head); - - on_attestation(&mut store, signed_attestation1, false).expect("First attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(0)); - - // Advance time to allow slot 1 - store.time = 1 * INTERVALS_PER_SLOT; - - // Second attestation at slot 1 - let signed_attestation2 = create_signed_attestation(1, Slot(1), store.head); - - on_attestation(&mut store, signed_attestation2, false).expect("Second attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(1)); -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_on_attestation_ignore_old_vote() { - let mut store = create_test_store(); - let validator_idx = ValidatorIndex(1); - - // Advance time - store.time = 2 * INTERVALS_PER_SLOT; - - // Newer attestation first - let signed_attestation_new = create_signed_attestation(1, Slot(2), store.head); - - on_attestation(&mut store, signed_attestation_new, false).expect("New attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(2)); - - // Older attestation second - let signed_attestation_old = create_signed_attestation(1, Slot(1), store.head); - - on_attestation(&mut store, signed_attestation_old, false).expect("Old attestation processed but ignored"); - // Should still be slot 2 - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(2)); -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_on_attestation_from_block_supersedes_new() { - let mut store = create_test_store(); - let validator_idx = ValidatorIndex(1); - - // First, add attestation via gossip - let signed_attestation1 = create_signed_attestation(1, Slot(0), store.head); - on_attestation(&mut store, signed_attestation1, false).expect("Gossip attestation valid"); - - assert!(store.latest_new_attestations.contains_key(&validator_idx)); - assert!(!store.latest_known_attestations.contains_key(&validator_idx)); - - // Then, add same attestation via block (on-chain) - let signed_attestation2 = create_signed_attestation(1, Slot(0), store.head); - on_attestation(&mut store, signed_attestation2, true).expect("Block attestation valid"); - - // Should move from new to known - assert!(!store.latest_new_attestations.contains_key(&validator_idx)); - assert!(store.latest_known_attestations.contains_key(&validator_idx)); -} - -#[test] -#[cfg(feature = "devnet1")] -fn test_on_attestation_newer_from_block_removes_older_new() { - let mut store = create_test_store(); - let validator_idx = ValidatorIndex(1); - - // Add older attestation via gossip - let signed_attestation_gossip = create_signed_attestation(1, Slot(0), store.head); - on_attestation(&mut store, signed_attestation_gossip, false).expect("Gossip attestation valid"); - - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(0)); - - // Add newer attestation via block (on-chain) - store.time = 1 * INTERVALS_PER_SLOT; - let signed_attestation_block = create_signed_attestation(1, Slot(1), store.head); - on_attestation(&mut store, signed_attestation_block, true).expect("Block attestation valid"); - - // New attestation should be removed (superseded by newer on-chain one) - assert!(!store.latest_new_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, Slot(1)); -} +// TODO: Add vote/attestation unit tests for devnet2 +// +// This file previously contained devnet1 vote tests that are no longer compatible +// with the devnet2 SignedAttestation structure (validator_id is now a direct field, +// message is AttestationData instead of Attestation wrapper). +// +// Tests to implement: +// - test_single_vote_updates_head +// - test_multiple_votes_same_block +// - test_competing_votes_different_blocks +// - test_vote_weight_accumulation +// - test_vote_from_unknown_validator +// - test_duplicate_vote_ignored +// - test_vote_for_unknown_block +// - test_late_vote_still_counted +// - test_vote_changes_fork_choice diff --git a/lean_client/networking/Cargo.toml b/lean_client/networking/Cargo.toml index 0584a0e..57578d1 100644 --- a/lean_client/networking/Cargo.toml +++ b/lean_client/networking/Cargo.toml @@ -5,8 +5,6 @@ edition = "2024" [features] default = [] -devnet1 = ["containers/devnet1", "env-config/devnet1"] -devnet2 = ["containers/devnet2", "env-config/devnet2"] [dependencies] env-config = { path = "../env-config", default-features = false } diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index f062f63..5b13b66 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -311,9 +311,6 @@ where } } Ok(GossipsubMessage::Attestation(signed_attestation)) => { - #[cfg(not(feature = "devnet2"))] - let slot = signed_attestation.message.data.slot.0; - #[cfg(feature = "devnet2")] let slot = signed_attestation.message.slot.0; if let Err(err) = self @@ -524,9 +521,6 @@ where } } OutboundP2pRequest::GossipAttestation(signed_attestation) => { - #[cfg(not(feature = "devnet2"))] - let slot = signed_attestation.message.data.slot.0; - #[cfg(feature = "devnet2")] let slot = signed_attestation.message.slot.0; match signed_attestation.to_ssz() { diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index fab4260..193eb18 100644 --- a/lean_client/networking/src/types.rs +++ b/lean_client/networking/src/types.rs @@ -93,11 +93,6 @@ impl Display for ChainMessage { ChainMessage::ProcessBlock { signed_block_with_attestation, .. } => { write!(f, "ProcessBlockWithAttestation(slot={})", signed_block_with_attestation.message.block.slot.0) } - #[cfg(not(feature = "devnet2"))] - ChainMessage::ProcessAttestation { signed_attestation, .. } => { - write!(f, "ProcessAttestation(slot={})", signed_attestation.message.data.slot.0) - } - #[cfg(feature = "devnet2")] ChainMessage::ProcessAttestation { signed_attestation, .. } => { write!(f, "ProcessAttestation(slot={})", signed_attestation.message.slot.0) } diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index 172bd90..f9f5b3c 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -220,9 +220,6 @@ async fn main() { block: genesis_block, proposer_attestation: genesis_proposer_attestation, }, - #[cfg(not(feature = "devnet2"))] - signature: PersistentList::default(), - #[cfg(feature = "devnet2")] signature: BlockSignatures { attestation_signatures: PersistentList::default(), proposer_signature: Signature::default(), @@ -434,9 +431,6 @@ async fn main() { if last_attestation_slot != Some(current_slot) { let attestations = vs.create_attestations(&store, Slot(current_slot)); for signed_att in attestations { - #[cfg(not(feature = "devnet2"))] - let validator_id = signed_att.message.validator_id.0; - #[cfg(feature = "devnet2")] let validator_id = signed_att.validator_id; info!( slot = current_slot, @@ -444,19 +438,6 @@ async fn main() { "Broadcasting attestation" ); - #[cfg(not(feature = "devnet2"))] - match on_attestation(&mut store, signed_att.clone(), false) { - Ok(()) => { - if let Err(e) = chain_outbound_sender.send( - OutboundP2pRequest::GossipAttestation(signed_att) - ) { - warn!("Failed to gossip attestation: {}", e); - } - } - Err(e) => warn!("Error processing own attestation: {}", e), - } - - #[cfg(feature = "devnet2")] match on_attestation(&mut store, signed_att.clone(), false) { Ok(()) => { if let Err(e) = chain_outbound_sender.send( @@ -552,22 +533,9 @@ async fn main() { should_gossip, .. } => { - #[cfg(not(feature = "devnet2"))] - let att_slot = signed_attestation.message.data.slot.0; - #[cfg(not(feature = "devnet2"))] - let source_slot = signed_attestation.message.data.source.slot.0; - #[cfg(not(feature = "devnet2"))] - let target_slot = signed_attestation.message.data.target.slot.0; - #[cfg(not(feature = "devnet2"))] - let validator_id = signed_attestation.message.validator_id.0; - - #[cfg(feature = "devnet2")] let att_slot = signed_attestation.message.slot.0; - #[cfg(feature = "devnet2")] let source_slot = signed_attestation.message.source.slot.0; - #[cfg(feature = "devnet2")] let target_slot = signed_attestation.message.target.slot.0; - #[cfg(feature = "devnet2")] let validator_id = signed_attestation.validator_id; info!( diff --git a/lean_client/validator/Cargo.toml b/lean_client/validator/Cargo.toml index ab09109..81ad309 100644 --- a/lean_client/validator/Cargo.toml +++ b/lean_client/validator/Cargo.toml @@ -6,8 +6,6 @@ edition = "2021" [features] default = ["xmss-signing"] xmss-signing = ["leansig"] -devnet1 = ["containers/devnet1", "fork-choice/devnet1", "env-config/devnet1"] -devnet2 = ["containers/devnet2", "fork-choice/devnet2", "env-config/devnet2"] [dependencies] env-config = { path = "../env-config", default-features = false } diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 8a44c14..7559bf9 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -2,18 +2,14 @@ use std::collections::HashMap; use std::path::Path; -use containers::attestation::{AggregatedAttestations}; -#[cfg(feature = "devnet2")] -use containers::attestation::MultisigAggregatedSignature; use containers::block::BlockSignatures; -#[cfg(feature = "devnet2")] use containers::ssz; use containers::{ attestation::{Attestation, AttestationData, Signature, SignedAttestation}, block::{hash_tree_root, BlockWithAttestation, SignedBlockWithAttestation}, checkpoint::Checkpoint, types::{Uint64, ValidatorIndex}, - AggregatedAttestation, Slot, + Slot, }; use fork_choice::store::{get_proposal_head, get_vote_target, Store}; use tracing::{info, warn}; @@ -178,9 +174,6 @@ impl ValidatorService { .latest_new_attestations .values() .filter(|att| { - #[cfg(not(feature = "devnet2"))] - let data = &att.message.data; - #[cfg(feature = "devnet2")] let data = &att.message; // Source must match the parent state's justified checkpoint (not store's!) let source_matches = data.source == parent_state.latest_justified; @@ -193,13 +186,6 @@ impl ValidatorService { }) .collect(); - #[cfg(not(feature = "devnet2"))] - let valid_attestations: Vec = valid_signed_attestations - .iter() - .map(|att| att.message.clone()) - .collect(); - - #[cfg(feature = "devnet2")] let valid_attestations: Vec = valid_signed_attestations .iter() .map(|att| att.message.clone()) @@ -213,18 +199,6 @@ impl ValidatorService { ); // Build block with collected attestations (empty body - attestations go to state) - #[cfg(not(feature = "devnet2"))] - let (block, _post_state, _collected_atts, sigs) = parent_state.build_block( - slot, - proposer_index, - parent_root, - Some(valid_attestations), - None, - None, - None, - None, - )?; - #[cfg(feature = "devnet2")] let (block, _post_state, _collected_atts, sigs) = { let valid_attestations: Vec = valid_attestations .iter() @@ -245,16 +219,6 @@ impl ValidatorService { )? }; - #[cfg(not(feature = "devnet2"))] - let mut signatures = sigs; - #[cfg(not(feature = "devnet2"))] - for signed_att in &valid_signed_attestations { - signatures - .push(signed_att.signature.clone()) - .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; - } - - #[cfg(feature = "devnet2")] let signatures = sigs; info!( @@ -267,9 +231,6 @@ impl ValidatorService { ); // Sign the proposer attestation - #[cfg(not(feature = "devnet2"))] - let proposer_signature: Signature; - #[cfg(feature = "devnet2")] let proposer_signature: Signature; if let Some(ref key_manager) = self.key_manager { @@ -279,17 +240,7 @@ impl ValidatorService { match key_manager.sign(proposer_index.0, epoch, &message.0.into()) { Ok(sig) => { - #[cfg(not(feature = "devnet2"))] - { - signatures - .push(sig.clone()) - .map_err(|e| format!("Failed to add proposer signature: {:?}", e))?; - proposer_signature = sig; - } - #[cfg(feature = "devnet2")] - { - proposer_signature = sig; - } + proposer_signature = sig; info!(proposer = proposer_index.0, "Signed proposer attestation"); } Err(e) => { @@ -304,7 +255,6 @@ impl ValidatorService { // Convert signatures to PersistentList for BlockSignatures // Extract proof_data from AggregatedSignatureProof for wire format - #[cfg(feature = "devnet2")] let attestation_signatures = { let mut list = ssz::PersistentList::default(); for proof in signatures { @@ -319,9 +269,6 @@ impl ValidatorService { block, proposer_attestation, }, - #[cfg(not(feature = "devnet2"))] - signature: signatures, - #[cfg(feature = "devnet2")] signature: BlockSignatures { attestation_signatures, proposer_signature, @@ -365,18 +312,6 @@ impl ValidatorService { .validator_indices .iter() .filter_map(|&idx| { - #[cfg(not(feature = "devnet2"))] - let attestation = Attestation { - validator_id: Uint64(idx), - data: AttestationData { - slot, - head: head_checkpoint.clone(), - target: vote_target.clone(), - source: store.latest_justified.clone(), - }, - }; - - #[cfg(feature = "devnet2")] let attestation = AttestationData { slot, head: head_checkpoint.clone(), @@ -421,24 +356,11 @@ impl ValidatorService { Signature::default() }; - { - #[cfg(not(feature = "devnet2"))] - { - Some(SignedAttestation { - message: attestation, - signature, - }) - } - - #[cfg(feature = "devnet2")] - { - Some(SignedAttestation { - validator_id: idx, - message: attestation, - signature, - }) - } - } + Some(SignedAttestation { + validator_id: idx, + message: attestation, + signature, + }) }) .collect() } From a3b16a3f4c33b30f3e3f49ec9286e07a2df6a9ed Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Sun, 18 Jan 2026 16:17:51 +0200 Subject: [PATCH 20/24] Implemented requested changes mentioned in the PR #37 --- lean_client/containers/src/attestation.rs | 13 +++---------- lean_client/containers/src/state.rs | 8 +++----- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index fb42916..0aad35a 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -340,20 +340,13 @@ impl AggregatedAttestation { }) .collect() } -} - -/// Trait for checking duplicate attestation data. -pub trait HasDuplicateData { - /// Returns true if the list contains duplicate AttestationData. - fn has_duplicate_data(&self) -> bool; -} -impl HasDuplicateData for AggregatedAttestations { - fn has_duplicate_data(&self) -> bool { + /// Returns true if the provided list contains duplicate AttestationData. + pub fn has_duplicate_data(attestations: &AggregatedAttestations) -> bool { use ssz::SszHash; use std::collections::HashSet; let mut seen: HashSet = HashSet::new(); - for attestation in self { + for attestation in attestations { let root = attestation.data.hash_tree_root(); if !seen.insert(root) { return true; diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index bdc6f57..a84d1cb 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use ssz::PersistentList as List; use ssz_derive::Ssz; use std::collections::BTreeMap; -use crate::attestation::{AggregatedAttestations, HasDuplicateData}; +use crate::attestation::{AggregatedAttestation, AggregatedAttestations}; pub const VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const JUSTIFICATION_ROOTS_LIMIT: usize = 1 << 18; // 262144 @@ -295,7 +295,7 @@ impl State { pub fn process_block(&self, block: &Block) -> Result { let state = self.process_block_header(block)?; - if block.body.attestations.has_duplicate_data() { + if AggregatedAttestation::has_duplicate_data(&block.body.attestations) { return Err("Block contains duplicate AttestationData".to_string()); } @@ -471,9 +471,7 @@ impl State { if 3 * count >= 2 * num_validators { *latest_justified = vote.target.clone(); - while justified_slots_working.len() <= target_slot_int { - justified_slots_working.push(false); - } + justified_slots_working.extend(std::iter::repeat_n(false, (target_slot_int + 1).saturating_sub(justified_slots_working.len()))); justified_slots_working[target_slot_int] = true; justifications.remove(&target_root); From 53e6e0aa105f1ec4b5776f4cee3ff392bfbd188e Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Sun, 18 Jan 2026 17:14:54 +0200 Subject: [PATCH 21/24] Changed tests to match devnet2 implementation --- .../tests/test_vectors/block_processing.rs | 18 +- .../containers/tests/test_vectors/mod.rs | 8 +- .../containers/tests/test_vectors/runner.rs | 3 +- .../tests/test_vectors/verify_signatures.rs | 13 +- .../containers/tests/unit_tests/common.rs | 10 +- .../tests/unit_tests/state_basic.rs | 8 +- .../tests/unit_tests/state_justifications.rs | 7 +- .../tests/unit_tests/state_process.rs | 28 +- .../tests/unit_tests/state_transition.rs | 21 +- .../tests/fork_choice_test_vectors.rs | 305 +++++++++++++++++- .../fork_choice/tests/unit_tests/votes.rs | 258 ++++++++++++++- 11 files changed, 576 insertions(+), 103 deletions(-) diff --git a/lean_client/containers/tests/test_vectors/block_processing.rs b/lean_client/containers/tests/test_vectors/block_processing.rs index 61c196b..d897513 100644 --- a/lean_client/containers/tests/test_vectors/block_processing.rs +++ b/lean_client/containers/tests/test_vectors/block_processing.rs @@ -1,16 +1,10 @@ -// Integration test: All block processing test vectors +// Integration test: All block processing test vectors for devnet2 format // -// TODO: Update these tests for devnet2 format: -// - test_process_first_block_after_genesis -// - test_blocks_with_gaps -// - test_linear_chain_multiple_blocks -// - test_block_extends_deep_chain -// - test_empty_blocks -// - test_empty_blocks_with_missed_slots -// - test_block_at_large_slot_number -// - test_block_with_invalid_parent_root -// - test_block_with_invalid_proposer -// - test_block_with_invalid_state_root +// NOTE: These tests are currently disabled because the JSON test vector files +// use a wrapper format (e.g., "validators": {"data": [...]}) that doesn't match +// the Rust deserializer expectations (which expects a direct list). +// The test vectors need to be regenerated by leanSpec to match the expected format, +// or the deserialization logic needs to be updated. use super::runner::TestRunner; diff --git a/lean_client/containers/tests/test_vectors/mod.rs b/lean_client/containers/tests/test_vectors/mod.rs index 106ac3a..83b9f84 100644 --- a/lean_client/containers/tests/test_vectors/mod.rs +++ b/lean_client/containers/tests/test_vectors/mod.rs @@ -1,10 +1,6 @@ -//! Test vector modules +//! Test vector modules for devnet2 format //! -//! TODO: Update these test vectors for devnet2 format: -//! - Update runner.rs to use AggregatedAttestations -//! - Update block_processing.rs tests -//! - Update genesis.rs tests -//! - Update verify_signatures.rs to use BlockSignatures structure +//! Contains test runners and test cases for block processing, genesis, and signature verification. // Test vector modules pub mod runner; diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index 19a8ef1..f0836d7 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -553,7 +553,8 @@ impl TestRunner { // Test runner for verify_signatures test vectors // Tests XMSS signature verification on SignedBlockWithAttestation // - // TODO: Update for devnet2 format - needs BlockSignatures structure + // NOTE: Disabled until test vector files are regenerated for devnet2 BlockSignatures format. + // The current JSON test vectors use signature.data array instead of attestation_signatures + proposer_signature. /* pub fn run_verify_signatures_test>(path: P) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; diff --git a/lean_client/containers/tests/test_vectors/verify_signatures.rs b/lean_client/containers/tests/test_vectors/verify_signatures.rs index cd6a14a..8be1b4e 100644 --- a/lean_client/containers/tests/test_vectors/verify_signatures.rs +++ b/lean_client/containers/tests/test_vectors/verify_signatures.rs @@ -1,13 +1,12 @@ -// Integration test: verify_signatures test vectors +// Integration test: verify_signatures test vectors for devnet2 format // Tests XMSS signature verification on SignedBlockWithAttestation // -// TODO: Update these tests for devnet2 format: -// - test_proposer_signature -// - test_proposer_and_attester_signatures -// - test_invalid_signature -// - test_mixed_valid_invalid_signatures +// NOTE: These tests are currently disabled because the JSON test vector files +// use the old signature format (signature.data array) rather than the devnet2 +// BlockSignatures structure (attestation_signatures + proposer_signature). +// The test vectors need to be regenerated by leanSpec to match the new format. // -// NOTE: Without the `xmss-verify` feature, signature verification only checks +// Without the `xmss-verify` feature, signature verification only checks // structure (attestation count matches signature count, validator indices valid). // Full cryptographic verification requires `--features xmss-verify`. // diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 611cff3..88e5242 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -1,9 +1,7 @@ -//! Common test utilities -//! -//! TODO: Update these test utilities for devnet2 format: -//! - Update create_block to use AggregatedAttestations instead of Attestations -//! - Update SignedBlockWithAttestation to use BlockSignatures structure -//! - Update process_attestations calls to use new signature format +//! Common test utilities for devnet2 format +//! +//! Helper functions for creating test blocks, states, and attestations +//! using the devnet2 data structures. use containers::{Attestation, Attestations, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators, AggregatedAttestation, Signature}; use ssz::{PersistentList}; diff --git a/lean_client/containers/tests/unit_tests/state_basic.rs b/lean_client/containers/tests/unit_tests/state_basic.rs index d815e55..d6a5cdc 100644 --- a/lean_client/containers/tests/unit_tests/state_basic.rs +++ b/lean_client/containers/tests/unit_tests/state_basic.rs @@ -1,10 +1,6 @@ -//! State basic tests +//! State basic tests for devnet2 format //! -//! TODO: Update these tests for devnet2 format: -//! - test_generate_genesis -//! - test_proposer_round_robin -//! - test_slot_justifiability_rules -//! - test_hash_tree_root +//! Tests for genesis generation, proposer selection, slot rules, and hash tree root. // tests/state_basic.rs use containers::{block::{BlockBody, hash_tree_root}, state::State, types::Uint64, ValidatorIndex}; diff --git a/lean_client/containers/tests/unit_tests/state_justifications.rs b/lean_client/containers/tests/unit_tests/state_justifications.rs index bb9f469..1876cef 100644 --- a/lean_client/containers/tests/unit_tests/state_justifications.rs +++ b/lean_client/containers/tests/unit_tests/state_justifications.rs @@ -1,9 +1,6 @@ -//! State justifications tests +//! State justifications tests for devnet2 format //! -//! TODO: Update these tests for devnet2 format: -//! - test_get_justifications_empty -//! - test_get_justifications_single_root -//! - test_get_justifications_multiple_roots +//! Tests for justification get/set operations and roundtrip verification. // tests/state_justifications.rs use containers::{ diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index 4ad95ef..6f89527 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -1,12 +1,7 @@ //! State process tests //! -//! TODO: Update these tests for devnet2 format: -//! - test_process_slot -//! - test_process_slots -//! - test_process_slots_backwards -//! - test_process_block_header_valid -//! - test_process_block_header_invalid -//! - test_process_attestations_justification_and_finalization +//! Tests for state processing functions including slot transitions, +//! block header processing, and attestation processing. // tests/state_process.rs use containers::{ @@ -15,7 +10,7 @@ use containers::{ slot::Slot, state::State, types::{Bytes32, Uint64, ValidatorIndex}, - Attestation, AttestationData, + Attestation, AttestationData, AggregatedAttestation, }; use pretty_assertions::assert_eq; use rstest::{fixture, rstest}; @@ -116,8 +111,6 @@ fn test_process_block_header_invalid( } // This test verifies that attestations correctly justify and finalize slots -// TODO: Update for devnet2 - needs AggregatedAttestations format -/* #[test] fn test_process_attestations_justification_and_finalization() { let mut state = genesis_state(); @@ -148,6 +141,7 @@ fn test_process_attestations_justification_and_finalization() { slot: Slot(4), }; + // Create attestations for slot 4 using devnet2 format let attestations_for_4: Vec = (0..7) .map(|i| Attestation { validator_id: Uint64(i), @@ -160,10 +154,13 @@ fn test_process_attestations_justification_and_finalization() { }) .collect(); - // Convert Vec to PersistentList - let mut attestations_list: List<_, U4096> = List::default(); - for a in attestations_for_4 { - attestations_list.push(a).unwrap(); + // Aggregate attestations for devnet2 format + let aggregated = AggregatedAttestation::aggregate_by_data(&attestations_for_4); + + // Convert to AggregatedAttestations (PersistentList) + let mut attestations_list: List = List::default(); + for agg in aggregated { + attestations_list.push(agg).unwrap(); } let new_state = state.process_attestations(&attestations_list); @@ -173,5 +170,4 @@ fn test_process_attestations_justification_and_finalization() { assert_eq!(justified_slot_4, true); assert_eq!(new_state.latest_finalized, genesis_checkpoint); assert!(!new_state.get_justifications().contains_key(&checkpoint4.root)); -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index 789b999..451889f 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -1,16 +1,14 @@ //! State transition tests -//! -//! TODO: Update these tests for devnet2 format: -//! - test_state_transition_full -//! - test_state_transition_invalid_signatures -//! - test_state_transition_bad_state_root +//! +//! Tests for full state transitions including signature validation +//! and state root verification. // tests/state_transition.rs use containers::{ - block::{hash_tree_root, Block, BlockWithAttestation, SignedBlockWithAttestation}, + block::{hash_tree_root, Block, BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, state::State, types::{Bytes32, Uint64}, - Attestation, Slot, + Attestation, Signature, Slot, }; use pretty_assertions::assert_eq; use rstest::fixture; @@ -92,8 +90,7 @@ fn test_state_transition_invalid_signatures() { assert_eq!(result.unwrap_err(), "Block signatures must be valid"); } -// TODO: Update for devnet2 - needs BlockSignatures structure -/* +// Test with bad state root using devnet2 BlockSignatures structure #[test] fn test_state_transition_bad_state_root() { let state = genesis_state(); @@ -110,14 +107,16 @@ fn test_state_transition_bad_state_root() { block, proposer_attestation: Attestation::default(), }, - signature: PersistentList::default(), + signature: BlockSignatures { + attestation_signatures: PersistentList::default(), + proposer_signature: Signature::default(), + }, }; let result = state.state_transition(final_signed_block_with_attestation, true); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Invalid block state root"); } -*/ #[test] fn test_state_transition_devnet2() { diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index c5a8da9..71e517b 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -1,17 +1,288 @@ -// TODO: Add fork choice test vectors for devnet2 -// -// This file previously contained devnet1 test vectors that are no longer compatible -// with the devnet2 data structures (SignedAttestation now uses AttestationData directly -// instead of Attestation wrapper, BlockSignatures structure changed, etc.) -// -// Tests to implement: -// - test_genesis_state_transition -// - test_basic_slot_transition -// - test_attestation_processing -// - test_justification_and_finalization -// - test_multiple_attestations -// - test_fork_choice_with_competing_blocks -// - test_reorg_on_higher_justified -// - test_finality_prevents_reorg -// - test_attestation_from_future_slot -// - test_attestation_with_wrong_source +//! Fork choice test vectors for devnet2 +//! +//! Integration tests for fork choice rule implementation +//! using devnet2 data structures. + +use containers::{ + attestation::{AttestationData, SignedAttestation}, + block::{Block, BlockBody, BlockWithAttestation, SignedBlockWithAttestation}, + checkpoint::Checkpoint, + config::Config, + state::State, + validator::Validator, + Bytes32, Slot, Uint64, ValidatorIndex, +}; +use fork_choice::store::{get_forkchoice_store, get_fork_choice_head, Store}; +use ssz::SszHash; +use std::collections::HashMap; + +/// Helper to create a genesis store for testing +fn create_genesis_store() -> Store { + let config = Config { genesis_time: 0 }; + let validators = vec![Validator::default(); 10]; + let state = State::generate_genesis_with_validators(Uint64(0), validators); + + let block = Block { + slot: Slot(0), + proposer_index: ValidatorIndex(0), + parent_root: Bytes32::default(), + state_root: Bytes32(state.hash_tree_root()), + body: BlockBody::default(), + }; + + let signed_block = SignedBlockWithAttestation { + message: BlockWithAttestation { + block, + proposer_attestation: Default::default(), + }, + signature: Default::default(), + }; + + get_forkchoice_store(state, signed_block, config) +} + +/// Helper to create a signed attestation +fn create_attestation( + validator_id: u64, + slot: u64, + head: Checkpoint, + target: Checkpoint, + source: Checkpoint, +) -> SignedAttestation { + SignedAttestation { + validator_id, + message: AttestationData { + slot: Slot(slot), + head, + target, + source, + }, + signature: Default::default(), + } +} + +/// Helper to add a block to the store +fn add_block(store: &mut Store, slot: u64, parent_root: Bytes32, proposer: u64) -> Bytes32 { + let block = Block { + slot: Slot(slot), + proposer_index: ValidatorIndex(proposer), + parent_root, + state_root: Bytes32::default(), + body: BlockBody::default(), + }; + let block_root = Bytes32(block.hash_tree_root()); + + store.blocks.insert(block_root, SignedBlockWithAttestation { + message: BlockWithAttestation { + block, + proposer_attestation: Default::default(), + }, + signature: Default::default(), + }); + + block_root +} + +#[test] +fn test_genesis_state_transition() { + let store = create_genesis_store(); + + // Verify genesis state is properly initialized + assert!(!store.head.0.is_zero()); + assert_eq!(store.blocks.len(), 1); + assert_eq!(store.states.len(), 1); + + // Genesis should be both justified and finalized + assert_eq!(store.latest_justified.slot, Slot(0)); + assert_eq!(store.latest_finalized.slot, Slot(0)); +} + +#[test] +fn test_basic_slot_transition() { + let mut store = create_genesis_store(); + let genesis_root = store.head; + + // Add blocks at slots 1, 2, 3 + let block1_root = add_block(&mut store, 1, genesis_root, 0); + let block2_root = add_block(&mut store, 2, block1_root, 0); + let block3_root = add_block(&mut store, 3, block2_root, 0); + + assert_eq!(store.blocks.len(), 4); + + // Without attestations and min_votes=1, head should stay at genesis + // (no blocks have enough votes to be considered) + let empty_attestations = HashMap::new(); + let head = get_fork_choice_head(&store, genesis_root, &empty_attestations, 1); + assert_eq!(head, genesis_root); + + // With attestation for block3 and min_votes=1, head should follow the voted chain + let mut attestations = HashMap::new(); + let checkpoint = Checkpoint { root: block3_root, slot: Slot(3) }; + let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; + + attestations.insert(ValidatorIndex(0), create_attestation( + 0, 3, checkpoint.clone(), checkpoint.clone(), genesis_checkpoint.clone() + )); + + // The fork choice should follow the chain with votes to find the heaviest head + let head = get_fork_choice_head(&store, genesis_root, &attestations, 1); + + // With 1 vote on block3, the entire chain block1->block2->block3 gets 1 vote each + // So head should be block3 (the tip of the voted chain) + assert_eq!(head, block3_root); +} + +#[test] +fn test_attestation_processing() { + let mut store = create_genesis_store(); + let genesis_root = store.head; + let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; + + // Create a block + let block1_root = add_block(&mut store, 1, genesis_root, 0); + let block1_checkpoint = Checkpoint { root: block1_root, slot: Slot(1) }; + + // Process attestations from multiple validators + let mut attestations = HashMap::new(); + for i in 0..5 { + attestations.insert(ValidatorIndex(i), create_attestation( + i, 1, block1_checkpoint.clone(), block1_checkpoint.clone(), genesis_checkpoint.clone() + )); + } + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + assert_eq!(head, block1_root); +} + +#[test] +fn test_multiple_attestations() { + let mut store = create_genesis_store(); + let genesis_root = store.head; + let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; + + // Create a chain of blocks + let block1_root = add_block(&mut store, 1, genesis_root, 0); + let block2_root = add_block(&mut store, 2, block1_root, 0); + let block3_root = add_block(&mut store, 3, block2_root, 0); + + let block3_checkpoint = Checkpoint { root: block3_root, slot: Slot(3) }; + + // All validators attest to block3 + let mut attestations = HashMap::new(); + for i in 0..10 { + attestations.insert(ValidatorIndex(i), create_attestation( + i, 3, block3_checkpoint.clone(), block3_checkpoint.clone(), genesis_checkpoint.clone() + )); + } + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + assert_eq!(head, block3_root); +} + +#[test] +fn test_fork_choice_with_competing_blocks() { + let mut store = create_genesis_store(); + let genesis_root = store.head; + let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; + + // Create two competing forks at slot 1 + let fork_a_root = add_block(&mut store, 1, genesis_root, 0); + let fork_b_root = add_block(&mut store, 1, genesis_root, 1); // Different proposer + + let fork_a_checkpoint = Checkpoint { root: fork_a_root, slot: Slot(1) }; + let fork_b_checkpoint = Checkpoint { root: fork_b_root, slot: Slot(1) }; + + // 6 validators vote for fork A + let mut attestations = HashMap::new(); + for i in 0..6 { + attestations.insert(ValidatorIndex(i), create_attestation( + i, 1, fork_a_checkpoint.clone(), fork_a_checkpoint.clone(), genesis_checkpoint.clone() + )); + } + + // 4 validators vote for fork B + for i in 6..10 { + attestations.insert(ValidatorIndex(i), create_attestation( + i, 1, fork_b_checkpoint.clone(), fork_b_checkpoint.clone(), genesis_checkpoint.clone() + )); + } + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + + // Fork A should win with more votes + assert_eq!(head, fork_a_root); +} + +#[test] +fn test_finality_prevents_reorg() { + let mut store = create_genesis_store(); + let genesis_root = store.head; + let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; + + // Create a finalized chain + let block1_root = add_block(&mut store, 1, genesis_root, 0); + let block2_root = add_block(&mut store, 2, block1_root, 0); + + // Update finalized checkpoint + store.latest_finalized = Checkpoint { root: block1_root, slot: Slot(1) }; + + // Create competing fork from genesis (should not be chosen due to finality) + let competing_root = add_block(&mut store, 1, genesis_root, 1); + + let block2_checkpoint = Checkpoint { root: block2_root, slot: Slot(2) }; + let competing_checkpoint = Checkpoint { root: competing_root, slot: Slot(1) }; + + // More votes for competing fork + let mut attestations = HashMap::new(); + for i in 0..7 { + attestations.insert(ValidatorIndex(i), create_attestation( + i, 1, competing_checkpoint.clone(), competing_checkpoint.clone(), genesis_checkpoint.clone() + )); + } + for i in 7..10 { + attestations.insert(ValidatorIndex(i), create_attestation( + i, 2, block2_checkpoint.clone(), block2_checkpoint.clone(), genesis_checkpoint.clone() + )); + } + + // Start from finalized block1 + let head = get_fork_choice_head(&store, block1_root, &attestations, 0); + + // Should follow the chain from block1, not competing fork + assert_eq!(head, block2_root); +} + +#[test] +fn test_attestation_from_future_slot() { + let mut store = create_genesis_store(); + let genesis_root = store.head; + let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; + + // Create block at slot 1 + let block1_root = add_block(&mut store, 1, genesis_root, 0); + let block1_checkpoint = Checkpoint { root: block1_root, slot: Slot(1) }; + + // Attestation claims to be from slot 100 (future) + // The fork choice still processes it based on what block it points to + let mut attestations = HashMap::new(); + attestations.insert(ValidatorIndex(0), create_attestation( + 0, 100, block1_checkpoint.clone(), block1_checkpoint.clone(), genesis_checkpoint.clone() + )); + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + + // Should still follow the attestation to block1 + assert_eq!(head, block1_root); +} + +#[test] +fn test_empty_attestations_returns_root() { + let store = create_genesis_store(); + let genesis_root = store.head; + + let empty_attestations = HashMap::new(); + let head = get_fork_choice_head(&store, genesis_root, &empty_attestations, 0); + + // With no attestations, should return the provided root + assert_eq!(head, genesis_root); +} diff --git a/lean_client/fork_choice/tests/unit_tests/votes.rs b/lean_client/fork_choice/tests/unit_tests/votes.rs index 24b437e..fd579f0 100644 --- a/lean_client/fork_choice/tests/unit_tests/votes.rs +++ b/lean_client/fork_choice/tests/unit_tests/votes.rs @@ -1,16 +1,242 @@ -// TODO: Add vote/attestation unit tests for devnet2 -// -// This file previously contained devnet1 vote tests that are no longer compatible -// with the devnet2 SignedAttestation structure (validator_id is now a direct field, -// message is AttestationData instead of Attestation wrapper). -// -// Tests to implement: -// - test_single_vote_updates_head -// - test_multiple_votes_same_block -// - test_competing_votes_different_blocks -// - test_vote_weight_accumulation -// - test_vote_from_unknown_validator -// - test_duplicate_vote_ignored -// - test_vote_for_unknown_block -// - test_late_vote_still_counted -// - test_vote_changes_fork_choice +//! Vote/attestation unit tests for devnet2 +//! +//! Tests for vote processing and fork choice weight calculations +//! using the devnet2 SignedAttestation structure. + +use super::common::create_test_store; +use containers::{ + attestation::{AttestationData, SignedAttestation}, + block::{Block, BlockBody, BlockWithAttestation, SignedBlockWithAttestation}, + checkpoint::Checkpoint, + Bytes32, Slot, ValidatorIndex, +}; +use fork_choice::store::get_fork_choice_head; +use ssz::SszHash; +use std::collections::HashMap; + +/// Helper to create a SignedAttestation for devnet2 +fn create_signed_attestation( + validator_id: u64, + slot: u64, + head_root: Bytes32, + head_slot: u64, + target_root: Bytes32, + target_slot: u64, + source_root: Bytes32, + source_slot: u64, +) -> SignedAttestation { + SignedAttestation { + validator_id, + message: AttestationData { + slot: Slot(slot), + head: Checkpoint { + root: head_root, + slot: Slot(head_slot), + }, + target: Checkpoint { + root: target_root, + slot: Slot(target_slot), + }, + source: Checkpoint { + root: source_root, + slot: Slot(source_slot), + }, + }, + signature: Default::default(), + } +} + +#[test] +fn test_single_vote_updates_head() { + let store = create_test_store(); + let genesis_root = store.head; + + // Create attestation pointing to genesis + let attestation = create_signed_attestation( + 0, // validator_id + 1, // slot + genesis_root, // head_root + 0, // head_slot + genesis_root, // target_root + 0, // target_slot + genesis_root, // source_root + 0, // source_slot + ); + + let mut attestations = HashMap::new(); + attestations.insert(ValidatorIndex(0), attestation); + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + + // With only one block, head should still be genesis + assert_eq!(head, genesis_root); +} + +#[test] +fn test_multiple_votes_same_block() { + let store = create_test_store(); + let genesis_root = store.head; + + // Multiple validators vote for same block + let mut attestations = HashMap::new(); + for i in 0..5 { + let attestation = create_signed_attestation( + i, + 1, + genesis_root, + 0, + genesis_root, + 0, + genesis_root, + 0, + ); + attestations.insert(ValidatorIndex(i), attestation); + } + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + + // All votes on same block, head unchanged + assert_eq!(head, genesis_root); +} + +#[test] +fn test_competing_votes_different_blocks() { + let mut store = create_test_store(); + let genesis_root = store.head; + + // Create two competing blocks at slot 1 + let block_a = Block { + slot: Slot(1), + proposer_index: ValidatorIndex(0), + parent_root: genesis_root, + state_root: Bytes32::default(), + body: BlockBody::default(), + }; + let block_a_root = Bytes32(block_a.hash_tree_root()); + + let mut block_b = block_a.clone(); + block_b.proposer_index = ValidatorIndex(1); // Different proposer to get different root + let block_b_root = Bytes32(block_b.hash_tree_root()); + + store.blocks.insert(block_a_root, SignedBlockWithAttestation { + message: BlockWithAttestation { + block: block_a, + proposer_attestation: Default::default(), + }, + signature: Default::default(), + }); + + store.blocks.insert(block_b_root, SignedBlockWithAttestation { + message: BlockWithAttestation { + block: block_b, + proposer_attestation: Default::default(), + }, + signature: Default::default(), + }); + + // 3 votes for block_a, 2 votes for block_b + let mut attestations = HashMap::new(); + for i in 0..3 { + attestations.insert(ValidatorIndex(i), create_signed_attestation( + i, 1, block_a_root, 1, genesis_root, 0, genesis_root, 0, + )); + } + for i in 3..5 { + attestations.insert(ValidatorIndex(i), create_signed_attestation( + i, 1, block_b_root, 1, genesis_root, 0, genesis_root, 0, + )); + } + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + + // Block A should win with more votes + assert_eq!(head, block_a_root); +} + +#[test] +fn test_vote_weight_accumulation() { + let mut store = create_test_store(); + let genesis_root = store.head; + + // Create a chain: genesis -> block1 -> block2 + let block1 = Block { + slot: Slot(1), + proposer_index: ValidatorIndex(0), + parent_root: genesis_root, + state_root: Bytes32::default(), + body: BlockBody::default(), + }; + let block1_root = Bytes32(block1.hash_tree_root()); + + let block2 = Block { + slot: Slot(2), + proposer_index: ValidatorIndex(0), + parent_root: block1_root, + state_root: Bytes32::default(), + body: BlockBody::default(), + }; + let block2_root = Bytes32(block2.hash_tree_root()); + + store.blocks.insert(block1_root, SignedBlockWithAttestation { + message: BlockWithAttestation { block: block1, proposer_attestation: Default::default() }, + signature: Default::default(), + }); + store.blocks.insert(block2_root, SignedBlockWithAttestation { + message: BlockWithAttestation { block: block2, proposer_attestation: Default::default() }, + signature: Default::default(), + }); + + // Vote for block2 - should accumulate to block1 as well + let mut attestations = HashMap::new(); + attestations.insert(ValidatorIndex(0), create_signed_attestation( + 0, 2, block2_root, 2, genesis_root, 0, genesis_root, 0, + )); + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + + // Head should be block2 (the one with votes) + assert_eq!(head, block2_root); +} + +#[test] +fn test_duplicate_vote_uses_latest() { + let store = create_test_store(); + let genesis_root = store.head; + + // Same validator can only have one vote in the map (latest wins) + let mut attestations = HashMap::new(); + + // Insert a vote + attestations.insert(ValidatorIndex(0), create_signed_attestation( + 0, 1, genesis_root, 0, genesis_root, 0, genesis_root, 0, + )); + + // "Update" with same validator - only latest is kept + attestations.insert(ValidatorIndex(0), create_signed_attestation( + 0, 2, genesis_root, 0, genesis_root, 0, genesis_root, 0, + )); + + // Should only have 1 attestation + assert_eq!(attestations.len(), 1); + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + assert_eq!(head, genesis_root); +} + +#[test] +fn test_vote_for_unknown_block_ignored() { + let store = create_test_store(); + let genesis_root = store.head; + let unknown_root = Bytes32(ssz::H256::from_slice(&[0xff; 32])); + + // Vote for block that doesn't exist + let mut attestations = HashMap::new(); + attestations.insert(ValidatorIndex(0), create_signed_attestation( + 0, 1, unknown_root, 1, genesis_root, 0, genesis_root, 0, + )); + + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); + + // Should still return genesis since unknown block is skipped + assert_eq!(head, genesis_root); +} From be1836ca112208e9de9a81f02ddb8c1a5490b94b Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Sun, 18 Jan 2026 17:55:56 +0200 Subject: [PATCH 22/24] Applied cargo fmt formatting --- lean_client/containers/src/attestation.rs | 29 +- lean_client/containers/src/serde_helpers.rs | 3 +- lean_client/containers/src/state.rs | 141 ++++++--- .../containers/tests/test_vectors/runner.rs | 295 +++++++++++------- .../containers/tests/unit_tests/common.rs | 53 ++-- .../containers/tests/unit_tests/mod.rs | 2 +- .../tests/unit_tests/state_process.rs | 68 +++- .../tests/unit_tests/state_transition.rs | 6 +- lean_client/fork_choice/src/handlers.rs | 28 +- lean_client/fork_choice/src/store.rs | 48 +-- .../tests/fork_choice_test_vectors.rs | 278 +++++++++++------ .../fork_choice/tests/unit_tests/votes.rs | 184 ++++++----- lean_client/validator/src/lib.rs | 2 +- 13 files changed, 719 insertions(+), 418 deletions(-) diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 931d1e5..742bc6c 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use ssz::BitList; use ssz::ByteVector; use ssz_derive::Ssz; -use typenum::{Prod, Sum, U100, U12, U31, U1024}; +use typenum::{Prod, Sum, U100, U1024, U12, U31}; // Type-level number for 1 MiB (1048576 = 1024 * 1024) pub type U1048576 = Prod; @@ -32,7 +32,7 @@ pub type AttestationSignatures = ssz::PersistentList; /// Aggregated signature proof from lean-multisig zkVM. -/// +/// /// This is a variable-length byte list (up to 1 MiB) containing the serialized /// proof bytes from `xmss_aggregate_signatures()`. The `#[ssz(transparent)]` /// attribute makes this type serialize directly as a ByteList for SSZ wire format. @@ -85,12 +85,9 @@ impl MultisigAggregatedSignature { return Err(AggregationError::MismatchedLengths); } - let proof_bytes = lean_multisig::xmss_aggregate_signatures( - public_keys, - signatures, - message, - epoch, - ).map_err(|_| AggregationError::AggregationFailed)?; + let proof_bytes = + lean_multisig::xmss_aggregate_signatures(public_keys, signatures, message, epoch) + .map_err(|_| AggregationError::AggregationFailed)?; Ok(Self::new(proof_bytes)) } @@ -113,7 +110,8 @@ impl MultisigAggregatedSignature { message, self.0.as_bytes(), epoch, - ).map_err(|_| AggregationError::VerificationFailed) + ) + .map_err(|_| AggregationError::VerificationFailed) } /// Verify the aggregated payload against validators and message. @@ -142,7 +140,7 @@ impl MultisigAggregatedSignature { // 2. Convert message bytes to field element format // 3. Call lean_multisig::xmss_verify_aggregated_signatures let _ = (validators, message, epoch); - + Ok(()) } } @@ -176,10 +174,12 @@ pub struct AggregatedSignatureProof { impl AggregatedSignatureProof { /// Create a new AggregatedSignatureProof. pub fn new(participants: AggregationBits, proof_data: MultisigAggregatedSignature) -> Self { - Self { participants, proof_data } + Self { + participants, + proof_data, + } } - pub fn from_aggregation(participant_ids: &[u64], proof: MultisigAggregatedSignature) -> Self { Self { participants: AggregationBits::from_validator_indices(participant_ids), @@ -281,7 +281,10 @@ pub struct SignatureKey { impl SignatureKey { /// Create a new signature key. pub fn new(validator_id: u64, data_root: crate::Bytes32) -> Self { - Self { validator_id, data_root } + Self { + validator_id, + data_root, + } } } diff --git a/lean_client/containers/src/serde_helpers.rs b/lean_client/containers/src/serde_helpers.rs index 7295ac5..987b86b 100644 --- a/lean_client/containers/src/serde_helpers.rs +++ b/lean_client/containers/src/serde_helpers.rs @@ -209,8 +209,7 @@ pub mod byte_list { let bytes = hex::decode(hex_str) .map_err(|e| D::Error::custom(format!("Invalid hex string: {}", e)))?; - ByteList::try_from(bytes) - .map_err(|_| D::Error::custom("ByteList exceeds maximum length")) + ByteList::try_from(bytes).map_err(|_| D::Error::custom("ByteList exceeds maximum length")) } pub fn serialize(value: &ByteList, serializer: S) -> Result diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 029f7fe..3b231e0 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -1,5 +1,9 @@ +use crate::attestation::{AggregatedAttestation, AggregatedAttestations}; use crate::validator::Validator; -use crate::{block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, Attestation, Bytes32, Checkpoint, Config, Signature, Slot, Uint64, ValidatorIndex}; +use crate::{ + block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, + Attestation, Bytes32, Checkpoint, Config, Signature, Slot, Uint64, ValidatorIndex, +}; use crate::{ HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, Validators, }; @@ -7,7 +11,6 @@ use serde::{Deserialize, Serialize}; use ssz::PersistentList as List; use ssz_derive::Ssz; use std::collections::BTreeMap; -use crate::attestation::{AggregatedAttestation, AggregatedAttestations}; pub const VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const JUSTIFICATION_ROOTS_LIMIT: usize = 1 << 18; // 262144 @@ -294,7 +297,7 @@ impl State { pub fn process_block(&self, block: &Block) -> Result { let state = self.process_block_header(block)?; - + if AggregatedAttestation::has_duplicate_data(&block.body.attestations) { return Err("Block contains duplicate AttestationData".to_string()); } @@ -402,7 +405,9 @@ impl State { } for aggregated_attestation in attestations { - let validator_ids = aggregated_attestation.aggregation_bits.to_validator_indices(); + let validator_ids = aggregated_attestation + .aggregation_bits + .to_validator_indices(); self.process_single_attestation( &aggregated_attestation.data, &validator_ids, @@ -414,7 +419,12 @@ impl State { ); } - self.finalize_attestation_processing(justifications, latest_justified, latest_finalized, justified_slots_working) + self.finalize_attestation_processing( + justifications, + latest_justified, + latest_finalized, + justified_slots_working, + ) } /// Process a single attestation's votes. @@ -436,11 +446,25 @@ impl State { let target_slot_int = target_slot.0 as usize; let source_slot_int = source_slot.0 as usize; - let source_is_justified = justified_slots_working.get(source_slot_int).copied().unwrap_or(false); - let target_already_justified = justified_slots_working.get(target_slot_int).copied().unwrap_or(false); - - let source_root_matches = self.historical_block_hashes.get(source_slot_int as u64).map(|r| *r == source_root).unwrap_or(false); - let target_root_matches = self.historical_block_hashes.get(target_slot_int as u64).map(|r| *r == target_root).unwrap_or(false); + let source_is_justified = justified_slots_working + .get(source_slot_int) + .copied() + .unwrap_or(false); + let target_already_justified = justified_slots_working + .get(target_slot_int) + .copied() + .unwrap_or(false); + + let source_root_matches = self + .historical_block_hashes + .get(source_slot_int as u64) + .map(|r| *r == source_root) + .unwrap_or(false); + let target_root_matches = self + .historical_block_hashes + .get(target_slot_int as u64) + .map(|r| *r == target_root) + .unwrap_or(false); let is_valid_vote = source_is_justified && !target_already_justified @@ -471,7 +495,10 @@ impl State { if 3 * count >= 2 * num_validators { *latest_justified = vote.target.clone(); - justified_slots_working.extend(std::iter::repeat_n(false, (target_slot_int + 1).saturating_sub(justified_slots_working.len()))); + justified_slots_working.extend(std::iter::repeat_n( + false, + (target_slot_int + 1).saturating_sub(justified_slots_working.len()), + )); justified_slots_working[target_slot_int] = true; justifications.remove(&target_root); @@ -537,10 +564,20 @@ impl State { available_attestations: Option>, known_block_roots: Option<&std::collections::HashSet>, gossip_signatures: Option<&std::collections::HashMap>, - aggregated_payloads: Option<&std::collections::HashMap>>, - ) -> Result<(Block, Self, Vec, Vec), String> { + aggregated_payloads: Option< + &std::collections::HashMap>, + >, + ) -> Result< + ( + Block, + Self, + Vec, + Vec, + ), + String, + > { use crate::attestation::{AggregatedAttestation, SignatureKey}; - + // Initialize attestation set let mut attestations = initial_attestations.unwrap_or_default(); @@ -577,11 +614,12 @@ impl State { Some(avail) => avail, None => { // No fixed-point: compute signatures and return - let (aggregated_attestations, aggregated_proofs) = self.compute_aggregated_signatures( - &attestations, - gossip_signatures, - aggregated_payloads, - )?; + let (aggregated_attestations, aggregated_proofs) = self + .compute_aggregated_signatures( + &attestations, + gossip_signatures, + aggregated_payloads, + )?; let mut final_attestations_list = AggregatedAttestations::default(); for att in &aggregated_attestations { @@ -600,7 +638,12 @@ impl State { }, }; - return Ok((final_block, post_state, aggregated_attestations, aggregated_proofs)); + return Ok(( + final_block, + post_state, + aggregated_attestations, + aggregated_proofs, + )); } }; @@ -638,8 +681,10 @@ impl State { // Check if we have a signature for this attestation let data_root = attestation.data.data_root_bytes(); let sig_key = SignatureKey::new(attestation.validator_id.0, data_root); - let has_gossip_sig = gossip_signatures.map_or(false, |gs| gs.contains_key(&sig_key)); - let has_block_proof = aggregated_payloads.map_or(false, |ap| ap.contains_key(&sig_key)); + let has_gossip_sig = + gossip_signatures.map_or(false, |gs| gs.contains_key(&sig_key)); + let has_block_proof = + aggregated_payloads.map_or(false, |ap| ap.contains_key(&sig_key)); if has_gossip_sig || has_block_proof { new_attestations.push(attestation.clone()); @@ -649,11 +694,12 @@ impl State { // Fixed point reached: no new attestations found if new_attestations.is_empty() { // Compute aggregated signatures - let (aggregated_attestations, aggregated_proofs) = self.compute_aggregated_signatures( - &attestations, - gossip_signatures, - aggregated_payloads, - )?; + let (aggregated_attestations, aggregated_proofs) = self + .compute_aggregated_signatures( + &attestations, + gossip_signatures, + aggregated_payloads, + )?; let mut final_attestations_list = AggregatedAttestations::default(); for att in &aggregated_attestations { @@ -672,7 +718,12 @@ impl State { }, }; - return Ok((final_block, post_state, aggregated_attestations, aggregated_proofs)); + return Ok(( + final_block, + post_state, + aggregated_attestations, + aggregated_proofs, + )); } // Add new attestations and continue iteration @@ -684,8 +735,16 @@ impl State { &self, attestations: &[Attestation], gossip_signatures: Option<&std::collections::HashMap>, - aggregated_payloads: Option<&std::collections::HashMap>>, - ) -> Result<(Vec, Vec), String> { + aggregated_payloads: Option< + &std::collections::HashMap>, + >, + ) -> Result< + ( + Vec, + Vec, + ), + String, + > { use crate::attestation::{AggregatedAttestation, AggregationBits, SignatureKey}; use std::collections::HashSet; @@ -726,12 +785,12 @@ impl State { // MultisigAggregatedSignature::aggregate(public_keys, signatures, message, epoch) if !gossip_ids.is_empty() { let participants = AggregationBits::from_validator_indices(&gossip_ids); - + // Create proof placeholder (matches Python test_mode behavior) // TODO: Call actual aggregation when lean-multisig supports proper encoding let proof_data = crate::MultisigAggregatedSignature::new(Vec::new()); let proof = crate::AggregatedSignatureProof::new(participants.clone(), proof_data); - + results.push(( AggregatedAttestation { aggregation_bits: participants, @@ -763,14 +822,10 @@ impl State { let (best_proof, covered_set) = candidates .iter() .map(|proof| { - let proof_validators: HashSet = proof - .get_participant_indices() - .into_iter() - .collect(); - let intersection: HashSet = remaining - .intersection(&proof_validators) - .copied() - .collect(); + let proof_validators: HashSet = + proof.get_participant_indices().into_iter().collect(); + let intersection: HashSet = + remaining.intersection(&proof_validators).copied().collect(); (proof, intersection) }) .max_by_key(|(_, intersection)| intersection.len()) @@ -784,7 +839,7 @@ impl State { // Record proof with its actual participants (from the proof itself) let covered_validators: Vec = best_proof.get_participant_indices(); let participants = AggregationBits::from_validator_indices(&covered_validators); - + results.push(( AggregatedAttestation { aggregation_bits: participants, @@ -806,11 +861,9 @@ impl State { } // Unzip results into parallel lists - let (aggregated_attestations, aggregated_proofs): (Vec<_>, Vec<_>) = + let (aggregated_attestations, aggregated_proofs): (Vec<_>, Vec<_>) = results.into_iter().unzip(); Ok((aggregated_attestations, aggregated_proofs)) } } - - diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index f0836d7..9be4935 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -6,14 +6,19 @@ use std::path::Path; pub struct TestRunner; impl TestRunner { - pub fn run_sequential_block_processing_tests>(path: P) -> Result<(), Box> { + pub fn run_sequential_block_processing_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; println!("Running test: {}", test_name); @@ -78,20 +83,22 @@ impl TestRunner { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } // Only check validator count if specified in post-state if let Some(expected_count) = post.validator_count { let num_validators = state.validators.len_u64(); - + if num_validators as usize != expected_count { return Err(format!( "Post-state validator count mismatch: expected {}, got {}", expected_count, num_validators - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); println!(" Validator count: {}", num_validators); @@ -100,38 +107,48 @@ impl TestRunner { println!(" Final slot: {:?}", state.slot); } } - + println!("\n✓✓✓ PASS: All blocks processed successfully with matching roots ✓✓✓"); } - + Ok(()) } - pub fn run_single_block_with_slot_gap_tests>(path: P) -> Result<(), Box> { + pub fn run_single_block_with_slot_gap_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; - + // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("Running test: {}", test_name); println!("Description: {}", test_case.info.description); if let Some(ref blocks) = test_case.blocks { let mut state = test_case.pre.clone(); - + for (idx, block) in blocks.iter().enumerate() { - println!("\nProcessing block {}: slot {:?} (gap from slot {:?})", idx + 1, block.slot, state.slot); - + println!( + "\nProcessing block {}: slot {:?} (gap from slot {:?})", + idx + 1, + block.slot, + state.slot + ); + // Advance state to the block's slot (this handles the slot gap) let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { return Err(format!( @@ -141,19 +158,19 @@ impl TestRunner { computed_parent_root ).into()); } - + println!(" ✓ Parent root matches: {:?}", computed_parent_root); - + // Process the block header let result = state_after_slots.process_block_header(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the vector if block.state_root != computed_state_root { return Err(format!( @@ -163,59 +180,69 @@ impl TestRunner { computed_state_root ).into()); } - + println!(" ✓ State root matches: {:?}", computed_state_root); - println!(" ✓ Block {} processed successfully (with {} empty slots)", idx + 1, block.slot.0 - test_case.pre.slot.0 - idx as u64); + println!( + " ✓ Block {} processed successfully (with {} empty slots)", + idx + 1, + block.slot.0 - test_case.pre.slot.0 - idx as u64 + ); } Err(e) => { return Err(format!("Block {} processing failed: {:?}", idx + 1, e).into()); } } } - + // Verify post-state conditions if let Some(post) = test_case.post { if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); } - + println!("\n✓✓✓ PASS: Block with slot gap processed successfully ✓✓✓"); } - + Ok(()) } - pub fn run_single_empty_block_tests>(path: P) -> Result<(), Box> { + pub fn run_single_empty_block_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; - + // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("Running test: {}", test_name); println!("Description: {}", test_case.info.description); if let Some(ref blocks) = test_case.blocks { let mut state = test_case.pre.clone(); - + // Should be exactly one block if blocks.len() != 1 { return Err(format!("Expected 1 block, found {}", blocks.len()).into()); } - + let block = &blocks[0]; println!("\nProcessing single empty block at slot {:?}", block.slot); - + // Verify it's an empty block (no attestations) let attestation_count = { let mut count = 0u64; @@ -227,18 +254,22 @@ impl TestRunner { } count }; - + if attestation_count > 0 { - return Err(format!("Expected empty block, but found {} attestations", attestation_count).into()); + return Err(format!( + "Expected empty block, but found {} attestations", + attestation_count + ) + .into()); } println!(" ✓ Confirmed: Block has no attestations (empty block)"); - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { return Err(format!( @@ -247,19 +278,19 @@ impl TestRunner { computed_parent_root ).into()); } - + println!(" ✓ Parent root matches: {:?}", computed_parent_root); - + // Process the block header let result = state_after_slots.process_block_header(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the vector if block.state_root != computed_state_root { return Err(format!( @@ -268,7 +299,7 @@ impl TestRunner { computed_state_root ).into()); } - + println!(" ✓ State root matches: {:?}", computed_state_root); println!(" ✓ Empty block processed successfully"); } @@ -276,38 +307,44 @@ impl TestRunner { return Err(format!("Block processing failed: {:?}", e).into()); } } - + // Verify post-state conditions if let Some(post) = test_case.post { if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); } - + println!("\n✓✓✓ PASS: Single empty block processed successfully ✓✓✓"); } - + Ok(()) } /// Generic test runner for block processing test vectors /// Handles all test vectors from test_blocks directory - pub fn run_block_processing_test>(path: P) -> Result<(), Box> { + pub fn run_block_processing_test>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); // Check if this is an invalid/exception test @@ -327,7 +364,7 @@ impl TestRunner { } let mut state = test_case.pre.clone(); - + for (idx, block) in blocks.iter().enumerate() { // Check if this is a gap (missed slots) let gap_size = if idx == 0 { @@ -335,19 +372,24 @@ impl TestRunner { } else { block.slot.0 - state.slot.0 - 1 }; - + if gap_size > 0 { - println!(" Block {}: slot {} (gap: {} empty slots)", idx + 1, block.slot.0, gap_size); + println!( + " Block {}: slot {} (gap: {} empty slots)", + idx + 1, + block.slot.0, + gap_size + ); } else { println!(" Block {}: slot {}", idx + 1, block.slot.0); } - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { println!(" \x1b[31m✗ FAIL: Parent root mismatch\x1b[0m"); @@ -355,7 +397,7 @@ impl TestRunner { println!(" Got: {:?}\n", computed_parent_root); return Err(format!("Block {} parent_root mismatch", idx + 1).into()); } - + // Check if block is empty (no attestations) let attestation_count = { let mut count = 0u64; @@ -367,17 +409,17 @@ impl TestRunner { } count }; - + // Process the full block (header + operations) let result = state_after_slots.process_block(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the block if block.state_root != computed_state_root { println!(" \x1b[31m✗ FAIL: State root mismatch\x1b[0m"); @@ -385,7 +427,7 @@ impl TestRunner { println!(" Got: {:?}\n", computed_state_root); return Err(format!("Block {} state_root mismatch", idx + 1).into()); } - + if attestation_count > 0 { println!(" ✓ Processed with {} attestation(s)", attestation_count); } else { @@ -399,13 +441,13 @@ impl TestRunner { } } } - + // Verify post-state conditions Self::verify_post_state(&state, &test_case)?; - + println!("\n\x1b[32m✓ PASS\x1b[0m\n"); } - + Ok(()) } @@ -413,50 +455,64 @@ impl TestRunner { /// Handles test vectors from test_genesis directory pub fn run_genesis_test>(path: P) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the TestVectorFile structure let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); let state = &test_case.pre; - + let num_validators = state.validators.len_u64(); - println!(" Genesis time: {}, slot: {}, validators: {}", state.config.genesis_time, state.slot.0, num_validators); - + println!( + " Genesis time: {}, slot: {}, validators: {}", + state.config.genesis_time, state.slot.0, num_validators + ); + // Verify it's at genesis (slot 0) if state.slot.0 != 0 { return Err(format!("Expected genesis at slot 0, got slot {}", state.slot.0).into()); } - + // Verify checkpoint initialization if state.latest_justified.slot.0 != 0 { - return Err(format!("Expected latest_justified at slot 0, got {}", state.latest_justified.slot.0).into()); + return Err(format!( + "Expected latest_justified at slot 0, got {}", + state.latest_justified.slot.0 + ) + .into()); } - + if state.latest_finalized.slot.0 != 0 { - return Err(format!("Expected latest_finalized at slot 0, got {}", state.latest_finalized.slot.0).into()); + return Err(format!( + "Expected latest_finalized at slot 0, got {}", + state.latest_finalized.slot.0 + ) + .into()); } - + // Verify empty historical data let has_history = state.historical_block_hashes.get(0).is_ok(); if has_history { return Err("Expected empty historical block hashes at genesis".into()); } - + println!(" ✓ Genesis state validated"); - + // Verify post-state if present if test_case.post.is_some() { Self::verify_post_state(state, &test_case)?; } - + println!("\n\x1b[32m✓ PASS\x1b[0m\n"); - + Ok(()) } @@ -473,10 +529,10 @@ impl TestRunner { for (idx, block) in blocks.iter().enumerate() { println!(" Block {}: slot {}", idx + 1, block.slot.0); - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Try to process the full block (header + body) - we expect this to fail let result = state_after_slots.process_block(block); @@ -484,14 +540,16 @@ impl TestRunner { Ok(new_state) => { // Block processing succeeded, now validate state root let computed_state_root = hash_tree_root(&new_state); - + if block.state_root != computed_state_root { error_occurred = true; println!(" ✓ Correctly rejected: Invalid block state root"); break; // Stop at first error } else { println!(" \x1b[31m✗ FAIL: Block processed successfully - but should have failed!\x1b[0m\n"); - return Err("Expected block processing to fail, but it succeeded".into()); + return Err( + "Expected block processing to fail, but it succeeded".into() + ); } } Err(e) => { @@ -501,52 +559,60 @@ impl TestRunner { } } } - + if !error_occurred { return Err("Expected an exception but all blocks processed successfully".into()); } } - + Ok(()) } /// Helper: Verify genesis state only (no blocks) fn verify_genesis_state(test_case: TestCase) -> Result<(), Box> { let state = &test_case.pre; - + // Verify post-state if present Self::verify_post_state(state, &test_case)?; - + Ok(()) } /// Helper: Verify post-state conditions - fn verify_post_state(state: &State, test_case: &TestCase) -> Result<(), Box> { + fn verify_post_state( + state: &State, + test_case: &TestCase, + ) -> Result<(), Box> { if let Some(ref post) = test_case.post { // Verify slot if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + // Verify validator count if specified if let Some(expected_count) = post.validator_count { let num_validators = state.validators.len_u64(); - + if num_validators as usize != expected_count { return Err(format!( "Post-state validator count mismatch: expected {}, got {}", expected_count, num_validators - ).into()); + ) + .into()); } - println!(" ✓ Post-state verified: slot {}, {} validators", state.slot.0, num_validators); + println!( + " ✓ Post-state verified: slot {}, {} validators", + state.slot.0, num_validators + ); } else { println!(" ✓ Post-state verified: slot {}", state.slot.0); } } - + Ok(()) } @@ -558,37 +624,37 @@ impl TestRunner { /* pub fn run_verify_signatures_test>(path: P) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the VerifySignaturesTestVectorFile structure let test_file: VerifySignaturesTestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file let (test_name, test_case) = test_file.tests.into_iter().next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); - + let anchor_state = test_case.anchor_state; let signed_block = test_case.signed_block_with_attestation; - + // Print some debug info about what we're verifying println!(" Block slot: {}", signed_block.message.block.slot.0); println!(" Proposer index: {}", signed_block.message.block.proposer_index.0); - + let attestation_count = signed_block.message.block.body.attestations.len_u64(); println!(" Attestations in block: {}", attestation_count); println!(" Proposer attestation validator: {}", signed_block.message.proposer_attestation.validator_id.0); - + let signature_count = signed_block.signature.len_u64(); println!(" Signatures: {}", signature_count); - + // Check if we expect this test to fail if let Some(ref exception) = test_case.expect_exception { println!(" Expecting exception: {}", exception); - + // Verify signatures - we expect this to fail (return false) let result = signed_block.verify_signatures(anchor_state); - + if result { println!(" \x1b[31m✗ FAIL: Signatures verified successfully but should have failed!\x1b[0m\n"); return Err("Expected signature verification to fail, but it succeeded".into()); @@ -599,7 +665,7 @@ impl TestRunner { } else { // Valid test case - signatures should verify successfully let result = signed_block.verify_signatures(anchor_state); - + if result { println!(" ✓ All signatures verified successfully"); println!("\n\x1b[32m✓ PASS\x1b[0m\n"); @@ -608,9 +674,8 @@ impl TestRunner { return Err("Signature verification failed".into()); } } - + Ok(()) } */ - } diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 88e5242..2981a2c 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -3,10 +3,18 @@ //! Helper functions for creating test blocks, states, and attestations //! using the devnet2 data structures. -use containers::{Attestation, Attestations, BlockWithAttestation, Config, SignedBlockWithAttestation, block::{Block, BlockBody, BlockHeader, hash_tree_root}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, ValidatorIndex}, Validators, AggregatedAttestation, Signature}; -use ssz::{PersistentList}; -use typenum::U4096; use containers::block::BlockSignatures; +use containers::{ + block::{hash_tree_root, Block, BlockBody, BlockHeader}, + checkpoint::Checkpoint, + slot::Slot, + state::State, + types::{Bytes32, ValidatorIndex}, + AggregatedAttestation, Attestation, Attestations, BlockWithAttestation, Config, Signature, + SignedBlockWithAttestation, Validators, +}; +use ssz::PersistentList; +use typenum::U4096; pub const DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT: usize = 1 << 12; // 4096 pub const TEST_VALIDATOR_COUNT: usize = 4; // Actual validator count used in tests @@ -15,30 +23,37 @@ pub const TEST_VALIDATOR_COUNT: usize = 4; // Actual validator count used in tes const _: [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT] = [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT]; -pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Option) -> SignedBlockWithAttestation { +pub fn create_block( + slot: u64, + parent_header: &mut BlockHeader, + attestations: Option, +) -> SignedBlockWithAttestation { let body = BlockBody { attestations: { let attestations_vec = attestations.unwrap_or_default(); - + // Convert PersistentList into a Vec - let attestations_vec: Vec = attestations_vec.into_iter().cloned().collect(); + let attestations_vec: Vec = + attestations_vec.into_iter().cloned().collect(); let aggregated: Vec = AggregatedAttestation::aggregate_by_data(&attestations_vec); // Create a new empty PersistentList - let mut persistent_list: PersistentList = PersistentList::default(); + let mut persistent_list: PersistentList = + PersistentList::default(); // Push each aggregated attestation for agg in aggregated { - persistent_list.push(agg).expect("PersistentList capacity exceeded"); + persistent_list + .push(agg) + .expect("PersistentList capacity exceeded"); } persistent_list }, }; - let block_message = Block { slot: Slot(slot), proposer_index: ValidatorIndex(slot % 10), @@ -55,11 +70,10 @@ pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Op signature: BlockSignatures { attestation_signatures: PersistentList::default(), proposer_signature: Signature::default(), - } + }, }; - - return_value + return_value } pub fn create_attestations(indices: &[usize]) -> Vec { @@ -94,8 +108,11 @@ pub fn base_state(config: Config) -> State { } pub fn base_state_with_validators(config: Config, num_validators: usize) -> State { - use containers::{HistoricalBlockHashes, JustificationRoots, JustifiedSlots, JustificationsValidators, validator::Validator, Uint64}; - + use containers::{ + validator::Validator, HistoricalBlockHashes, JustificationRoots, JustificationsValidators, + JustifiedSlots, Uint64, + }; + // Create validators list with the specified number of validators let mut validators = Validators::default(); for i in 0..num_validators { @@ -105,7 +122,7 @@ pub fn base_state_with_validators(config: Config, num_validators: usize) -> Stat }; validators.push(validator).expect("within limit"); } - + State { config, slot: Slot(0), @@ -121,7 +138,5 @@ pub fn base_state_with_validators(config: Config, num_validators: usize) -> Stat } pub fn sample_config() -> Config { - Config { - genesis_time: 0, - } -} \ No newline at end of file + Config { genesis_time: 0 } +} diff --git a/lean_client/containers/tests/unit_tests/mod.rs b/lean_client/containers/tests/unit_tests/mod.rs index cb5fc61..315792d 100644 --- a/lean_client/containers/tests/unit_tests/mod.rs +++ b/lean_client/containers/tests/unit_tests/mod.rs @@ -1,9 +1,9 @@ // tests/unit_tests/mod.rs // Modules that work with both devnet1 and devnet2 +mod attestation_aggregation; mod state_basic; mod state_justifications; -mod attestation_aggregation; // TODO: Update these modules for devnet2 data structures // (SignedAttestation now uses AttestationData directly, BlockSignatures changed, etc.) diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index 6f89527..e07aaa9 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -1,16 +1,16 @@ //! State process tests -//! +//! //! Tests for state processing functions including slot transitions, //! block header processing, and attestation processing. // tests/state_process.rs use containers::{ - block::{Block, BlockBody, hash_tree_root}, + block::{hash_tree_root, Block, BlockBody}, checkpoint::Checkpoint, slot::Slot, state::State, types::{Bytes32, Uint64, ValidatorIndex}, - Attestation, AttestationData, AggregatedAttestation, + AggregatedAttestation, Attestation, AttestationData, }; use pretty_assertions::assert_eq; use rstest::{fixture, rstest}; @@ -31,15 +31,24 @@ pub fn genesis_state() -> State { fn test_process_slot() { let genesis_state = genesis_state(); - assert_eq!(genesis_state.latest_block_header.state_root, Bytes32(ssz::H256::zero())); + assert_eq!( + genesis_state.latest_block_header.state_root, + Bytes32(ssz::H256::zero()) + ); let state_after_slot = genesis_state.process_slot(); let expected_root = hash_tree_root(&genesis_state); - assert_eq!(state_after_slot.latest_block_header.state_root, expected_root); + assert_eq!( + state_after_slot.latest_block_header.state_root, + expected_root + ); let state_after_second_slot = state_after_slot.process_slot(); - assert_eq!(state_after_second_slot.latest_block_header.state_root, expected_root); + assert_eq!( + state_after_second_slot.latest_block_header.state_root, + expected_root + ); } #[test] @@ -50,7 +59,10 @@ fn test_process_slots() { let new_state = genesis_state.process_slots(target_slot).unwrap(); assert_eq!(new_state.slot, target_slot); - assert_eq!(new_state.latest_block_header.state_root, hash_tree_root(&genesis_state)); + assert_eq!( + new_state.latest_block_header.state_root, + hash_tree_root(&genesis_state) + ); } #[test] @@ -73,11 +85,21 @@ fn test_process_block_header_valid() { assert_eq!(new_state.latest_finalized.root, genesis_header_root); assert_eq!(new_state.latest_justified.root, genesis_header_root); - assert_eq!(new_state.historical_block_hashes.get(0).ok(), Some(&genesis_header_root)); - let justified_slot_0 = new_state.justified_slots.get(0).map(|b| *b).unwrap_or(false); + assert_eq!( + new_state.historical_block_hashes.get(0).ok(), + Some(&genesis_header_root) + ); + let justified_slot_0 = new_state + .justified_slots + .get(0) + .map(|b| *b) + .unwrap_or(false); assert_eq!(justified_slot_0, true); assert_eq!(new_state.latest_block_header.slot, Slot(1)); - assert_eq!(new_state.latest_block_header.state_root, Bytes32(ssz::H256::zero())); + assert_eq!( + new_state.latest_block_header.state_root, + Bytes32(ssz::H256::zero()) + ); } #[rstest] @@ -100,7 +122,9 @@ fn test_process_block_header_invalid( proposer_index: ValidatorIndex(bad_proposer), parent_root: bad_parent_root.unwrap_or(parent_root), state_root: Bytes32(ssz::H256::zero()), - body: BlockBody { attestations: List::default() }, + body: BlockBody { + attestations: List::default(), + }, }; let result = state_at_slot_1.process_block_header(&block); @@ -119,13 +143,17 @@ fn test_process_attestations_justification_and_finalization() { let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); let block1 = create_block(1, &mut state_at_slot_1.latest_block_header, None); // Use process_block_header and process_operations separately to avoid state root validation - let state_after_header1 = state_at_slot_1.process_block_header(&block1.message.block).unwrap(); + let state_after_header1 = state_at_slot_1 + .process_block_header(&block1.message.block) + .unwrap(); state = state_after_header1.process_attestations(&block1.message.block.body.attestations); // Process slot 4 and block let mut state_at_slot_4 = state.process_slots(Slot(4)).unwrap(); let block4 = create_block(4, &mut state_at_slot_4.latest_block_header, None); - let state_after_header4 = state_at_slot_4.process_block_header(&block4.message.block).unwrap(); + let state_after_header4 = state_at_slot_4 + .process_block_header(&block4.message.block) + .unwrap(); state = state_after_header4.process_attestations(&block4.message.block.body.attestations); // Advance to slot 5 @@ -156,7 +184,7 @@ fn test_process_attestations_justification_and_finalization() { // Aggregate attestations for devnet2 format let aggregated = AggregatedAttestation::aggregate_by_data(&attestations_for_4); - + // Convert to AggregatedAttestations (PersistentList) let mut attestations_list: List = List::default(); for agg in aggregated { @@ -166,8 +194,14 @@ fn test_process_attestations_justification_and_finalization() { let new_state = state.process_attestations(&attestations_list); assert_eq!(new_state.latest_justified, checkpoint4); - let justified_slot_4 = new_state.justified_slots.get(4).map(|b| *b).unwrap_or(false); + let justified_slot_4 = new_state + .justified_slots + .get(4) + .map(|b| *b) + .unwrap_or(false); assert_eq!(justified_slot_4, true); assert_eq!(new_state.latest_finalized, genesis_checkpoint); - assert!(!new_state.get_justifications().contains_key(&checkpoint4.root)); -} \ No newline at end of file + assert!(!new_state + .get_justifications() + .contains_key(&checkpoint4.root)); +} diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index 451889f..bcf0ea1 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -5,7 +5,9 @@ // tests/state_transition.rs use containers::{ - block::{hash_tree_root, Block, BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, + block::{ + hash_tree_root, Block, BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation, + }, state::State, types::{Bytes32, Uint64}, Attestation, Signature, Slot, @@ -132,7 +134,7 @@ fn test_state_transition_devnet2() { let state_after_header = state_at_slot_1.process_block_header(&block).unwrap(); let expected_state = state_after_header.process_attestations(&block.body.attestations); - + // Ensure the state root matches the expected state let block_with_correct_root = Block { state_root: hash_tree_root(&expected_state), diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index d1935ff..b00b071 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -1,8 +1,8 @@ use crate::store::*; +use containers::SignatureKey; use containers::{ attestation::SignedAttestation, block::SignedBlockWithAttestation, Bytes32, ValidatorIndex, }; -use containers::SignatureKey; use ssz::SszHash; #[inline] @@ -53,9 +53,7 @@ pub fn on_attestation( if store .latest_known_attestations .get(&validator_id) - .map_or(true, |existing| { - existing.message.slot < attestation_slot - }) + .map_or(true, |existing| existing.message.slot < attestation_slot) { store .latest_known_attestations @@ -73,15 +71,15 @@ pub fn on_attestation( // Store signature for later aggregation during block building let data_root = signed_attestation.message.data_root_bytes(); let sig_key = SignatureKey::new(signed_attestation.validator_id, data_root); - store.gossip_signatures.insert(sig_key, signed_attestation.signature.clone()); - + store + .gossip_signatures + .insert(sig_key, signed_attestation.signature.clone()); + // Track attestation for fork choice if store .latest_new_attestations .get(&validator_id) - .map_or(true, |existing| { - existing.message.slot < attestation_slot - }) + .map_or(true, |existing| existing.message.slot < attestation_slot) { store .latest_new_attestations @@ -155,12 +153,12 @@ fn process_block_internal( let aggregated_attestations = &signed_block.message.block.body.attestations; let proposer_attestation = &signed_block.message.proposer_attestation; - + // Store aggregated proofs for future block building // Each attestation_signature proof is indexed by (validator_id, data_root) for each participating validator for (att_idx, aggregated_attestation) in aggregated_attestations.into_iter().enumerate() { let data_root = aggregated_attestation.data.data_root_bytes(); - + // Get the corresponding proof from attestation_signatures if let Ok(proof_data) = signatures.attestation_signatures.get(att_idx as u64) { // Create AggregatedSignatureProof with participants and proof data @@ -168,13 +166,14 @@ fn process_block_internal( aggregated_attestation.aggregation_bits.clone(), proof_data.clone(), ); - + // Store proof for each validator in the aggregation for (bit_idx, bit) in aggregated_attestation.aggregation_bits.0.iter().enumerate() { if *bit { let validator_id = bit_idx as u64; let sig_key = SignatureKey::new(validator_id, data_root); - store.aggregated_payloads + store + .aggregated_payloads .entry(sig_key) .or_insert_with(Vec::new) .push(aggregated_proof.clone()); @@ -187,7 +186,8 @@ fn process_block_internal( // Note: Signature verification is done in verify_signatures() before on_block() for aggregated_attestation in aggregated_attestations.into_iter() { let validator_ids: Vec = aggregated_attestation - .aggregation_bits.0 + .aggregation_bits + .0 .iter() .enumerate() .filter(|(_, bit)| **bit) diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 010dbb0..62f6b17 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -288,12 +288,21 @@ pub fn produce_block_with_signatures( store: &mut Store, slot: Slot, validator_index: ValidatorIndex, -) -> Result<(Root, containers::block::Block, Vec), String> { +) -> Result< + ( + Root, + containers::block::Block, + Vec, + ), + String, +> { use containers::Attestation; // Get parent block head let head_root = get_proposal_head(store, slot); - let head_state = store.states.get(&head_root) + let head_state = store + .states + .get(&head_root) .ok_or_else(|| "Head state not found".to_string())? .clone(); @@ -308,30 +317,31 @@ pub fn produce_block_with_signatures( } // Convert AttestationData to Attestation objects for build_block - let available_attestations: Vec = store.latest_known_attestations + let available_attestations: Vec = store + .latest_known_attestations .iter() - .map(|(validator_idx, signed_att)| { - Attestation { - validator_id: containers::Uint64(validator_idx.0), - data: signed_att.message.clone(), - } + .map(|(validator_idx, signed_att)| Attestation { + validator_id: containers::Uint64(validator_idx.0), + data: signed_att.message.clone(), }) .collect(); // Get known block roots for attestation validation - let known_block_roots: std::collections::HashSet = store.blocks.keys().copied().collect(); + let known_block_roots: std::collections::HashSet = + store.blocks.keys().copied().collect(); // Build block with fixed-point attestation collection and signature aggregation - let (final_block, final_post_state, _aggregated_attestations, signatures) = head_state.build_block( - slot, - validator_index, - head_root, - None, // initial_attestations - start with empty, let fixed-point collect - Some(available_attestations), - Some(&known_block_roots), - Some(&store.gossip_signatures), - Some(&store.aggregated_payloads), - )?; + let (final_block, final_post_state, _aggregated_attestations, signatures) = head_state + .build_block( + slot, + validator_index, + head_root, + None, // initial_attestations - start with empty, let fixed-point collect + Some(available_attestations), + Some(&known_block_roots), + Some(&store.gossip_signatures), + Some(&store.aggregated_payloads), + )?; // Compute block root let block_root = Bytes32(final_block.hash_tree_root()); diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index 71e517b..6ac6438 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -12,7 +12,7 @@ use containers::{ validator::Validator, Bytes32, Slot, Uint64, ValidatorIndex, }; -use fork_choice::store::{get_forkchoice_store, get_fork_choice_head, Store}; +use fork_choice::store::{get_fork_choice_head, get_forkchoice_store, Store}; use ssz::SszHash; use std::collections::HashMap; @@ -21,7 +21,7 @@ fn create_genesis_store() -> Store { let config = Config { genesis_time: 0 }; let validators = vec![Validator::default(); 10]; let state = State::generate_genesis_with_validators(Uint64(0), validators); - + let block = Block { slot: Slot(0), proposer_index: ValidatorIndex(0), @@ -29,7 +29,7 @@ fn create_genesis_store() -> Store { state_root: Bytes32(state.hash_tree_root()), body: BlockBody::default(), }; - + let signed_block = SignedBlockWithAttestation { message: BlockWithAttestation { block, @@ -37,7 +37,7 @@ fn create_genesis_store() -> Store { }, signature: Default::default(), }; - + get_forkchoice_store(state, signed_block, config) } @@ -71,27 +71,30 @@ fn add_block(store: &mut Store, slot: u64, parent_root: Bytes32, proposer: u64) body: BlockBody::default(), }; let block_root = Bytes32(block.hash_tree_root()); - - store.blocks.insert(block_root, SignedBlockWithAttestation { - message: BlockWithAttestation { - block, - proposer_attestation: Default::default(), + + store.blocks.insert( + block_root, + SignedBlockWithAttestation { + message: BlockWithAttestation { + block, + proposer_attestation: Default::default(), + }, + signature: Default::default(), }, - signature: Default::default(), - }); - + ); + block_root } #[test] fn test_genesis_state_transition() { let store = create_genesis_store(); - + // Verify genesis state is properly initialized assert!(!store.head.0.is_zero()); assert_eq!(store.blocks.len(), 1); assert_eq!(store.states.len(), 1); - + // Genesis should be both justified and finalized assert_eq!(store.latest_justified.slot, Slot(0)); assert_eq!(store.latest_finalized.slot, Slot(0)); @@ -101,32 +104,45 @@ fn test_genesis_state_transition() { fn test_basic_slot_transition() { let mut store = create_genesis_store(); let genesis_root = store.head; - + // Add blocks at slots 1, 2, 3 let block1_root = add_block(&mut store, 1, genesis_root, 0); let block2_root = add_block(&mut store, 2, block1_root, 0); let block3_root = add_block(&mut store, 3, block2_root, 0); - + assert_eq!(store.blocks.len(), 4); - - // Without attestations and min_votes=1, head should stay at genesis + + // Without attestations and min_votes=1, head should stay at genesis // (no blocks have enough votes to be considered) let empty_attestations = HashMap::new(); let head = get_fork_choice_head(&store, genesis_root, &empty_attestations, 1); assert_eq!(head, genesis_root); - + // With attestation for block3 and min_votes=1, head should follow the voted chain let mut attestations = HashMap::new(); - let checkpoint = Checkpoint { root: block3_root, slot: Slot(3) }; - let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; - - attestations.insert(ValidatorIndex(0), create_attestation( - 0, 3, checkpoint.clone(), checkpoint.clone(), genesis_checkpoint.clone() - )); - + let checkpoint = Checkpoint { + root: block3_root, + slot: Slot(3), + }; + let genesis_checkpoint = Checkpoint { + root: genesis_root, + slot: Slot(0), + }; + + attestations.insert( + ValidatorIndex(0), + create_attestation( + 0, + 3, + checkpoint.clone(), + checkpoint.clone(), + genesis_checkpoint.clone(), + ), + ); + // The fork choice should follow the chain with votes to find the heaviest head let head = get_fork_choice_head(&store, genesis_root, &attestations, 1); - + // With 1 vote on block3, the entire chain block1->block2->block3 gets 1 vote each // So head should be block3 (the tip of the voted chain) assert_eq!(head, block3_root); @@ -136,20 +152,33 @@ fn test_basic_slot_transition() { fn test_attestation_processing() { let mut store = create_genesis_store(); let genesis_root = store.head; - let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; - + let genesis_checkpoint = Checkpoint { + root: genesis_root, + slot: Slot(0), + }; + // Create a block let block1_root = add_block(&mut store, 1, genesis_root, 0); - let block1_checkpoint = Checkpoint { root: block1_root, slot: Slot(1) }; - + let block1_checkpoint = Checkpoint { + root: block1_root, + slot: Slot(1), + }; + // Process attestations from multiple validators let mut attestations = HashMap::new(); for i in 0..5 { - attestations.insert(ValidatorIndex(i), create_attestation( - i, 1, block1_checkpoint.clone(), block1_checkpoint.clone(), genesis_checkpoint.clone() - )); + attestations.insert( + ValidatorIndex(i), + create_attestation( + i, + 1, + block1_checkpoint.clone(), + block1_checkpoint.clone(), + genesis_checkpoint.clone(), + ), + ); } - + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); assert_eq!(head, block1_root); } @@ -158,23 +187,36 @@ fn test_attestation_processing() { fn test_multiple_attestations() { let mut store = create_genesis_store(); let genesis_root = store.head; - let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; - + let genesis_checkpoint = Checkpoint { + root: genesis_root, + slot: Slot(0), + }; + // Create a chain of blocks let block1_root = add_block(&mut store, 1, genesis_root, 0); let block2_root = add_block(&mut store, 2, block1_root, 0); let block3_root = add_block(&mut store, 3, block2_root, 0); - - let block3_checkpoint = Checkpoint { root: block3_root, slot: Slot(3) }; - + + let block3_checkpoint = Checkpoint { + root: block3_root, + slot: Slot(3), + }; + // All validators attest to block3 let mut attestations = HashMap::new(); for i in 0..10 { - attestations.insert(ValidatorIndex(i), create_attestation( - i, 3, block3_checkpoint.clone(), block3_checkpoint.clone(), genesis_checkpoint.clone() - )); + attestations.insert( + ValidatorIndex(i), + create_attestation( + i, + 3, + block3_checkpoint.clone(), + block3_checkpoint.clone(), + genesis_checkpoint.clone(), + ), + ); } - + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); assert_eq!(head, block3_root); } @@ -183,32 +225,55 @@ fn test_multiple_attestations() { fn test_fork_choice_with_competing_blocks() { let mut store = create_genesis_store(); let genesis_root = store.head; - let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; - + let genesis_checkpoint = Checkpoint { + root: genesis_root, + slot: Slot(0), + }; + // Create two competing forks at slot 1 let fork_a_root = add_block(&mut store, 1, genesis_root, 0); let fork_b_root = add_block(&mut store, 1, genesis_root, 1); // Different proposer - - let fork_a_checkpoint = Checkpoint { root: fork_a_root, slot: Slot(1) }; - let fork_b_checkpoint = Checkpoint { root: fork_b_root, slot: Slot(1) }; - + + let fork_a_checkpoint = Checkpoint { + root: fork_a_root, + slot: Slot(1), + }; + let fork_b_checkpoint = Checkpoint { + root: fork_b_root, + slot: Slot(1), + }; + // 6 validators vote for fork A let mut attestations = HashMap::new(); for i in 0..6 { - attestations.insert(ValidatorIndex(i), create_attestation( - i, 1, fork_a_checkpoint.clone(), fork_a_checkpoint.clone(), genesis_checkpoint.clone() - )); + attestations.insert( + ValidatorIndex(i), + create_attestation( + i, + 1, + fork_a_checkpoint.clone(), + fork_a_checkpoint.clone(), + genesis_checkpoint.clone(), + ), + ); } - + // 4 validators vote for fork B for i in 6..10 { - attestations.insert(ValidatorIndex(i), create_attestation( - i, 1, fork_b_checkpoint.clone(), fork_b_checkpoint.clone(), genesis_checkpoint.clone() - )); + attestations.insert( + ValidatorIndex(i), + create_attestation( + i, + 1, + fork_b_checkpoint.clone(), + fork_b_checkpoint.clone(), + genesis_checkpoint.clone(), + ), + ); } - + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); - + // Fork A should win with more votes assert_eq!(head, fork_a_root); } @@ -217,37 +282,63 @@ fn test_fork_choice_with_competing_blocks() { fn test_finality_prevents_reorg() { let mut store = create_genesis_store(); let genesis_root = store.head; - let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; - + let genesis_checkpoint = Checkpoint { + root: genesis_root, + slot: Slot(0), + }; + // Create a finalized chain let block1_root = add_block(&mut store, 1, genesis_root, 0); let block2_root = add_block(&mut store, 2, block1_root, 0); - + // Update finalized checkpoint - store.latest_finalized = Checkpoint { root: block1_root, slot: Slot(1) }; - + store.latest_finalized = Checkpoint { + root: block1_root, + slot: Slot(1), + }; + // Create competing fork from genesis (should not be chosen due to finality) let competing_root = add_block(&mut store, 1, genesis_root, 1); - - let block2_checkpoint = Checkpoint { root: block2_root, slot: Slot(2) }; - let competing_checkpoint = Checkpoint { root: competing_root, slot: Slot(1) }; - + + let block2_checkpoint = Checkpoint { + root: block2_root, + slot: Slot(2), + }; + let competing_checkpoint = Checkpoint { + root: competing_root, + slot: Slot(1), + }; + // More votes for competing fork let mut attestations = HashMap::new(); for i in 0..7 { - attestations.insert(ValidatorIndex(i), create_attestation( - i, 1, competing_checkpoint.clone(), competing_checkpoint.clone(), genesis_checkpoint.clone() - )); + attestations.insert( + ValidatorIndex(i), + create_attestation( + i, + 1, + competing_checkpoint.clone(), + competing_checkpoint.clone(), + genesis_checkpoint.clone(), + ), + ); } for i in 7..10 { - attestations.insert(ValidatorIndex(i), create_attestation( - i, 2, block2_checkpoint.clone(), block2_checkpoint.clone(), genesis_checkpoint.clone() - )); + attestations.insert( + ValidatorIndex(i), + create_attestation( + i, + 2, + block2_checkpoint.clone(), + block2_checkpoint.clone(), + genesis_checkpoint.clone(), + ), + ); } - + // Start from finalized block1 let head = get_fork_choice_head(&store, block1_root, &attestations, 0); - + // Should follow the chain from block1, not competing fork assert_eq!(head, block2_root); } @@ -256,21 +347,34 @@ fn test_finality_prevents_reorg() { fn test_attestation_from_future_slot() { let mut store = create_genesis_store(); let genesis_root = store.head; - let genesis_checkpoint = Checkpoint { root: genesis_root, slot: Slot(0) }; - + let genesis_checkpoint = Checkpoint { + root: genesis_root, + slot: Slot(0), + }; + // Create block at slot 1 let block1_root = add_block(&mut store, 1, genesis_root, 0); - let block1_checkpoint = Checkpoint { root: block1_root, slot: Slot(1) }; - + let block1_checkpoint = Checkpoint { + root: block1_root, + slot: Slot(1), + }; + // Attestation claims to be from slot 100 (future) // The fork choice still processes it based on what block it points to let mut attestations = HashMap::new(); - attestations.insert(ValidatorIndex(0), create_attestation( - 0, 100, block1_checkpoint.clone(), block1_checkpoint.clone(), genesis_checkpoint.clone() - )); - + attestations.insert( + ValidatorIndex(0), + create_attestation( + 0, + 100, + block1_checkpoint.clone(), + block1_checkpoint.clone(), + genesis_checkpoint.clone(), + ), + ); + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); - + // Should still follow the attestation to block1 assert_eq!(head, block1_root); } @@ -279,10 +383,10 @@ fn test_attestation_from_future_slot() { fn test_empty_attestations_returns_root() { let store = create_genesis_store(); let genesis_root = store.head; - + let empty_attestations = HashMap::new(); let head = get_fork_choice_head(&store, genesis_root, &empty_attestations, 0); - + // With no attestations, should return the provided root assert_eq!(head, genesis_root); } diff --git a/lean_client/fork_choice/tests/unit_tests/votes.rs b/lean_client/fork_choice/tests/unit_tests/votes.rs index fd579f0..a3c9b8a 100644 --- a/lean_client/fork_choice/tests/unit_tests/votes.rs +++ b/lean_client/fork_choice/tests/unit_tests/votes.rs @@ -50,24 +50,24 @@ fn create_signed_attestation( fn test_single_vote_updates_head() { let store = create_test_store(); let genesis_root = store.head; - + // Create attestation pointing to genesis let attestation = create_signed_attestation( - 0, // validator_id - 1, // slot - genesis_root, // head_root - 0, // head_slot - genesis_root, // target_root - 0, // target_slot - genesis_root, // source_root - 0, // source_slot + 0, // validator_id + 1, // slot + genesis_root, // head_root + 0, // head_slot + genesis_root, // target_root + 0, // target_slot + genesis_root, // source_root + 0, // source_slot ); - + let mut attestations = HashMap::new(); attestations.insert(ValidatorIndex(0), attestation); - + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); - + // With only one block, head should still be genesis assert_eq!(head, genesis_root); } @@ -76,25 +76,17 @@ fn test_single_vote_updates_head() { fn test_multiple_votes_same_block() { let store = create_test_store(); let genesis_root = store.head; - + // Multiple validators vote for same block let mut attestations = HashMap::new(); for i in 0..5 { - let attestation = create_signed_attestation( - i, - 1, - genesis_root, - 0, - genesis_root, - 0, - genesis_root, - 0, - ); + let attestation = + create_signed_attestation(i, 1, genesis_root, 0, genesis_root, 0, genesis_root, 0); attestations.insert(ValidatorIndex(i), attestation); } - + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); - + // All votes on same block, head unchanged assert_eq!(head, genesis_root); } @@ -103,7 +95,7 @@ fn test_multiple_votes_same_block() { fn test_competing_votes_different_blocks() { let mut store = create_test_store(); let genesis_root = store.head; - + // Create two competing blocks at slot 1 let block_a = Block { slot: Slot(1), @@ -113,42 +105,50 @@ fn test_competing_votes_different_blocks() { body: BlockBody::default(), }; let block_a_root = Bytes32(block_a.hash_tree_root()); - + let mut block_b = block_a.clone(); block_b.proposer_index = ValidatorIndex(1); // Different proposer to get different root let block_b_root = Bytes32(block_b.hash_tree_root()); - - store.blocks.insert(block_a_root, SignedBlockWithAttestation { - message: BlockWithAttestation { - block: block_a, - proposer_attestation: Default::default(), + + store.blocks.insert( + block_a_root, + SignedBlockWithAttestation { + message: BlockWithAttestation { + block: block_a, + proposer_attestation: Default::default(), + }, + signature: Default::default(), }, - signature: Default::default(), - }); - - store.blocks.insert(block_b_root, SignedBlockWithAttestation { - message: BlockWithAttestation { - block: block_b, - proposer_attestation: Default::default(), + ); + + store.blocks.insert( + block_b_root, + SignedBlockWithAttestation { + message: BlockWithAttestation { + block: block_b, + proposer_attestation: Default::default(), + }, + signature: Default::default(), }, - signature: Default::default(), - }); - + ); + // 3 votes for block_a, 2 votes for block_b let mut attestations = HashMap::new(); for i in 0..3 { - attestations.insert(ValidatorIndex(i), create_signed_attestation( - i, 1, block_a_root, 1, genesis_root, 0, genesis_root, 0, - )); + attestations.insert( + ValidatorIndex(i), + create_signed_attestation(i, 1, block_a_root, 1, genesis_root, 0, genesis_root, 0), + ); } for i in 3..5 { - attestations.insert(ValidatorIndex(i), create_signed_attestation( - i, 1, block_b_root, 1, genesis_root, 0, genesis_root, 0, - )); + attestations.insert( + ValidatorIndex(i), + create_signed_attestation(i, 1, block_b_root, 1, genesis_root, 0, genesis_root, 0), + ); } - + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); - + // Block A should win with more votes assert_eq!(head, block_a_root); } @@ -157,7 +157,7 @@ fn test_competing_votes_different_blocks() { fn test_vote_weight_accumulation() { let mut store = create_test_store(); let genesis_root = store.head; - + // Create a chain: genesis -> block1 -> block2 let block1 = Block { slot: Slot(1), @@ -167,7 +167,7 @@ fn test_vote_weight_accumulation() { body: BlockBody::default(), }; let block1_root = Bytes32(block1.hash_tree_root()); - + let block2 = Block { slot: Slot(2), proposer_index: ValidatorIndex(0), @@ -176,24 +176,37 @@ fn test_vote_weight_accumulation() { body: BlockBody::default(), }; let block2_root = Bytes32(block2.hash_tree_root()); - - store.blocks.insert(block1_root, SignedBlockWithAttestation { - message: BlockWithAttestation { block: block1, proposer_attestation: Default::default() }, - signature: Default::default(), - }); - store.blocks.insert(block2_root, SignedBlockWithAttestation { - message: BlockWithAttestation { block: block2, proposer_attestation: Default::default() }, - signature: Default::default(), - }); - + + store.blocks.insert( + block1_root, + SignedBlockWithAttestation { + message: BlockWithAttestation { + block: block1, + proposer_attestation: Default::default(), + }, + signature: Default::default(), + }, + ); + store.blocks.insert( + block2_root, + SignedBlockWithAttestation { + message: BlockWithAttestation { + block: block2, + proposer_attestation: Default::default(), + }, + signature: Default::default(), + }, + ); + // Vote for block2 - should accumulate to block1 as well let mut attestations = HashMap::new(); - attestations.insert(ValidatorIndex(0), create_signed_attestation( - 0, 2, block2_root, 2, genesis_root, 0, genesis_root, 0, - )); - + attestations.insert( + ValidatorIndex(0), + create_signed_attestation(0, 2, block2_root, 2, genesis_root, 0, genesis_root, 0), + ); + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); - + // Head should be block2 (the one with votes) assert_eq!(head, block2_root); } @@ -202,23 +215,25 @@ fn test_vote_weight_accumulation() { fn test_duplicate_vote_uses_latest() { let store = create_test_store(); let genesis_root = store.head; - + // Same validator can only have one vote in the map (latest wins) let mut attestations = HashMap::new(); - + // Insert a vote - attestations.insert(ValidatorIndex(0), create_signed_attestation( - 0, 1, genesis_root, 0, genesis_root, 0, genesis_root, 0, - )); - + attestations.insert( + ValidatorIndex(0), + create_signed_attestation(0, 1, genesis_root, 0, genesis_root, 0, genesis_root, 0), + ); + // "Update" with same validator - only latest is kept - attestations.insert(ValidatorIndex(0), create_signed_attestation( - 0, 2, genesis_root, 0, genesis_root, 0, genesis_root, 0, - )); - + attestations.insert( + ValidatorIndex(0), + create_signed_attestation(0, 2, genesis_root, 0, genesis_root, 0, genesis_root, 0), + ); + // Should only have 1 attestation assert_eq!(attestations.len(), 1); - + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); assert_eq!(head, genesis_root); } @@ -228,15 +243,16 @@ fn test_vote_for_unknown_block_ignored() { let store = create_test_store(); let genesis_root = store.head; let unknown_root = Bytes32(ssz::H256::from_slice(&[0xff; 32])); - + // Vote for block that doesn't exist let mut attestations = HashMap::new(); - attestations.insert(ValidatorIndex(0), create_signed_attestation( - 0, 1, unknown_root, 1, genesis_root, 0, genesis_root, 0, - )); - + attestations.insert( + ValidatorIndex(0), + create_signed_attestation(0, 1, unknown_root, 1, genesis_root, 0, genesis_root, 0), + ); + let head = get_fork_choice_head(&store, genesis_root, &attestations, 0); - + // Should still return genesis since unknown block is skipped assert_eq!(head, genesis_root); } diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index 7559bf9..e1dfd2d 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -232,7 +232,7 @@ impl ValidatorService { // Sign the proposer attestation let proposer_signature: Signature; - + if let Some(ref key_manager) = self.key_manager { // Sign proposer attestation with XMSS let message = hash_tree_root(&proposer_attestation); From 43a95e7a2bf5ecfdb8e3251991f68e1a285ad9a8 Mon Sep 17 00:00:00 2001 From: Julius Mieliauskas Date: Thu, 22 Jan 2026 22:04:24 +0200 Subject: [PATCH 23/24] - added wrapper classes for public key and signature - added test vectors with new types - added custom serialization with new types - implemented signature verification for single XMSS signatures --- lean_client/Cargo.toml | 10 +- lean_client/containers/Cargo.toml | 12 +- lean_client/containers/src/attestation.rs | 6 +- lean_client/containers/src/block.rs | 65 +- lean_client/containers/src/lib.rs | 2 + lean_client/containers/src/public_key.rs | 207 +++ lean_client/containers/src/serde_helpers.rs | 217 ++- lean_client/containers/src/signature.rs | 121 ++ lean_client/containers/src/state.rs | 2 +- lean_client/containers/src/validator.rs | 88 +- .../containers/tests/test_vectors/runner.rs | 24 +- .../tests/test_vectors/verify_signatures.rs | 34 +- lean_client/fork_choice/Cargo.toml | 9 +- lean_client/fork_choice/src/handlers.rs | 8 +- lean_client/src/main.rs | 4 +- .../test_invalid_signature.json | 58 +- .../test_mixed_valid_invalid_signatures.json | 491 ----- ...test_proposer_and_attester_signatures.json | 1635 ++++++++++++----- .../test_proposer_signature.json | 1326 +++++++++++-- lean_client/validator/Cargo.toml | 1 - lean_client/validator/src/lib.rs | 2 +- 21 files changed, 2948 insertions(+), 1374 deletions(-) create mode 100644 lean_client/containers/src/public_key.rs create mode 100644 lean_client/containers/src/signature.rs delete mode 100644 lean_client/tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_mixed_valid_invalid_signatures.json diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index 492d348..6b5fbbf 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["chain", "containers", "env-config", "fork_choice", "networking", "validator"] +members = ["chain", "containers", "fork_choice", "networking", "validator"] resolver = "2" [workspace.package] @@ -14,7 +14,7 @@ containers = { path = "./containers" } fork_choice = { path = "./fork_choice" } networking = { path = "./networking" } validator = { path = "./validator" } -libp2p = { version = "0.56.0", default-features = false, features = [ +libp2p = {version = "0.56.0", default-features = false, features = [ 'dns', 'gossipsub', 'identify', @@ -34,12 +34,13 @@ serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.9" snap = "1.1" ssz = { git = "https://github.com/grandinetech/grandine", package = "ssz", branch = "develop" } -ssz-derive = { git = "https://github.com/grandinetech/grandine", package = "ssz_derive", branch = "develop" } +ssz_derive = { git = "https://github.com/grandinetech/grandine", package = "ssz_derive", branch = "develop" } ssz-types = "0.3.0" tokio = { version = "1.0", features = ["full"] } tree-hash = "0.4.0" typenum = "1.19" sha2 = "0.10" +rand = "0.9" [workspace.dev-dependencies] rstest = "0.18.2" @@ -52,8 +53,9 @@ version = "0.1.0" edition = "2021" [features] -default = ["xmss-signing"] +default = ["xmss-signing", "containers/xmss-verify"] xmss-signing = ["validator/xmss-signing"] +xmss-verify = ["containers/xmss-verify"] [dependencies] chain = { path = "./chain" } diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index 0a59a33..0927f7e 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [features] -xmss-verify = ["leansig"] +xmss-verify = [] default = [] [lib] @@ -13,16 +13,18 @@ path = "src/lib.rs" [dependencies] env-config = { path = "../env-config", default-features = false } -ssz = { git = "https://github.com/grandinetech/grandine", package = "ssz", branch = "develop" } -ssz_derive = { git = "https://github.com/grandinetech/grandine", package = "ssz_derive", branch = "develop" } +ssz = { workspace = true } +serde = { workspace = true } +ssz_derive = { workspace = true } typenum = "1" -serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" hex = "0.4.3" sha2 = "0.10" -leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "main", optional = true } +leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "main" } lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig", branch = "main" } +anyhow = "1.0.100" +alloy-primitives = "1.5.2" [dev-dependencies] rstest = "0.18" diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 742bc6c..eebffce 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -25,7 +25,7 @@ pub type Attestations = ssz::PersistentList; pub type AggregatedAttestations = ssz::PersistentList; -pub type AttestationSignatures = ssz::PersistentList; +pub type AttestationSignatures = ssz::PersistentList; /// Legacy naive aggregated signature type (list of individual XMSS signatures). /// Kept for backwards compatibility but no longer used in wire format. @@ -164,6 +164,7 @@ pub enum AggregationError { /// matches Python's `AggregatedSignatureProof` container structure. /// Used in `aggregated_payloads` to track which validators are covered by each proof. #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct AggregatedSignatureProof { /// Bitfield indicating which validators' signatures are included. pub participants: AggregationBits, @@ -196,7 +197,7 @@ impl AggregatedSignatureProof { /// Bitlist representing validator participation in an attestation. /// Limit is VALIDATOR_REGISTRY_LIMIT (4096). #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] -pub struct AggregationBits(pub BitList); +pub struct AggregationBits(#[serde(with = "crate::serde_helpers::bitlist")] pub BitList); impl AggregationBits { pub const LIMIT: u64 = 4096; @@ -308,6 +309,7 @@ pub struct SignedAttestation { /// Aggregated attestation consisting of participation bits and message. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct AggregatedAttestation { /// Bitfield indicating which validators participated in the aggregation. pub aggregation_bits: AggregationBits, diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index b49a11b..52d6d59 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -1,9 +1,9 @@ -use crate::{Attestation, Bytes32, Signature, Slot, State, ValidatorIndex}; +use crate::{ + Attestation, Bytes32, MultisigAggregatedSignature, Signature, Slot, State, ValidatorIndex, +}; use serde::{Deserialize, Serialize}; use ssz_derive::Ssz; -#[cfg(feature = "xmss-verify")] -use leansig::signature::generalized_xmss::instantiations_poseidon::lifetime_2_to_the_20::target_sum::SIGTargetSumLifetime20W2NoOff; use crate::attestation::{AggregatedAttestations, AttestationSignatures}; /// The body of a block, containing payload data. @@ -12,6 +12,7 @@ use crate::attestation::{AggregatedAttestations, AttestationSignatures}; /// separately in BlockSignatures to match the spec architecture. #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct BlockBody { + #[serde(with = "crate::serde_helpers::aggregated_attestations")] pub attestations: AggregatedAttestations, } @@ -46,8 +47,11 @@ pub struct BlockWithAttestation { } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Ssz, Deserialize, Default)] +#[serde(rename_all = "camelCase")] pub struct BlockSignatures { + #[serde(with = "crate::serde_helpers::attestation_signatures")] pub attestation_signatures: AttestationSignatures, + #[serde(with = "crate::serde_helpers::signature")] pub proposer_signature: Signature, } @@ -138,7 +142,7 @@ impl SignedBlockWithAttestation { let num_validators = validators.len_u64(); // Verify each aggregated attestation's zkVM proof - for (aggregated_attestation, aggregated_signature) in (&aggregated_attestations) + for (aggregated_attestation, _aggregated_signature_proof) in (&aggregated_attestations) .into_iter() .zip((&attestation_signatures).into_iter()) { @@ -154,23 +158,24 @@ impl SignedBlockWithAttestation { ); } - let attestation_data_root: [u8; 32] = - hash_tree_root(&aggregated_attestation.data).0.into(); + // let attestation_data_root: [u8; 32] = + // hash_tree_root(&aggregated_attestation.data).0.into(); // Verify the lean-multisig aggregated proof for this attestation // // The proof verifies that all validators in aggregation_bits signed // the same attestation_data_root at the given epoch (slot). - aggregated_signature - .verify_aggregated_payload( - &validator_ids - .iter() - .map(|vid| validators.get(*vid).expect("validator must exist")) - .collect::>(), - &attestation_data_root, - aggregated_attestation.data.slot.0, - ) - .expect("Attestation aggregated signature verification failed"); + // TODO + // aggregated_signature_proof + // .verify_aggregated_payload( + // &validator_ids + // .iter() + // .map(|vid| validators.get(*vid).expect("validator must exist")) + // .collect::>(), + // &attestation_data_root, + // aggregated_attestation.data.slot.0, + // ) + // .expect("Attestation aggregated signature verification failed"); } // Verify the proposer attestation signature (outside the attestation loop) @@ -186,10 +191,10 @@ impl SignedBlockWithAttestation { .get(proposer_attestation.validator_id.0) .expect("proposer must exist"); - let proposer_root: [u8; 32] = hash_tree_root(proposer_attestation).0.into(); + let proposer_root: [u8; 32] = hash_tree_root(&proposer_attestation.data).0.into(); assert!( verify_xmss_signature( - proposer.pubkey.0.as_bytes(), + proposer.pubkey, proposer_attestation.data.slot, &proposer_root, proposer_signature, @@ -203,34 +208,22 @@ impl SignedBlockWithAttestation { #[cfg(feature = "xmss-verify")] pub fn verify_xmss_signature( - pubkey_bytes: &[u8], + public_key: crate::public_key::PublicKey, slot: Slot, message_bytes: &[u8; 32], signature: &Signature, ) -> bool { - use leansig::serialization::Serializable; - use leansig::signature::SignatureScheme; - let epoch = slot.0 as u32; + let signature = crate::signature::Signature::from(signature.as_bytes()); - type PubKey = ::PublicKey; - let pubkey = match PubKey::from_bytes(pubkey_bytes) { - Ok(pk) => pk, - Err(_) => return false, - }; - - type Sig = ::Signature; - let sig = match Sig::from_bytes(signature.as_bytes()) { - Ok(s) => s, - Err(_) => return false, - }; - - SIGTargetSumLifetime20W2NoOff::verify(&pubkey, epoch, message_bytes, &sig) + signature + .verify(&public_key, epoch, message_bytes) + .unwrap_or_else(|_| false) } #[cfg(not(feature = "xmss-verify"))] pub fn verify_xmss_signature( - _pubkey_bytes: &[u8], + _public_key: crate::public_key::PublicKey, _slot: Slot, _message_bytes: &[u8; 32], _signature: &Signature, diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index 6f98fcc..0125a08 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -2,7 +2,9 @@ pub mod attestation; pub mod block; pub mod checkpoint; pub mod config; +pub mod public_key; pub mod serde_helpers; +pub mod signature; pub mod slot; pub mod state; pub mod status; diff --git a/lean_client/containers/src/public_key.rs b/lean_client/containers/src/public_key.rs new file mode 100644 index 0000000..114b17c --- /dev/null +++ b/lean_client/containers/src/public_key.rs @@ -0,0 +1,207 @@ +use alloy_primitives::{hex::{self, ToHexExt}}; +use anyhow::{anyhow}; +use leansig::{serialization::Serializable, signature::SignatureScheme}; +use leansig::signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8; +use serde::{Deserialize, Deserializer, Serialize}; +use ssz::{SszSize, SszRead, SszWrite, SszHash, Size, WriteError, ReadError, H256}; + +const PUBLIC_KEY_SIZE: usize = 52; +pub type LeanSigPublicKey = + ::PublicKey; + +// This is a wrapper class for storing public keys, implementation based on Ream client +#[derive(Debug, PartialEq, Clone, Eq, Hash, Copy)] +pub struct PublicKey { + pub inner: [u8; PUBLIC_KEY_SIZE], +} + +impl From<&[u8]> for PublicKey { + fn from(value: &[u8]) -> Self { + // Handle potential length panics or ensure slice is correct size + let mut inner = [0u8; PUBLIC_KEY_SIZE]; + let len = value.len().min(PUBLIC_KEY_SIZE); + inner[..len].copy_from_slice(&value[..len]); + Self { inner } + } +} + +impl Default for PublicKey { + fn default() -> Self { + Self { + inner: [0u8; PUBLIC_KEY_SIZE], + } + } +} + +impl SszSize for PublicKey { + const SIZE: Size = Size::Fixed { + size: PUBLIC_KEY_SIZE, + }; +} + +// 2. Define how to write (Serialize) +impl SszWrite for PublicKey { + fn write_fixed(&self, _bytes: &mut [u8]) { + panic!("SszWrite::write_fixed must be implemented for fixed-size types"); + } + + fn write_variable(&self, _bytes: &mut Vec) -> Result<(), WriteError> { + panic!("SszWrite::write_variable must be implemented for variable-size types"); + } + + fn to_ssz(&self) -> Result, WriteError> { + match Self::SIZE { + Size::Fixed { size } => { + let mut bytes = vec![0; size]; + self.write_fixed(bytes.as_mut_slice()); + Ok(bytes) + } + Size::Variable { minimum_size } => { + let mut bytes = Vec::with_capacity(minimum_size); + self.write_variable(&mut bytes)?; + Ok(bytes) + } + } + } +} + +impl SszRead for PublicKey { + fn from_ssz_unchecked(_context: &C, bytes: &[u8]) -> Result { + // For a fixed-size struct, we must ensure we have exactly + // the number of bytes required by our SszSize implementation. + if bytes.len() != PUBLIC_KEY_SIZE { + return Err(ReadError::FixedSizeMismatch { + expected: PUBLIC_KEY_SIZE, + actual: bytes.len(), + }); + } + + let mut inner = [0u8; PUBLIC_KEY_SIZE]; + inner.copy_from_slice(bytes); + + Ok(Self { inner }) + } + fn from_ssz(context: &C, bytes: impl AsRef<[u8]>) -> Result { + let bytes_ref = bytes.as_ref(); + + // SSZ fixed-size validation + if bytes_ref.len() != PUBLIC_KEY_SIZE { + return Err(ReadError::FixedSizeMismatch { + expected: PUBLIC_KEY_SIZE, + actual: bytes_ref.len(), + }); + } + + Self::from_ssz_unchecked(context, bytes_ref) + } +} + +impl SszHash for PublicKey { + type PackingFactor = typenum::U1; + + fn hash_tree_root(&self) -> H256 { + // Simple implementation: hash the inner bytes directly + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(&self.inner); + let result = hasher.finalize(); + H256::from_slice(&result) + } +} + +impl PublicKey { + pub fn new(inner: [u8; PUBLIC_KEY_SIZE]) -> Self { + Self { inner } + } + + pub fn from_lean_sig(public_key: LeanSigPublicKey) -> Result { + let bytes = public_key.to_bytes(); + // Ensure we fit into 52 bytes + if bytes.len() != PUBLIC_KEY_SIZE { + return Err(anyhow!( + "LeanSigPublicKey length mismatch: expected 52, got {}", + bytes.len() + )); + } + let mut inner = [0u8; PUBLIC_KEY_SIZE]; + inner.copy_from_slice(&bytes); + Ok(Self { inner }) + } + + pub fn as_lean_sig(&self) -> anyhow::Result { + LeanSigPublicKey::from_bytes(&self.inner) + .map_err(|err| anyhow!("Failed to decode LeanSigPublicKey from SSZ: {err:?}")) + } + + pub fn from_hex>(s: S) -> anyhow::Result { + let s = s.as_ref(); + + // Allow optional 0x prefix + let s = s.strip_prefix("0x").unwrap_or(s); + + let bytes = hex::decode(s).map_err(|e| anyhow!("Invalid hex public key: {e}"))?; + + if bytes.len() != 52 { + return Err(anyhow!( + "PublicKey hex length mismatch: expected 52 bytes, got {}", + bytes.len() + )); + } + + // Validate structure via LeanSig + let lean_pk = LeanSigPublicKey::from_bytes(&bytes) + .map_err(|e| anyhow!("Invalid XMSS public key encoding: {e:?}"))?; + + Self::from_lean_sig(lean_pk) + } + + pub fn debug_roundtrip(&self) -> anyhow::Result<()> { + let pk = self.as_lean_sig()?; + let re = pk.to_bytes(); + + anyhow::ensure!( + re.as_slice() == self.inner.as_slice(), + "PublicKey roundtrip mismatch: decoded->encoded bytes differ" + ); + + Ok(()) + } + + pub fn fingerprint_hex(&self) -> String { + use alloy_primitives::hex::ToHexExt; + let take = self.inner.len().min(12); + format!("0x{}", ToHexExt::encode_hex(&self.inner[..take].iter())) + } +} + +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!( + "0x{}", + self.as_lean_sig() + .map_err(serde::ser::Error::custom)? + .to_bytes() + .encode_hex() + )) + } +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let result: String = Deserialize::deserialize(deserializer)?; + let result = hex::decode(&result).map_err(serde::de::Error::custom)?; + + Self::from_lean_sig( + LeanSigPublicKey::from_bytes(&result) + .map_err(|err| anyhow!("Convert to error, with error trait implemented {err:?}")) + .map_err(serde::de::Error::custom)?, + ) + .map_err(serde::de::Error::custom) + } +} diff --git a/lean_client/containers/src/serde_helpers.rs b/lean_client/containers/src/serde_helpers.rs index 987b86b..3f3aa86 100644 --- a/lean_client/containers/src/serde_helpers.rs +++ b/lean_client/containers/src/serde_helpers.rs @@ -120,7 +120,7 @@ pub mod signature { siblings: DataWrapper>>>, } - pub fn deserialize_single<'de, D>(deserializer: D) -> Result + pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { @@ -130,7 +130,7 @@ pub mod signature { let value = Value::deserialize(deserializer)?; // Check if it's a hex string (normal format) - if let Value::String(hex_str) = &value { + if let Value::String(hex_str) = value { let hex_str = hex_str.trim_start_matches("0x"); let bytes = hex::decode(hex_str) .map_err(|e| D::Error::custom(format!("Invalid hex string: {}", e)))?; @@ -140,36 +140,96 @@ pub mod signature { } // Otherwise, parse as structured XMSS signature - let xmss_sig: XmssSignature = serde_json::from_value(value) + let xmss_sig: XmssSignature = serde_json::from_value(value.clone()) .map_err(|e| D::Error::custom(format!("Failed to parse XMSS signature: {}", e)))?; - // Serialize the XMSS signature to bytes - // Format: siblings (variable length) + rho (28 bytes) + hashes (variable length) - let mut bytes = Vec::new(); + println!( + "Parsed XMSS Signature | siblings: {:?}", + xmss_sig.path.siblings.data.len() + ); + println!("Parsed XMSS Signature | rho: {:?}", xmss_sig.rho.data.len()); + println!( + "Parsed XMSS Signature | hashes: {:?}", + xmss_sig.hashes.data.len() + ); + + // --- STEP 1: PREPARE DATA BUFFERS --- + + // 1. Serialize Rho (Fixed length) + // RAND_LEN_FE = 7, assuming u32 elements -> 28 bytes + let mut rho_bytes = Vec::new(); + for val in &xmss_sig.rho.data { + rho_bytes.extend_from_slice(&val.to_le_bytes()); + } + let rho_len = rho_bytes.len(); // Should be 28 (7 * 4) - // Write siblings + // 2. Serialize Path/Siblings (Variable length) + let mut path_bytes = Vec::new(); + // Prepend 4 bytes (containing 4) as an offset which would come with real SSZ serialization + let inner_offset: u32 = 4; + path_bytes.extend_from_slice(&inner_offset.to_le_bytes()); // [04 00 00 00] for sibling in &xmss_sig.path.siblings.data { for val in &sibling.data { - bytes.extend_from_slice(&val.to_le_bytes()); + path_bytes.extend_from_slice(&val.to_le_bytes()); } } - // Write rho (7 u32s = 28 bytes) - for val in &xmss_sig.rho.data { - bytes.extend_from_slice(&val.to_le_bytes()); - } - - // Write hashes + // 3. Serialize Hashes (Variable length) + let mut hashes_bytes = Vec::new(); for hash in &xmss_sig.hashes.data { for val in &hash.data { - bytes.extend_from_slice(&val.to_le_bytes()); + hashes_bytes.extend_from_slice(&val.to_le_bytes()); } } - // Pad or truncate to 3112 bytes - bytes.resize(3112, 0); + // --- STEP 2: CALCULATE OFFSETS --- + + // The fixed part contains: + // 1. Path Offset (4 bytes) + // 2. Rho Data (rho_len bytes) + // 3. Hashes Offset (4 bytes) + let fixed_part_size = 4 + rho_len + 4; + + // Offset to 'path' starts immediately after the fixed part + let offset_path = fixed_part_size as u32; + + // Offset to 'hashes' starts after 'path' data + let offset_hashes = offset_path + (path_bytes.len() as u32); + + // --- STEP 3: CONSTRUCT FINAL SSZ BYTES --- + + // Print all offsets and lengths for debugging + println!( + "SSZ Offsets | offset_path: {} | offset_hashes: {}", + offset_path, offset_hashes + ); + println!( + "SSZ Lengths | rho_len: {} | path_len: {} | hashes_len: {}", + rho_len, + path_bytes.len(), + hashes_bytes.len() + ); + + let mut ssz_bytes = Vec::new(); + + // 1. Write Offset to Path (u32, Little Endian) + ssz_bytes.extend_from_slice(&offset_path.to_le_bytes()); + + // 2. Write Rho Data (Fixed) + ssz_bytes.extend_from_slice(&rho_bytes); + + // 3. Write Offset to Hashes (u32, Little Endian) + ssz_bytes.extend_from_slice(&offset_hashes.to_le_bytes()); + + // 4. Write Path Data (Variable) + ssz_bytes.extend_from_slice(&path_bytes); + + // 5. Write Hashes Data (Variable) + ssz_bytes.extend_from_slice(&hashes_bytes); - Signature::try_from(bytes.as_slice()) + println!("Total SSZ Bytes Length: {}", ssz_bytes.len()); + + Signature::try_from(ssz_bytes.as_slice()) .map_err(|_| D::Error::custom("Failed to create signature")) } @@ -183,8 +243,53 @@ pub mod signature { } } -// NOTE: The block_signatures module was removed as it was only used for devnet1 format. -// TODO: If BlockSignatures custom serialization is needed for devnet2, implement it here. +/// Custom deserializer for AttestationSignatures that handles the {"data": [sig, ...]} format +/// where each signature can be either hex string or structured XMSS format +pub mod attestation_signatures { + use super::*; + use crate::attestation::AttestationSignatures; + use crate::AggregatedSignatureProof; + use serde::de::Error; + use ssz::PersistentList; + use typenum::U4096; + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let outer: DataWrapper> = + DataWrapper::deserialize(deserializer)?; + + let mut out: PersistentList = PersistentList::default(); + + for aggregated_proof in outer.data.into_iter() { + out.push(aggregated_proof).map_err(|e| { + D::Error::custom(format!( + "AttestationSignatures push aggregated entry failed: {e:?}" + )) + })?; + } + + Ok(out) + } + + pub fn serialize(_value: &AttestationSignatures, _serializer: S) -> Result + where + S: Serializer, + { + // let mut inner: Vec = Vec::new(); + // + // // inner.push(format!("0x{}", hex::encode(sig.as_bytes()))); + // for sig in value.into_iter() { + // inner.push(format!("0x{}", hex::encode(sig.as_bytes()))); + // } + // + // DataWrapper { data: inner }.serialize(serializer) + // TODO: implement serialization + Err(serde::ser::Error::custom( + "AttestationSignatures serialization not implemented for devnet2", + )) + } +} /// Serde helper for ssz::ByteList - serializes as hex string pub mod byte_list { @@ -199,17 +304,32 @@ pub mod byte_list { { use serde::de::Error; - let hex_str = String::deserialize(deserializer)?; - let hex_str = hex_str.trim_start_matches("0x"); + println!("Deserializing ByteList..."); - if hex_str.is_empty() { - return Ok(ByteList::default()); - } + // First, try to parse as a JSON value to inspect the structure + // let value = Value::deserialize(deserializer)?; + let wrapper = DataWrapper::::deserialize(deserializer)?; + + println!("Wrapper data length: {}", wrapper.data.len()); + + // Check if it's a hex string (normal format) + match wrapper.data { + hex_str => { + let hex_str = hex_str.trim_start_matches("0x"); + + if hex_str.is_empty() { + return Ok(ByteList::default()); + } + + let bytes = hex::decode(hex_str) + .map_err(|e| D::Error::custom(format!("Invalid hex string: {}", e)))?; - let bytes = hex::decode(hex_str) - .map_err(|e| D::Error::custom(format!("Invalid hex string: {}", e)))?; + println!("Decoded ByteList bytes length: {}", bytes.len()); - ByteList::try_from(bytes).map_err(|_| D::Error::custom("ByteList exceeds maximum length")) + return ByteList::try_from(bytes) + .map_err(|_| D::Error::custom("ByteList exceeds maximum length")); + } + } } pub fn serialize(value: &ByteList, serializer: S) -> Result @@ -221,3 +341,44 @@ pub mod byte_list { hex_str.serialize(serializer) } } + +/// Custom deserializer for AggregatedAttestations that handles the {"data": [sig, ...]} format +/// where each signature can be either hex string or structured XMSS format +pub mod aggregated_attestations { + use super::*; + use crate::attestation::AggregatedAttestations; + use crate::AggregatedAttestation; + use serde::de::Error; + use ssz::PersistentList; + use typenum::U4096; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let outer: DataWrapper> = + DataWrapper::deserialize(deserializer)?; + + let mut out: PersistentList = PersistentList::default(); + + for aggregated_attestations in outer.data.into_iter() { + out.push(aggregated_attestations).map_err(|e| { + D::Error::custom(format!( + "AggregatedAttestations push aggregated entry failed: {e:?}" + )) + })?; + } + + Ok(out) + } + + pub fn serialize(_value: &AggregatedAttestations, _serializer: S) -> Result + where + S: Serializer, + { + // TODO: implement serialization + Err(serde::ser::Error::custom( + "AttestationSignatures serialization not implemented for devnet2", + )) + } +} diff --git a/lean_client/containers/src/signature.rs b/lean_client/containers/src/signature.rs new file mode 100644 index 0000000..ab39873 --- /dev/null +++ b/lean_client/containers/src/signature.rs @@ -0,0 +1,121 @@ +use alloy_primitives::hex::ToHexExt; +use anyhow::anyhow; +use leansig::{MESSAGE_LENGTH, serialization::Serializable, signature::SignatureScheme}; +use leansig::signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8; +use serde::{Deserialize, Deserializer, Serialize}; +use crate::public_key::{PublicKey}; + +const SIGNATURE_SIZE: usize = 3112; + +type LeanSigSignature = ::Signature; + +/// Wrapper around a fixed-size serialized hash-based signature. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Signature { + pub inner: [u8; SIGNATURE_SIZE], +} + +impl From<&[u8]> for Signature { + fn from(value: &[u8]) -> Self { + // Handle potential length panics or ensure slice is correct size + let mut inner = [0u8; SIGNATURE_SIZE]; + let len = value.len().min(SIGNATURE_SIZE); + inner[..len].copy_from_slice(&value[..len]); + Self { inner } + } +} + +impl Signature { + pub fn new(inner: [u8; SIGNATURE_SIZE]) -> Self { + Self { inner } + } + + pub fn from_lean_sig(signature: LeanSigSignature) -> Result { + let bytes = signature.to_bytes(); + // Ensure we fit into 3112 bytes + if bytes.len() != 3112 { + return Err(anyhow!( + "LeanSigSignature length mismatch: expected 3112, got {}", + bytes.len() + )); + } + let mut inner = [0u8; SIGNATURE_SIZE]; + inner.copy_from_slice(&bytes); + Ok(Self { inner }) + } + + pub fn as_lean_sig(&self) -> anyhow::Result { + println!("Converting Signature to LeanSigSignature..."); + LeanSigSignature::from_bytes(&self.inner) + .map_err(|err| anyhow!("Failed to decode LeanSigSignature from SSZ: {err:?}")) + } + + pub fn verify( + &self, + public_key: &PublicKey, + epoch: u32, + message: &[u8; MESSAGE_LENGTH], + ) -> anyhow::Result { + Ok( + ::verify( + &public_key.as_lean_sig()?, + epoch, + message, + &self.as_lean_sig()?, + ), + ) + } + + /// Debug helper: decode using leansig, then re-encode and ensure bytes match. + pub fn debug_roundtrip(&self) -> anyhow::Result<()> { + let sig = self.as_lean_sig()?; + let re = sig.to_bytes(); + + anyhow::ensure!( + re.as_slice() == self.inner.as_slice(), + "Signature roundtrip mismatch: decoded->encoded bytes differ" + ); + + Ok(()) + } + + /// Debug helper: short stable fingerprint for logs. + pub fn fingerprint_hex(&self) -> String { + use alloy_primitives::hex::ToHexExt; + let bytes = self.inner.as_slice(); + let take = bytes.len().min(12); + format!("0x{}", ToHexExt::encode_hex(&bytes[..take].iter())) + } +} + +impl Serialize for Signature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&format!( + "0x{}", + self.as_lean_sig() + .map_err(serde::ser::Error::custom)? + .to_bytes() + .encode_hex() + )) + } +} + +impl<'de> Deserialize<'de> for Signature { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let result: String = Deserialize::deserialize(deserializer)?; + let result = alloy_primitives::hex::decode(&result).map_err(serde::de::Error::custom)?; + + Self::from_lean_sig( + LeanSigSignature::from_bytes(&result) + .map_err(|err| anyhow!("Convert to error, with error trait implemented {err:?}")) + .map_err(serde::de::Error::custom)?, + ) + .map_err(serde::de::Error::custom) + } +} diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 3b231e0..c894d8c 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -108,7 +108,7 @@ impl State { let mut validators = List::default(); for i in 0..num_validators.0 { let validator = Validator { - pubkey: crate::validator::PublicKey::default(), + pubkey: crate::public_key::PublicKey::default(), index: Uint64(i), }; validators.push(validator).expect("Failed to add validator"); diff --git a/lean_client/containers/src/validator.rs b/lean_client/containers/src/validator.rs index 8a2da60..a6580da 100644 --- a/lean_client/containers/src/validator.rs +++ b/lean_client/containers/src/validator.rs @@ -1,92 +1,10 @@ -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use ssz::ByteVector; +use serde::{Deserialize, Serialize}; use ssz_derive::Ssz; -use typenum::{Unsigned, U52}; - -/// Size of XMSS public keys in bytes (as defined in lean spec) -type PublicKeySize = U52; - -/// XMSS public key (as defined in lean spec) -#[derive(Clone, Debug, PartialEq, Eq, Ssz)] -#[ssz(transparent)] -pub struct PublicKey(pub ByteVector); - -impl Default for PublicKey { - fn default() -> Self { - PublicKey(ByteVector::default()) - } -} - -// Custom serde implementation -impl Serialize for PublicKey { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // ByteVector might have to_vec() or similar - // For now, use unsafe to access the underlying bytes - let bytes = unsafe { - std::slice::from_raw_parts( - &self.0 as *const ByteVector as *const u8, - PublicKeySize::USIZE, - ) - }; - let hex_string = format!("0x{}", hex::encode(bytes)); - serializer.serialize_str(&hex_string) - } -} - -impl<'de> Deserialize<'de> for PublicKey { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let s = s.strip_prefix("0x").unwrap_or(&s); - - let decoded = hex::decode(s).map_err(serde::de::Error::custom)?; - if decoded.len() != PublicKeySize::USIZE { - return Err(serde::de::Error::custom(format!( - "Expected {} bytes, got {}", - PublicKeySize::USIZE, - decoded.len() - ))); - } - - // Create ByteVector from decoded bytes using unsafe - let mut byte_vec = ByteVector::default(); - unsafe { - let dest = &mut byte_vec as *mut ByteVector as *mut u8; - std::ptr::copy_nonoverlapping(decoded.as_ptr(), dest, PublicKeySize::USIZE); - } - - Ok(PublicKey(byte_vec)) - } -} - -impl PublicKey { - pub fn from_hex(s: &str) -> Result { - let s = s.strip_prefix("0x").unwrap_or(s); - let decoded = hex::decode(s).map_err(|e| e.to_string())?; - if decoded.len() != PublicKeySize::USIZE { - return Err(format!( - "Expected {} bytes, got {}", - PublicKeySize::USIZE, - decoded.len() - )); - } - let mut byte_vec = ByteVector::default(); - unsafe { - let dest = &mut byte_vec as *mut ByteVector as *mut u8; - std::ptr::copy_nonoverlapping(decoded.as_ptr(), dest, PublicKeySize::USIZE); - } - Ok(PublicKey(byte_vec)) - } -} #[derive(Clone, Debug, PartialEq, Eq, Default, Ssz, Serialize, Deserialize)] pub struct Validator { - pub pubkey: PublicKey, + // This now uses new XMSS PublicKey struct + pub pubkey: crate::public_key::PublicKey, #[serde(default)] pub index: crate::Uint64, } diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index 9be4935..7459538 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -621,15 +621,19 @@ impl TestRunner { // // NOTE: Disabled until test vector files are regenerated for devnet2 BlockSignatures format. // The current JSON test vectors use signature.data array instead of attestation_signatures + proposer_signature. - /* - pub fn run_verify_signatures_test>(path: P) -> Result<(), Box> { + pub fn run_verify_signatures_test>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; // Parse using the VerifySignaturesTestVectorFile structure let test_file: VerifySignaturesTestVectorFile = serde_json::from_str(&json_content)?; // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; println!("\n{}: {}", test_name, test_case.info.description); @@ -639,14 +643,17 @@ impl TestRunner { // Print some debug info about what we're verifying println!(" Block slot: {}", signed_block.message.block.slot.0); - println!(" Proposer index: {}", signed_block.message.block.proposer_index.0); + println!( + " Proposer index: {}", + signed_block.message.block.proposer_index.0 + ); let attestation_count = signed_block.message.block.body.attestations.len_u64(); println!(" Attestations in block: {}", attestation_count); - println!(" Proposer attestation validator: {}", signed_block.message.proposer_attestation.validator_id.0); - - let signature_count = signed_block.signature.len_u64(); - println!(" Signatures: {}", signature_count); + println!( + " Proposer attestation validator: {}", + signed_block.message.proposer_attestation.validator_id.0 + ); // Check if we expect this test to fail if let Some(ref exception) = test_case.expect_exception { @@ -677,5 +684,4 @@ impl TestRunner { Ok(()) } - */ } diff --git a/lean_client/containers/tests/test_vectors/verify_signatures.rs b/lean_client/containers/tests/test_vectors/verify_signatures.rs index 8be1b4e..ecfc76e 100644 --- a/lean_client/containers/tests/test_vectors/verify_signatures.rs +++ b/lean_client/containers/tests/test_vectors/verify_signatures.rs @@ -1,30 +1,13 @@ // Integration test: verify_signatures test vectors for devnet2 format // Tests XMSS signature verification on SignedBlockWithAttestation -// -// NOTE: These tests are currently disabled because the JSON test vector files -// use the old signature format (signature.data array) rather than the devnet2 -// BlockSignatures structure (attestation_signatures + proposer_signature). -// The test vectors need to be regenerated by leanSpec to match the new format. -// -// Without the `xmss-verify` feature, signature verification only checks -// structure (attestation count matches signature count, validator indices valid). -// Full cryptographic verification requires `--features xmss-verify`. -// -// IMPORTANT: There is currently a configuration mismatch between leanSpec Python -// (HASH_LEN_FE=8, 52-byte pubkeys) and leansig Rust (HASH_LEN_FE=7, 48-byte pubkeys). -// Until this is resolved, the xmss-verify tests will fail with "Invalid public key length". + use super::runner::TestRunner; // Valid signature tests -// These tests verify that properly signed blocks pass verification. -// Without xmss-verify feature, they pass because structural validation succeeds. - -/* #[test] fn test_proposer_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json"; - TestRunner::run_verify_signatures_test(test_path) - .expect("test_proposer_signature failed"); + TestRunner::run_verify_signatures_test(test_path).expect("test_proposer_signature failed"); } #[test] @@ -34,24 +17,17 @@ fn test_proposer_and_attester_signatures() { .expect("test_proposer_and_attester_signatures failed"); } -// Invalid signature tests (expecting verification failure) -// NOTE: These tests are ignored by default because without the `xmss-verify` feature, -// signature verification doesn't actually check cryptographic validity. -// Run with `cargo test --features xmss-verify` to enable full signature verification. - #[test] -#[ignore = "Requires xmss-verify feature for actual signature validation. Run with: cargo test --features xmss-verify"] +#[ignore = "TODO: Fails because of poor error handling, the code panics as it receives wrong length signature although it should handle it more elegantly"] fn test_invalid_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json"; - TestRunner::run_verify_signatures_test(test_path) - .expect("test_invalid_signature failed"); + TestRunner::run_verify_signatures_test(test_path).expect("test_invalid_signature failed"); } #[test] -#[ignore = "Requires xmss-verify feature for actual signature validation. Run with: cargo test --features xmss-verify"] +#[ignore = "This test is commented out in the spec"] fn test_mixed_valid_invalid_signatures() { let test_path = "../tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_mixed_valid_invalid_signatures.json"; TestRunner::run_verify_signatures_test(test_path) .expect("test_mixed_valid_invalid_signatures failed"); } -*/ diff --git a/lean_client/fork_choice/Cargo.toml b/lean_client/fork_choice/Cargo.toml index d08f647..f707648 100644 --- a/lean_client/fork_choice/Cargo.toml +++ b/lean_client/fork_choice/Cargo.toml @@ -9,11 +9,4 @@ default = [] [dependencies] env-config = { path = "../env-config", default-features = false } containers = { path = "../containers", default-features = false } -ssz = { git = "https://github.com/grandinetech/grandine", package = "ssz", branch = "develop"} -ssz_derive = { git = "https://github.com/grandinetech/grandine", package = "ssz_derive", branch = "develop" } -typenum = "1.17.0" -serde = { version = "1.0", features = ["derive"] } - -[dev-dependencies] -ssz_rs = "0.9" -serde_json = "1.0" +ssz = { workspace = true } diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index b00b071..3054052 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -161,12 +161,6 @@ fn process_block_internal( // Get the corresponding proof from attestation_signatures if let Ok(proof_data) = signatures.attestation_signatures.get(att_idx as u64) { - // Create AggregatedSignatureProof with participants and proof data - let aggregated_proof = containers::AggregatedSignatureProof::new( - aggregated_attestation.aggregation_bits.clone(), - proof_data.clone(), - ); - // Store proof for each validator in the aggregation for (bit_idx, bit) in aggregated_attestation.aggregation_bits.0.iter().enumerate() { if *bit { @@ -176,7 +170,7 @@ fn process_block_internal( .aggregated_payloads .entry(sig_key) .or_insert_with(Vec::new) - .push(aggregated_proof.clone()); + .push(proof_data.clone()); } } } diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index a38ce83..acc6bc1 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -164,7 +164,7 @@ async fn main() { .iter() .enumerate() .map(|(i, v_str)| { - let pubkey = containers::validator::PublicKey::from_hex(v_str) + let pubkey = containers::public_key::PublicKey::from_hex(v_str) .expect("Invalid genesis validator pubkey"); containers::validator::Validator { pubkey, @@ -178,7 +178,7 @@ async fn main() { let num_validators = 3; let validators = (0..num_validators) .map(|i| containers::validator::Validator { - pubkey: containers::validator::PublicKey::default(), + pubkey: containers::public_key::PublicKey::default(), index: Uint64(i as u64), }) .collect(); diff --git a/lean_client/tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json b/lean_client/tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json index a4cd2af..ef3fcf3 100644 --- a/lean_client/tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json +++ b/lean_client/tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json @@ -1,6 +1,7 @@ { "tests/consensus/devnet/verify_signatures/test_invalid_signatures.py::test_invalid_signature[fork_Devnet][fork_Devnet-verify_signatures_test]": { "network": "Devnet", + "leanEnv": "prod", "anchorState": { "config": { "genesisTime": 0 @@ -30,7 +31,7 @@ "validators": { "data": [ { - "pubkey": "0xb40ec9783b56572d99965130ad1cec4f066df43a0fd89d22d420535c62b52711cdaf7f5f696c6b1e4df66a6bab48bb51abcdd77e", + "pubkey": "0x2c7aa65d0b45c01e3ba7155997d3001613775f1e1b85fd1ab65dcd72c65e105f2552c43a311a290af56fa0262dfebe10745ea837", "index": 0 } ] @@ -47,8 +48,8 @@ "block": { "slot": 1, "proposerIndex": 0, - "parentRoot": "0x7ee509d36952a8f41f5dc5b4627487f4d523c9333ef7af2a692ae12867eeee16", - "stateRoot": "0xe9dd253c1cff8e8719113e66970f54a44c885997f3e57ba6dc993861eca4f40c", + "parentRoot": "0x438b1cbc01c3c69b14f8853c6463d28b58118798f414f3be472aae4cd77dd572", + "stateRoot": "0x38d7edaf9c08ffb285eb3e1e64456fbe430d4c4a4bab9b0bf29565b74da5c620", "body": { "attestations": { "data": [] @@ -60,52 +61,53 @@ "data": { "slot": 1, "head": { - "root": "0xf0c95e7f668652482cc00916c57ab6c84b435a29d030f682b92d95f9f3add62a", + "root": "0x6f2ebcd6e5eb1b34823a5fb5867ee63984905cc670722a01a060894b9b2cec3f", "slot": 1 }, "target": { - "root": "0xf0c95e7f668652482cc00916c57ab6c84b435a29d030f682b92d95f9f3add62a", + "root": "0x6f2ebcd6e5eb1b34823a5fb5867ee63984905cc670722a01a060894b9b2cec3f", "slot": 1 }, "source": { - "root": "0x7ee509d36952a8f41f5dc5b4627487f4d523c9333ef7af2a692ae12867eeee16", + "root": "0x438b1cbc01c3c69b14f8853c6463d28b58118798f414f3be472aae4cd77dd572", "slot": 0 } } } }, "signature": { - "data": [ - { - "path": { - "siblings": { - "data": [] - } - }, - "rho": { - "data": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - }, - "hashes": { + "attestationSignatures": { + "data": [] + }, + "proposerSignature": { + "path": { + "siblings": { "data": [] } + }, + "rho": { + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "hashes": { + "data": [] } - ] + } } }, "expectException": "AssertionError", "_info": { - "hash": "0x7befd11b0a26c4bfbd4c40d5b4c21d7a510cc2ed899aa3f494b7c4d9574fd7d3", + "hash": "0x8d97e6b6a601e10856ae70720ffad8cd392dab8169fe158054edac0bc0f0e49a", "comment": "`leanSpec` generated test", "testId": "tests/consensus/devnet/verify_signatures/test_invalid_signatures.py::test_invalid_signature[fork_Devnet]", - "description": "Test that invalid signatures are properly rejected during verification.\n\n Scenario\n --------\n - Single block at slot 1\n - Proposer attestation has an invalid signature\n - No additional attestations (only proposer attestation)\n\n Expected Behavior\n -----------------\n 1. Proposer's signature in SignedBlockWithAttestation is rejected\n\n Why This Matters\n ----------------\n This test verifies the negative case:\n - Signature verification actually validates cryptographic correctness\n not just structural correctness.\n - Invalid signatures are caught, not silently accepted", + "description": "Test that invalid signatures are properly rejected during verification.\n\nScenario\n--------\n- Single block at slot 1\n- Proposer attestation has an invalid signature\n- No additional attestations (only proposer attestation)\n\nExpected Behavior\n-----------------\n1. Proposer's signature in SignedBlockWithAttestation is rejected\n\nWhy This Matters\n----------------\nThis test verifies the negative case:\n- Signature verification actually validates cryptographic correctness\n not just structural correctness.\n- Invalid signatures are caught, not silently accepted", "fixtureFormat": "verify_signatures_test" } } diff --git a/lean_client/tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_mixed_valid_invalid_signatures.json b/lean_client/tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_mixed_valid_invalid_signatures.json deleted file mode 100644 index c6a10f9..0000000 --- a/lean_client/tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_mixed_valid_invalid_signatures.json +++ /dev/null @@ -1,491 +0,0 @@ -{ - "tests/consensus/devnet/verify_signatures/test_invalid_signatures.py::test_mixed_valid_invalid_signatures[fork_Devnet][fork_Devnet-verify_signatures_test]": { - "network": "Devnet", - "anchorState": { - "config": { - "genesisTime": 0 - }, - "slot": 0, - "latestBlockHeader": { - "slot": 0, - "proposerIndex": 0, - "parentRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", - "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", - "bodyRoot": "0xdba9671bac9513c9482f1416a53aabd2c6ce90d5a5f865ce5a55c775325c9136" - }, - "latestJustified": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - }, - "latestFinalized": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - }, - "historicalBlockHashes": { - "data": [] - }, - "justifiedSlots": { - "data": [] - }, - "validators": { - "data": [ - { - "pubkey": "0xb40ec9783b56572d99965130ad1cec4f066df43a0fd89d22d420535c62b52711cdaf7f5f696c6b1e4df66a6bab48bb51abcdd77e", - "index": 0 - }, - { - "pubkey": "0xc5ed4b6fa46e83281e29201b878ad52b4a9b8a2f3c9d8017cfd9b81e8097e1372a097e62be37194c39e2f36f76e8906d2e818238", - "index": 1 - }, - { - "pubkey": "0x0cc8a378cdeb1a3c4b87156bcba3e87d31fb8c5f1c3ce74b4370001086b8bd5a502ea12d0878536b19cb6769b7285e1c0504fb3e", - "index": 2 - } - ] - }, - "justificationsRoots": { - "data": [] - }, - "justificationsValidators": { - "data": [] - } - }, - "signedBlockWithAttestation": { - "message": { - "block": { - "slot": 1, - "proposerIndex": 1, - "parentRoot": "0x9e3b89451933da29e3697c588770a4d63c900e0a8af56e2a4e0777abdd355450", - "stateRoot": "0x85222dc92460f8b51fe414fb34ef9f35247653eb6036b1268261a91ba617cda8", - "body": { - "attestations": { - "data": [ - { - "validatorId": 0, - "data": { - "slot": 1, - "head": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - }, - "target": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - }, - "source": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - } - } - }, - { - "validatorId": 2, - "data": { - "slot": 1, - "head": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - }, - "target": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - }, - "source": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - } - } - } - ] - } - } - }, - "proposerAttestation": { - "validatorId": 1, - "data": { - "slot": 1, - "head": { - "root": "0xde28efe59b19913717bb76a32611fe839ad69a4b3c0915d79c8304b35b57ddf7", - "slot": 1 - }, - "target": { - "root": "0xde28efe59b19913717bb76a32611fe839ad69a4b3c0915d79c8304b35b57ddf7", - "slot": 1 - }, - "source": { - "root": "0x9e3b89451933da29e3697c588770a4d63c900e0a8af56e2a4e0777abdd355450", - "slot": 0 - } - } - } - }, - "signature": { - "data": [ - { - "path": { - "siblings": { - "data": [ - { - "data": [ - 1715018400, - 48710923, - 1748245036, - 163403131, - 924640484, - 1566519705, - 1860210712, - 1236746232 - ] - }, - { - "data": [ - 318044943, - 399009750, - 1038257959, - 729848679, - 1449298444, - 436326364, - 977163460, - 1497861895 - ] - }, - { - "data": [ - 1468833114, - 1734637349, - 1929839981, - 1267639175, - 796117685, - 47500478, - 1956344905, - 1320986094 - ] - }, - { - "data": [ - 1266260145, - 725202725, - 218929017, - 126358625, - 921715766, - 1979527002, - 1695252564, - 1220353106 - ] - }, - { - "data": [ - 298307958, - 2042817198, - 1699263182, - 1453266496, - 1023068754, - 224889272, - 2049483392, - 1399154486 - ] - }, - { - "data": [ - 1430973325, - 1579483336, - 1154958176, - 318946268, - 1584562777, - 1947187050, - 886182999, - 1154818886 - ] - }, - { - "data": [ - 1056622773, - 601147086, - 1222204938, - 264848405, - 1363314459, - 109131915, - 517301456, - 938514082 - ] - }, - { - "data": [ - 483358618, - 57732057, - 329296853, - 1352276692, - 88248225, - 1800662461, - 2098999624, - 2064134886 - ] - } - ] - } - }, - "rho": { - "data": [ - 1133244814, - 826948562, - 694211053, - 360187930, - 342494093, - 526958919, - 549074822 - ] - }, - "hashes": { - "data": [ - { - "data": [ - 780469602, - 1390643088, - 2027017214, - 1431163446, - 1529907007, - 365863100, - 1195111668, - 1880854250 - ] - }, - { - "data": [ - 1605769376, - 741812234, - 1163184354, - 1147446555, - 871882010, - 948907942, - 551347671, - 840722750 - ] - }, - { - "data": [ - 1181134299, - 1236421381, - 185118722, - 573142269, - 160921481, - 1510683126, - 294606954, - 1927123925 - ] - }, - { - "data": [ - 1347741188, - 1460449909, - 596275218, - 1289700342, - 1411024602, - 1833568587, - 1711725928, - 6783578 - ] - } - ] - } - }, - { - "path": { - "siblings": { - "data": [] - } - }, - "rho": { - "data": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ] - }, - "hashes": { - "data": [] - } - }, - { - "path": { - "siblings": { - "data": [ - { - "data": [ - 1331122823, - 1643080471, - 525470619, - 2031212805, - 2098748101, - 1243499988, - 4104831, - 2063254342 - ] - }, - { - "data": [ - 744813374, - 813288082, - 613270960, - 679006507, - 533419743, - 993872253, - 286847881, - 1451300746 - ] - }, - { - "data": [ - 11079834, - 1639687748, - 1929217599, - 202276490, - 40122498, - 1566411931, - 675580467, - 904231754 - ] - }, - { - "data": [ - 2020782635, - 1574539412, - 1138761573, - 422111916, - 1389054530, - 1510501223, - 197381584, - 402775535 - ] - }, - { - "data": [ - 264438681, - 1087279288, - 263416095, - 1860660250, - 1429146526, - 2094709316, - 867692041, - 1662763959 - ] - }, - { - "data": [ - 544815286, - 311733778, - 799237190, - 2102805408, - 1661418854, - 1157854114, - 1855929130, - 744137327 - ] - }, - { - "data": [ - 1462886854, - 1889836598, - 2029637687, - 53625032, - 673267735, - 2047374486, - 49833992, - 1921530767 - ] - }, - { - "data": [ - 110758107, - 12788259, - 1920648715, - 413804969, - 419452419, - 1819780529, - 1147494825, - 526331496 - ] - } - ] - } - }, - "rho": { - "data": [ - 754265996, - 879244860, - 1486259768, - 2046100849, - 517389142, - 1321641711, - 698992751 - ] - }, - "hashes": { - "data": [ - { - "data": [ - 1782166851, - 755483088, - 705928616, - 1054809239, - 1035991143, - 598933101, - 107624567, - 580522477 - ] - }, - { - "data": [ - 191975122, - 1845573414, - 1060661118, - 3844096, - 767890828, - 1256682430, - 1322161263, - 1960290303 - ] - }, - { - "data": [ - 1186545818, - 1781761501, - 1739225478, - 720736896, - 819060565, - 601088943, - 1836159403, - 4774455 - ] - }, - { - "data": [ - 523507152, - 640464516, - 1715695303, - 1682124987, - 1442709818, - 495961733, - 1030632883, - 2056248017 - ] - } - ] - } - } - ] - } - }, - "expectException": "AssertionError", - "_info": { - "hash": "0xf28229e6d5294df33294f6f72cb404de35083227e0f0548f9e03d55a7b8aa32d", - "comment": "`leanSpec` generated test", - "testId": "tests/consensus/devnet/verify_signatures/test_invalid_signatures.py::test_mixed_valid_invalid_signatures[fork_Devnet]", - "description": "Test that signature verification catches invalid signatures among valid ones.\n\n Scenario\n --------\n - Single block at slot 1\n - Proposer attestation from validator 1\n - 2 non-proposer attestations from validators 0 and 2\n - Total: 3 signatures, middle attestation (validator 2) has an invalid signature\n\n Expected Behavior\n -----------------\n 1. The SignedBlockWithAttestation is rejected due to 1 invalid signature\n\n Why This Matters\n ----------------\n This test verifies that signature verification:\n - Checks every signature individually, not just the first or last\n - Cannot be bypassed by surrounding invalid signatures with valid ones\n - Properly fails even when some signatures are valid\n - Validates all attestations in the block", - "fixtureFormat": "verify_signatures_test" - } - } -} \ No newline at end of file diff --git a/lean_client/tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_and_attester_signatures.json b/lean_client/tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_and_attester_signatures.json index 306a4a2..ad8df6f 100644 --- a/lean_client/tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_and_attester_signatures.json +++ b/lean_client/tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_and_attester_signatures.json @@ -1,6 +1,7 @@ { "tests/consensus/devnet/verify_signatures/test_valid_signatures.py::test_proposer_and_attester_signatures[fork_Devnet][fork_Devnet-verify_signatures_test]": { "network": "Devnet", + "leanEnv": "prod", "anchorState": { "config": { "genesisTime": 0 @@ -30,15 +31,15 @@ "validators": { "data": [ { - "pubkey": "0xb40ec9783b56572d99965130ad1cec4f066df43a0fd89d22d420535c62b52711cdaf7f5f696c6b1e4df66a6bab48bb51abcdd77e", + "pubkey": "0x2c7aa65d0b45c01e3ba7155997d3001613775f1e1b85fd1ab65dcd72c65e105f2552c43a311a290af56fa0262dfebe10745ea837", "index": 0 }, { - "pubkey": "0xc5ed4b6fa46e83281e29201b878ad52b4a9b8a2f3c9d8017cfd9b81e8097e1372a097e62be37194c39e2f36f76e8906d2e818238", + "pubkey": "0x67047f6113b87f6aea3a0e6c108a8d7dcc4b5e479dda2a1cb79397705340bd6227f1ea63ee39c91fff4599375b749425ae65922e", "index": 1 }, { - "pubkey": "0x0cc8a378cdeb1a3c4b87156bcba3e87d31fb8c5f1c3ce74b4370001086b8bd5a502ea12d0878536b19cb6769b7285e1c0504fb3e", + "pubkey": "0x235cec31602fa34e41069453a2a56568d4c3ff62e31066659b90722e595e93255637eb0d42f98542e79c60031c2f05396bc74146", "index": 2 } ] @@ -55,31 +56,19 @@ "block": { "slot": 1, "proposerIndex": 1, - "parentRoot": "0x9e3b89451933da29e3697c588770a4d63c900e0a8af56e2a4e0777abdd355450", - "stateRoot": "0x85222dc92460f8b51fe414fb34ef9f35247653eb6036b1268261a91ba617cda8", + "parentRoot": "0x0b530309ddcb6e08a7ddbe1558a772ddea639cec05c2ddff23b597411df0745e", + "stateRoot": "0x92f8e374cbfbb7a91bee6a58e0c60cb6781bc4ba4bea028be0e46e5146b9e034", "body": { "attestations": { "data": [ { - "validatorId": 0, - "data": { - "slot": 1, - "head": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - }, - "target": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - }, - "source": { - "root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "slot": 0 - } - } - }, - { - "validatorId": 2, + "aggregationBits": { + "data": [ + true, + false, + true + ] + }, "data": { "slot": 1, "head": { @@ -105,531 +94,1219 @@ "data": { "slot": 1, "head": { - "root": "0xde28efe59b19913717bb76a32611fe839ad69a4b3c0915d79c8304b35b57ddf7", + "root": "0xc18aff973b62478f234db568edc4811cd2a59885fa7f712e593b9956dde7fa5a", "slot": 1 }, "target": { - "root": "0xde28efe59b19913717bb76a32611fe839ad69a4b3c0915d79c8304b35b57ddf7", + "root": "0xc18aff973b62478f234db568edc4811cd2a59885fa7f712e593b9956dde7fa5a", "slot": 1 }, "source": { - "root": "0x9e3b89451933da29e3697c588770a4d63c900e0a8af56e2a4e0777abdd355450", + "root": "0x0b530309ddcb6e08a7ddbe1558a772ddea639cec05c2ddff23b597411df0745e", "slot": 0 } } } }, "signature": { - "data": [ - { - "path": { - "siblings": { + "attestationSignatures": { + "data": [ + { + "participants": { "data": [ - { - "data": [ - 1715018400, - 48710923, - 1748245036, - 163403131, - 924640484, - 1566519705, - 1860210712, - 1236746232 - ] - }, - { - "data": [ - 318044943, - 399009750, - 1038257959, - 729848679, - 1449298444, - 436326364, - 977163460, - 1497861895 - ] - }, - { - "data": [ - 1468833114, - 1734637349, - 1929839981, - 1267639175, - 796117685, - 47500478, - 1956344905, - 1320986094 - ] - }, - { - "data": [ - 1266260145, - 725202725, - 218929017, - 126358625, - 921715766, - 1979527002, - 1695252564, - 1220353106 - ] - }, - { - "data": [ - 298307958, - 2042817198, - 1699263182, - 1453266496, - 1023068754, - 224889272, - 2049483392, - 1399154486 - ] - }, - { - "data": [ - 1430973325, - 1579483336, - 1154958176, - 318946268, - 1584562777, - 1947187050, - 886182999, - 1154818886 - ] - }, - { - "data": [ - 1056622773, - 601147086, - 1222204938, - 264848405, - 1363314459, - 109131915, - 517301456, - 938514082 - ] - }, - { - "data": [ - 483358618, - 57732057, - 329296853, - 1352276692, - 88248225, - 1800662461, - 2098999624, - 2064134886 - ] - } + true, + false, + true ] + }, + "proofData": { + "data": "0x00" } - }, - "rho": { - "data": [ - 1133244814, - 826948562, - 694211053, - 360187930, - 342494093, - 526958919, - 549074822 - ] - }, - "hashes": { + } + ] + }, + "proposerSignature": { + "path": { + "siblings": { "data": [ { "data": [ - 780469602, - 1390643088, - 2027017214, - 1431163446, - 1529907007, - 365863100, - 1195111668, - 1880854250 + 1291233891, + 901611691, + 1515447763, + 518859210, + 691456689, + 1723616255, + 1740655893, + 1397350123 ] }, { "data": [ - 1605769376, - 741812234, - 1163184354, - 1147446555, - 871882010, - 948907942, - 551347671, - 840722750 + 1456518562, + 484811598, + 2010092021, + 282492124, + 1770180907, + 1769233778, + 494579282, + 1699827616 ] }, { "data": [ - 1181134299, - 1236421381, - 185118722, - 573142269, - 160921481, - 1510683126, - 294606954, - 1927123925 + 704838371, + 2081776305, + 559183133, + 1733361779, + 263725425, + 344472513, + 2086779080, + 1240527530 ] }, { "data": [ - 1347741188, - 1460449909, - 596275218, - 1289700342, - 1411024602, - 1833568587, - 1711725928, - 6783578 + 1039415328, + 308461932, + 2043838459, + 611597666, + 1580071319, + 1516124162, + 698977291, + 1585930624 ] - } - ] - } - }, - { - "path": { - "siblings": { - "data": [ - { - "data": [ - 245987955, - 1574460560, - 1525707632, - 123960916, - 1086053397, - 2080663024, - 366379460, - 1998029054 - ] - }, - { - "data": [ - 2019630499, - 1360353298, - 1175664713, - 1796753426, - 1565903239, - 1735318974, - 1982905657, - 614050054 - ] - }, - { - "data": [ - 1454884048, - 454543812, - 1015264795, - 356242932, - 501670992, - 463708403, - 459704849, - 1579831167 - ] - }, - { - "data": [ - 665199378, - 1891601081, - 858563106, - 596194897, - 332003681, - 2038255694, - 1339684636, - 1387152804 - ] - }, - { - "data": [ - 543025152, - 786491463, - 1123029687, - 1264918908, - 984563024, - 1188331300, - 680922823, - 1816896828 - ] - }, - { - "data": [ - 1326664843, - 1319602875, - 1528705430, - 227865620, - 708371624, - 1637263589, - 1037139620, - 1485301417 - ] - }, - { - "data": [ - 1192630398, - 484414116, - 313965305, - 196703586, - 1501710664, - 1683049995, - 904524380, - 1174797428 - ] - }, - { - "data": [ - 2063074693, - 101123966, - 607293596, - 477669941, - 1941778158, - 1668669356, - 1179339209, - 864743993 - ] - } - ] - } - }, - "rho": { - "data": [ - 2045446833, - 446323957, - 1182968240, - 517145103, - 1767538833, - 631690441, - 1778446117 - ] - }, - "hashes": { - "data": [ + }, { "data": [ - 423057391, - 388700883, - 1083440537, - 359923932, - 388595029, - 1736053252, - 350530532, - 731868998 + 250662127, + 1008190943, + 983708486, + 1247986374, + 1886580775, + 1647509743, + 1550488627, + 1260597451 ] }, { "data": [ - 1090832151, - 533474659, - 809763430, - 507768385, - 98359830, - 1734162267, - 1243455951, - 1715081891 + 416883050, + 188953242, + 1182024076, + 59202244, + 1978179518, + 1739410190, + 679526947, + 861617775 ] }, { "data": [ - 634986620, - 1063163068, - 1040321224, - 1267702389, - 627871860, - 228913093, - 921478258, - 1324923786 + 1378484229, + 241452936, + 163273125, + 436861107, + 388667496, + 1797577532, + 443869040, + 906552846 ] }, { "data": [ - 786393727, - 991293795, - 1375401462, - 1980764312, - 680859391, - 1131959594, - 396370528, - 1759770981 + 799696786, + 1508418299, + 1856736097, + 1058842462, + 269359710, + 1099230447, + 749521578, + 520853695 ] - } - ] - } - }, - { - "path": { - "siblings": { - "data": [ - { - "data": [ - 1331122823, - 1643080471, - 525470619, - 2031212805, - 2098748101, - 1243499988, - 4104831, - 2063254342 - ] - }, - { - "data": [ - 744813374, - 813288082, - 613270960, - 679006507, - 533419743, - 993872253, - 286847881, - 1451300746 - ] - }, - { - "data": [ - 11079834, - 1639687748, - 1929217599, - 202276490, - 40122498, - 1566411931, - 675580467, - 904231754 - ] - }, - { - "data": [ - 2020782635, - 1574539412, - 1138761573, - 422111916, - 1389054530, - 1510501223, - 197381584, - 402775535 - ] - }, - { - "data": [ - 264438681, - 1087279288, - 263416095, - 1860660250, - 1429146526, - 2094709316, - 867692041, - 1662763959 - ] - }, - { - "data": [ - 544815286, - 311733778, - 799237190, - 2102805408, - 1661418854, - 1157854114, - 1855929130, - 744137327 - ] - }, - { - "data": [ - 1462886854, - 1889836598, - 2029637687, - 53625032, - 673267735, - 2047374486, - 49833992, - 1921530767 - ] - }, - { - "data": [ - 110758107, - 12788259, - 1920648715, - 413804969, - 419452419, - 1819780529, - 1147494825, - 526331496 - ] - } - ] - } - }, - "rho": { - "data": [ - 754265996, - 879244860, - 1486259768, - 2046100849, - 517389142, - 1321641711, - 698992751 - ] - }, - "hashes": { - "data": [ + }, { "data": [ - 1782166851, - 755483088, - 705928616, - 1054809239, - 1035991143, - 598933101, - 107624567, - 580522477 + 439422050, + 1286915872, + 1358195170, + 840970194, + 1786302274, + 893515818, + 370457729, + 1427474800 ] }, { "data": [ - 191975122, - 1845573414, - 1060661118, - 3844096, - 767890828, - 1256682430, - 1322161263, - 1960290303 + 2007504140, + 418866321, + 198684106, + 1996996870, + 1261455552, + 2118739919, + 56436890, + 1806594952 ] }, { "data": [ - 1186545818, - 1781761501, - 1739225478, - 720736896, - 819060565, - 601088943, - 1836159403, - 4774455 + 128324476, + 61519552, + 113352202, + 1839954511, + 2112962791, + 1831734346, + 1400107873, + 849776052 ] }, { "data": [ - 523507152, - 640464516, - 1715695303, - 1682124987, - 1442709818, - 495961733, - 1030632883, - 2056248017 + 694706906, + 2057189854, + 1419202856, + 2020454400, + 1916412209, + 304600413, + 1119212112, + 988476852 + ] + }, + { + "data": [ + 1607491401, + 916185160, + 150839959, + 1915584536, + 1192630676, + 166752571, + 44618577, + 1961530862 + ] + }, + { + "data": [ + 1949287225, + 1766709295, + 928506169, + 833212136, + 35771750, + 71835570, + 1852857681, + 1205452729 + ] + }, + { + "data": [ + 1644443152, + 1520132256, + 1370044265, + 851862297, + 261020286, + 1001533477, + 571576626, + 907308311 + ] + }, + { + "data": [ + 1557846841, + 210200770, + 685212717, + 1586976910, + 463743886, + 395493034, + 1562290362, + 1016157604 + ] + }, + { + "data": [ + 208831676, + 1180089898, + 2064964824, + 1411007716, + 1673605982, + 1643551528, + 1539845891, + 704493341 + ] + }, + { + "data": [ + 895079925, + 877096130, + 2081347331, + 124656629, + 1179296144, + 1491205760, + 356412314, + 926452265 + ] + }, + { + "data": [ + 1422286144, + 984088526, + 1135304910, + 162305405, + 1064769342, + 1110991338, + 104215457, + 1422827345 + ] + }, + { + "data": [ + 912789203, + 2010420420, + 429286304, + 295855493, + 2084240709, + 1193367228, + 1021205972, + 560375846 + ] + }, + { + "data": [ + 489627247, + 396093595, + 475714912, + 573904495, + 382358549, + 668148792, + 1416579671, + 1444313453 + ] + }, + { + "data": [ + 1345967333, + 723445075, + 2048740831, + 153155071, + 10838758, + 1236457738, + 18985351, + 1138484833 + ] + }, + { + "data": [ + 993666071, + 473860152, + 482974488, + 1244638895, + 1287107597, + 1492708811, + 1976127099, + 653628523 + ] + }, + { + "data": [ + 791701066, + 885184677, + 106955182, + 1572481724, + 1456627534, + 1452427937, + 490533016, + 991293345 + ] + }, + { + "data": [ + 578639661, + 1631171923, + 268843612, + 1788996364, + 1746080381, + 1046103170, + 455298193, + 562115509 + ] + }, + { + "data": [ + 235948816, + 1018300141, + 1002498336, + 201831066, + 1124789148, + 1994905284, + 561014981, + 1257286951 + ] + }, + { + "data": [ + 1045385620, + 1058192257, + 938515492, + 573527403, + 989948080, + 1342850602, + 1832637791, + 358929324 + ] + }, + { + "data": [ + 1902152809, + 252599905, + 623219565, + 758434560, + 640896011, + 207032991, + 835792870, + 1665795896 + ] + }, + { + "data": [ + 723715685, + 587576367, + 853971724, + 1144944495, + 873175376, + 498689849, + 43297292, + 819091873 + ] + }, + { + "data": [ + 280016656, + 437742428, + 255947140, + 349343920, + 1615039346, + 1488983802, + 1389523623, + 1912297556 + ] + }, + { + "data": [ + 901881368, + 1526942608, + 852049680, + 731288118, + 661508501, + 2010590000, + 868332352, + 1726397935 + ] + }, + { + "data": [ + 1349685696, + 2130099561, + 1462674991, + 1479393751, + 1013840091, + 1746802826, + 422993280, + 544780634 ] } ] } + }, + "rho": { + "data": [ + 1708063334, + 500631902, + 912463888, + 1859022035, + 2093407176, + 589622141, + 421358791 + ] + }, + "hashes": { + "data": [ + { + "data": [ + 1258012936, + 1759805387, + 1282523131, + 1087625667, + 2066885140, + 347413941, + 912179848, + 1182795675 + ] + }, + { + "data": [ + 2120621302, + 1146272237, + 220452956, + 1654122849, + 667076721, + 647293161, + 1473822684, + 610014272 + ] + }, + { + "data": [ + 588127966, + 1623774910, + 1981302286, + 1654815645, + 1739263748, + 476540846, + 1430988502, + 371554021 + ] + }, + { + "data": [ + 147575978, + 584959873, + 482088843, + 547413274, + 878230111, + 947438052, + 2065600800, + 1725116311 + ] + }, + { + "data": [ + 782401603, + 181111304, + 74795908, + 1495556562, + 2014424927, + 2103029287, + 626628827, + 915290649 + ] + }, + { + "data": [ + 1386657693, + 226764475, + 170886560, + 1391287227, + 241686273, + 1439085926, + 1299696477, + 224457038 + ] + }, + { + "data": [ + 156077062, + 984761927, + 636114116, + 2128285193, + 804007702, + 145764472, + 1829941080, + 897763155 + ] + }, + { + "data": [ + 832369921, + 238965180, + 160945039, + 1145687264, + 1389349131, + 1691977522, + 979195797, + 524772744 + ] + }, + { + "data": [ + 1305584230, + 1558936225, + 439307137, + 1771754638, + 875067293, + 1165888195, + 1802707435, + 1814799779 + ] + }, + { + "data": [ + 1584322090, + 1505637739, + 1431439512, + 2049043333, + 2031336532, + 1994220632, + 535798250, + 1003107923 + ] + }, + { + "data": [ + 536723909, + 1389453682, + 407278690, + 1778319839, + 1777727737, + 1948491210, + 1489215087, + 94425788 + ] + }, + { + "data": [ + 1939722636, + 1550150715, + 601277655, + 1185348003, + 205771634, + 1131394685, + 1434984545, + 971649311 + ] + }, + { + "data": [ + 1895618555, + 1937165542, + 1924755874, + 1626462217, + 1247425034, + 1370792545, + 10993310, + 259827571 + ] + }, + { + "data": [ + 960895278, + 1441935738, + 564122556, + 476582278, + 1152905344, + 187751495, + 1256558054, + 1184773204 + ] + }, + { + "data": [ + 1142655319, + 976140656, + 1227333160, + 2129498013, + 867861598, + 1790717609, + 866871241, + 1774362003 + ] + }, + { + "data": [ + 310323031, + 1437920355, + 878272104, + 722971220, + 1015159963, + 409582600, + 512066076, + 854680398 + ] + }, + { + "data": [ + 1078600753, + 1684508279, + 316224361, + 1222713314, + 336701486, + 1165314551, + 997088307, + 1438291675 + ] + }, + { + "data": [ + 1317260158, + 1861218639, + 1224575160, + 421550610, + 765270751, + 1827053147, + 289080869, + 538182924 + ] + }, + { + "data": [ + 191317502, + 911206352, + 127176973, + 1283200174, + 122086992, + 2069722074, + 1696651747, + 1805703619 + ] + }, + { + "data": [ + 1585522895, + 580813326, + 1019407832, + 475961126, + 2007366427, + 808496979, + 1181091986, + 697679912 + ] + }, + { + "data": [ + 1512587243, + 1963077188, + 1954992331, + 1545360150, + 1178760012, + 1515958126, + 705452917, + 2114456876 + ] + }, + { + "data": [ + 2049583212, + 1094272227, + 1039456282, + 787530139, + 1640279372, + 1330559514, + 240146549, + 1313599913 + ] + }, + { + "data": [ + 1716678544, + 878739389, + 47637648, + 1124863294, + 1855735812, + 648435874, + 1372962920, + 1357760622 + ] + }, + { + "data": [ + 1700444351, + 164566502, + 397969528, + 335079975, + 293991016, + 1078783808, + 326444266, + 1217021268 + ] + }, + { + "data": [ + 545406936, + 53426137, + 424114470, + 1111280438, + 618160273, + 1802384583, + 1667812411, + 783526014 + ] + }, + { + "data": [ + 305021847, + 844199180, + 1053002307, + 573437770, + 1003609966, + 752594751, + 1962990311, + 1014845114 + ] + }, + { + "data": [ + 1726913971, + 580369471, + 1458334500, + 153335379, + 781417921, + 1461588083, + 1878087297, + 1976620351 + ] + }, + { + "data": [ + 1342240957, + 1951347581, + 927989919, + 979795407, + 446565280, + 1833560107, + 1611077456, + 1708982869 + ] + }, + { + "data": [ + 1555733412, + 183459902, + 1845209457, + 294206894, + 487746799, + 1581300684, + 1098936144, + 1463828258 + ] + }, + { + "data": [ + 1627490533, + 1198683786, + 347829445, + 868233249, + 668381542, + 1667170279, + 1843656481, + 2118868916 + ] + }, + { + "data": [ + 25335971, + 1947476635, + 1202969731, + 1324303434, + 840681315, + 1530295647, + 73829885, + 2084868034 + ] + }, + { + "data": [ + 1384538139, + 1830543638, + 1993528212, + 829245670, + 987182524, + 1984193286, + 1630629317, + 671245330 + ] + }, + { + "data": [ + 1350732214, + 1458554923, + 1967947691, + 1326432866, + 2116862031, + 1830754813, + 1993865530, + 1629953044 + ] + }, + { + "data": [ + 1146542130, + 280817620, + 386152006, + 1428960819, + 1210084215, + 452674181, + 14651754, + 888508333 + ] + }, + { + "data": [ + 1560045092, + 1296963539, + 284985770, + 1434652130, + 229612754, + 1450040209, + 1958058095, + 1037043393 + ] + }, + { + "data": [ + 2020927885, + 18361940, + 653582762, + 1686120847, + 1597265575, + 28912714, + 443462147, + 870096418 + ] + }, + { + "data": [ + 1695586982, + 3373096, + 104141097, + 1042336897, + 994168241, + 1453130775, + 511038748, + 965536893 + ] + }, + { + "data": [ + 1605236949, + 127566776, + 21238712, + 434461941, + 1022175801, + 1240317127, + 1122138289, + 1008747176 + ] + }, + { + "data": [ + 2102377138, + 1530162129, + 909575023, + 1237305669, + 511960395, + 2038778105, + 287638646, + 545475552 + ] + }, + { + "data": [ + 123969828, + 595339923, + 285763600, + 1913417170, + 1555092419, + 2103200507, + 568212685, + 726567164 + ] + }, + { + "data": [ + 811207331, + 1566057452, + 346845733, + 1405783110, + 296074182, + 686180472, + 1562194090, + 1331754094 + ] + }, + { + "data": [ + 1195458345, + 1015303239, + 1769326913, + 1798476475, + 1959426322, + 263548056, + 1086173773, + 616986172 + ] + }, + { + "data": [ + 1679743849, + 267745726, + 813229082, + 1802821399, + 1106957379, + 681723311, + 38255328, + 119212296 + ] + }, + { + "data": [ + 538262759, + 561853307, + 1220138601, + 648920532, + 96368560, + 1848614699, + 564258293, + 877652518 + ] + }, + { + "data": [ + 236889586, + 1945633712, + 888366492, + 1363228903, + 1535081845, + 1843716973, + 870982648, + 2828223 + ] + }, + { + "data": [ + 1813717520, + 157322480, + 353732586, + 1058663683, + 767198951, + 185375665, + 1574055980, + 434808322 + ] + }, + { + "data": [ + 379825016, + 1815775610, + 718153065, + 878888419, + 2004655473, + 329280888, + 1716255418, + 2005381073 + ] + }, + { + "data": [ + 1590446408, + 1173249277, + 2092549673, + 208887188, + 912239485, + 796567703, + 274938304, + 390283874 + ] + }, + { + "data": [ + 779263010, + 747574741, + 1434583711, + 1620835829, + 1551673235, + 1284998639, + 679093843, + 1406669023 + ] + }, + { + "data": [ + 1291148586, + 1081265798, + 1526996412, + 391781492, + 1711281276, + 1313014433, + 1384242624, + 623027609 + ] + }, + { + "data": [ + 953015683, + 934645423, + 1771313714, + 470438654, + 1645632988, + 1239732071, + 688694286, + 1693593789 + ] + }, + { + "data": [ + 1677902343, + 45276613, + 2103891579, + 1112027086, + 161866262, + 1434591076, + 1951598120, + 772846762 + ] + }, + { + "data": [ + 551705762, + 1871931766, + 1065697665, + 283086151, + 2053411512, + 1094840383, + 1766312832, + 256750162 + ] + }, + { + "data": [ + 429680689, + 125824827, + 1965715718, + 2057352154, + 1776082615, + 2118510694, + 176499827, + 1212838505 + ] + }, + { + "data": [ + 1556722792, + 1298468122, + 657266497, + 1348176792, + 97032780, + 432903656, + 1713460397, + 236087742 + ] + }, + { + "data": [ + 268618238, + 781464872, + 1629401850, + 2084984500, + 1651362468, + 871068115, + 1722302961, + 712459750 + ] + }, + { + "data": [ + 1361188658, + 539241318, + 1966654298, + 1775313608, + 143728868, + 1546419295, + 665328088, + 2041591993 + ] + }, + { + "data": [ + 1660687505, + 535693535, + 1188238224, + 1910372027, + 441895189, + 208597526, + 1637375248, + 1439508567 + ] + }, + { + "data": [ + 1139697001, + 555025515, + 1728548969, + 1245647761, + 1604041527, + 1752808772, + 1419902779, + 1640507729 + ] + }, + { + "data": [ + 1960240298, + 666218643, + 1280441627, + 1940051826, + 1775703419, + 598652016, + 140095253, + 829013884 + ] + }, + { + "data": [ + 303840967, + 363183783, + 2079196084, + 1077588941, + 1843884294, + 229585661, + 84469314, + 1923645935 + ] + }, + { + "data": [ + 1487940169, + 725658218, + 1422188831, + 2055497525, + 1396855667, + 456348791, + 1027525060, + 1026406513 + ] + }, + { + "data": [ + 1435497940, + 276128380, + 1036933776, + 678450869, + 1197285788, + 816650348, + 240096989, + 898816825 + ] + }, + { + "data": [ + 248028907, + 940423932, + 2017860464, + 1112538086, + 866251675, + 135676603, + 1729849157, + 73108520 + ] + } + ] } - ] + } } }, "_info": { - "hash": "0x85747e3680f0686a8ee60ad657c0ea06878219df31e568b1d6ae6c58e5007fe7", + "hash": "0x879f5976fdea34463877eebb31b5f5c7c966720d6a103273499e11d7dec42c05", "comment": "`leanSpec` generated test", "testId": "tests/consensus/devnet/verify_signatures/test_valid_signatures.py::test_proposer_and_attester_signatures[fork_Devnet]", - "description": "Test valid proposer and attester signatures in SignedBlockWithAttestation.\n\n Scenario\n --------\n - Single block at slot 1\n - 3 validators in the genesis state\n - 2 additional attestations from validators 0 and 2 (in addition to proposer)\n - Verifies that all signatures are generated correctly\n\n Expected Behavior\n -----------------\n 1. Proposer's signature in SignedBlockWithAttestation can be verified against\n the validator's pubkey in the state\n 2. Attester's signatures in SignedBlockWithAttestation can be verified against\n the validator's pubkey in the state\n\n Why This Matters\n ----------------\n This test verifies multi-validator signature scenarios:\n - Multiple XMSS keys are generated for different validators\n - Attestations from non-proposer validators are correctly verified\n - Signature aggregation works with multiple attestations (signature positions are correct)", + "description": "Test valid proposer and attester signatures in SignedBlockWithAttestation.\n\nScenario\n--------\n- Single block at slot 1\n- 3 validators in the genesis state\n- 2 additional attestations from validators 0 and 2 (in addition to proposer)\n- Verifies that all signatures are generated correctly\n\nExpected Behavior\n-----------------\n1. Proposer's signature in SignedBlockWithAttestation can be verified against\n the validator's pubkey in the state\n2. Attester's signatures in SignedBlockWithAttestation can be verified against\n the validator's pubkey in the state\n\nWhy This Matters\n----------------\nThis test verifies multi-validator signature scenarios:\n- Multiple XMSS keys are generated for different validators\n- Attestations from non-proposer validators are correctly verified\n- Signature aggregation works with multiple attestations (signature positions are correct)", "fixtureFormat": "verify_signatures_test" } } diff --git a/lean_client/tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json b/lean_client/tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json index 96dc2bb..14fd63e 100644 --- a/lean_client/tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json +++ b/lean_client/tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json @@ -1,6 +1,7 @@ { "tests/consensus/devnet/verify_signatures/test_valid_signatures.py::test_proposer_signature[fork_Devnet][fork_Devnet-verify_signatures_test]": { "network": "Devnet", + "leanEnv": "prod", "anchorState": { "config": { "genesisTime": 0 @@ -30,11 +31,11 @@ "validators": { "data": [ { - "pubkey": "0xb40ec9783b56572d99965130ad1cec4f066df43a0fd89d22d420535c62b52711cdaf7f5f696c6b1e4df66a6bab48bb51abcdd77e", + "pubkey": "0x2c7aa65d0b45c01e3ba7155997d3001613775f1e1b85fd1ab65dcd72c65e105f2552c43a311a290af56fa0262dfebe10745ea837", "index": 0 }, { - "pubkey": "0xc5ed4b6fa46e83281e29201b878ad52b4a9b8a2f3c9d8017cfd9b81e8097e1372a097e62be37194c39e2f36f76e8906d2e818238", + "pubkey": "0x67047f6113b87f6aea3a0e6c108a8d7dcc4b5e479dda2a1cb79397705340bd6227f1ea63ee39c91fff4599375b749425ae65922e", "index": 1 } ] @@ -51,8 +52,8 @@ "block": { "slot": 1, "proposerIndex": 1, - "parentRoot": "0x2a63e3011f0b83fd7134abc2829e011dd57789ea5e44c1766d803ed877d391b7", - "stateRoot": "0xe4c2b970f38f0cd022017f58e06cff629700138c939cd5ddb19906c0de2f1c46", + "parentRoot": "0x25f6beaf778b5d1cad9d33b9bc39f7d2ed521cb3f03d8b086b6af8408617635a", + "stateRoot": "0xf8947796ac01c5ab946e51321a263b897421f03497964007001ca86e15ee4c8d", "body": { "attestations": { "data": [] @@ -64,197 +65,1206 @@ "data": { "slot": 1, "head": { - "root": "0xc185996796d67eb0a7d76c91209a0fee3f0e168ec2f571dafba2cc27377081a9", + "root": "0x16e2e465a92b9d884438754ce66e3b04e25bce25bd76ab182f8dba7502a4aca6", "slot": 1 }, "target": { - "root": "0xc185996796d67eb0a7d76c91209a0fee3f0e168ec2f571dafba2cc27377081a9", + "root": "0x16e2e465a92b9d884438754ce66e3b04e25bce25bd76ab182f8dba7502a4aca6", "slot": 1 }, "source": { - "root": "0x2a63e3011f0b83fd7134abc2829e011dd57789ea5e44c1766d803ed877d391b7", + "root": "0x25f6beaf778b5d1cad9d33b9bc39f7d2ed521cb3f03d8b086b6af8408617635a", "slot": 0 } } } }, "signature": { - "data": [ - { - "path": { - "siblings": { - "data": [ - { - "data": [ - 1331122823, - 1643080471, - 525470619, - 2031212805, - 2098748101, - 1243499988, - 4104831, - 2063254342 - ] - }, - { - "data": [ - 744813374, - 813288082, - 613270960, - 679006507, - 533419743, - 993872253, - 286847881, - 1451300746 - ] - }, - { - "data": [ - 11079834, - 1639687748, - 1929217599, - 202276490, - 40122498, - 1566411931, - 675580467, - 904231754 - ] - }, - { - "data": [ - 2020782635, - 1574539412, - 1138761573, - 422111916, - 1389054530, - 1510501223, - 197381584, - 402775535 - ] - }, - { - "data": [ - 264438681, - 1087279288, - 263416095, - 1860660250, - 1429146526, - 2094709316, - 867692041, - 1662763959 - ] - }, - { - "data": [ - 544815286, - 311733778, - 799237190, - 2102805408, - 1661418854, - 1157854114, - 1855929130, - 744137327 - ] - }, - { - "data": [ - 1462886854, - 1889836598, - 2029637687, - 53625032, - 673267735, - 2047374486, - 49833992, - 1921530767 - ] - }, - { - "data": [ - 110758107, - 12788259, - 1920648715, - 413804969, - 419452419, - 1819780529, - 1147494825, - 526331496 - ] - } - ] - } - }, - "rho": { - "data": [ - 822936441, - 1691280078, - 133155482, - 639707584, - 188230399, - 56199352, - 1344091725 - ] - }, - "hashes": { + "attestationSignatures": { + "data": [] + }, + "proposerSignature": { + "path": { + "siblings": { "data": [ { "data": [ - 1313453765, - 509566156, - 1191425365, - 1394696109, - 578431309, - 849344093, - 1900015578, - 1268473644 + 1291233891, + 901611691, + 1515447763, + 518859210, + 691456689, + 1723616255, + 1740655893, + 1397350123 + ] + }, + { + "data": [ + 1456518562, + 484811598, + 2010092021, + 282492124, + 1770180907, + 1769233778, + 494579282, + 1699827616 + ] + }, + { + "data": [ + 704838371, + 2081776305, + 559183133, + 1733361779, + 263725425, + 344472513, + 2086779080, + 1240527530 + ] + }, + { + "data": [ + 1039415328, + 308461932, + 2043838459, + 611597666, + 1580071319, + 1516124162, + 698977291, + 1585930624 + ] + }, + { + "data": [ + 250662127, + 1008190943, + 983708486, + 1247986374, + 1886580775, + 1647509743, + 1550488627, + 1260597451 + ] + }, + { + "data": [ + 416883050, + 188953242, + 1182024076, + 59202244, + 1978179518, + 1739410190, + 679526947, + 861617775 + ] + }, + { + "data": [ + 1378484229, + 241452936, + 163273125, + 436861107, + 388667496, + 1797577532, + 443869040, + 906552846 + ] + }, + { + "data": [ + 799696786, + 1508418299, + 1856736097, + 1058842462, + 269359710, + 1099230447, + 749521578, + 520853695 + ] + }, + { + "data": [ + 439422050, + 1286915872, + 1358195170, + 840970194, + 1786302274, + 893515818, + 370457729, + 1427474800 + ] + }, + { + "data": [ + 2007504140, + 418866321, + 198684106, + 1996996870, + 1261455552, + 2118739919, + 56436890, + 1806594952 + ] + }, + { + "data": [ + 128324476, + 61519552, + 113352202, + 1839954511, + 2112962791, + 1831734346, + 1400107873, + 849776052 + ] + }, + { + "data": [ + 694706906, + 2057189854, + 1419202856, + 2020454400, + 1916412209, + 304600413, + 1119212112, + 988476852 + ] + }, + { + "data": [ + 1607491401, + 916185160, + 150839959, + 1915584536, + 1192630676, + 166752571, + 44618577, + 1961530862 + ] + }, + { + "data": [ + 1949287225, + 1766709295, + 928506169, + 833212136, + 35771750, + 71835570, + 1852857681, + 1205452729 ] }, { "data": [ - 1366485659, - 1605565996, - 671158305, - 1622538715, - 394010590, - 1825233468, - 1018705001, - 1071542400 + 1644443152, + 1520132256, + 1370044265, + 851862297, + 261020286, + 1001533477, + 571576626, + 907308311 ] }, { "data": [ - 1186545818, - 1781761501, - 1739225478, - 720736896, - 819060565, - 601088943, - 1836159403, - 4774455 + 1557846841, + 210200770, + 685212717, + 1586976910, + 463743886, + 395493034, + 1562290362, + 1016157604 ] }, { "data": [ - 462810434, - 24773107, - 467766012, - 716711919, - 1288811897, - 436514417, - 852671571, - 2110518257 + 208831676, + 1180089898, + 2064964824, + 1411007716, + 1673605982, + 1643551528, + 1539845891, + 704493341 + ] + }, + { + "data": [ + 895079925, + 877096130, + 2081347331, + 124656629, + 1179296144, + 1491205760, + 356412314, + 926452265 + ] + }, + { + "data": [ + 1422286144, + 984088526, + 1135304910, + 162305405, + 1064769342, + 1110991338, + 104215457, + 1422827345 + ] + }, + { + "data": [ + 912789203, + 2010420420, + 429286304, + 295855493, + 2084240709, + 1193367228, + 1021205972, + 560375846 + ] + }, + { + "data": [ + 489627247, + 396093595, + 475714912, + 573904495, + 382358549, + 668148792, + 1416579671, + 1444313453 + ] + }, + { + "data": [ + 1345967333, + 723445075, + 2048740831, + 153155071, + 10838758, + 1236457738, + 18985351, + 1138484833 + ] + }, + { + "data": [ + 993666071, + 473860152, + 482974488, + 1244638895, + 1287107597, + 1492708811, + 1976127099, + 653628523 + ] + }, + { + "data": [ + 791701066, + 885184677, + 106955182, + 1572481724, + 1456627534, + 1452427937, + 490533016, + 991293345 + ] + }, + { + "data": [ + 578639661, + 1631171923, + 268843612, + 1788996364, + 1746080381, + 1046103170, + 455298193, + 562115509 + ] + }, + { + "data": [ + 235948816, + 1018300141, + 1002498336, + 201831066, + 1124789148, + 1994905284, + 561014981, + 1257286951 + ] + }, + { + "data": [ + 1045385620, + 1058192257, + 938515492, + 573527403, + 989948080, + 1342850602, + 1832637791, + 358929324 + ] + }, + { + "data": [ + 1902152809, + 252599905, + 623219565, + 758434560, + 640896011, + 207032991, + 835792870, + 1665795896 + ] + }, + { + "data": [ + 723715685, + 587576367, + 853971724, + 1144944495, + 873175376, + 498689849, + 43297292, + 819091873 + ] + }, + { + "data": [ + 280016656, + 437742428, + 255947140, + 349343920, + 1615039346, + 1488983802, + 1389523623, + 1912297556 + ] + }, + { + "data": [ + 901881368, + 1526942608, + 852049680, + 731288118, + 661508501, + 2010590000, + 868332352, + 1726397935 + ] + }, + { + "data": [ + 1349685696, + 2130099561, + 1462674991, + 1479393751, + 1013840091, + 1746802826, + 422993280, + 544780634 ] } ] } + }, + "rho": { + "data": [ + 716203521, + 581420617, + 1286685526, + 1194695558, + 1641401137, + 1997614743, + 496514183 + ] + }, + "hashes": { + "data": [ + { + "data": [ + 1007398320, + 1251646982, + 1612967266, + 537381478, + 93386731, + 1083280595, + 1721356609, + 1286371566 + ] + }, + { + "data": [ + 170535041, + 1162715015, + 694935546, + 306154186, + 1318628375, + 1972574425, + 1164163541, + 93198021 + ] + }, + { + "data": [ + 975961833, + 531824562, + 2035811416, + 1364843342, + 322668550, + 458809832, + 349604618, + 2092803528 + ] + }, + { + "data": [ + 654742765, + 1159278164, + 1972996313, + 523872121, + 1805821337, + 124485604, + 1193056330, + 807117735 + ] + }, + { + "data": [ + 919568569, + 2007737629, + 1957321079, + 1976407600, + 533410046, + 2111488436, + 1620339375, + 801239907 + ] + }, + { + "data": [ + 1248809976, + 857619471, + 1558705607, + 1000070635, + 536413566, + 1646494315, + 1742462035, + 361896064 + ] + }, + { + "data": [ + 1984415707, + 2120523866, + 460541868, + 1401766703, + 191855557, + 62277793, + 839423381, + 1933336793 + ] + }, + { + "data": [ + 720993176, + 565557239, + 144294658, + 1965029513, + 1320601105, + 1033475156, + 924580775, + 1891983182 + ] + }, + { + "data": [ + 2107882818, + 42805544, + 1038376944, + 1485088008, + 413502243, + 595063755, + 208296301, + 137522358 + ] + }, + { + "data": [ + 1584322090, + 1505637739, + 1431439512, + 2049043333, + 2031336532, + 1994220632, + 535798250, + 1003107923 + ] + }, + { + "data": [ + 536723909, + 1389453682, + 407278690, + 1778319839, + 1777727737, + 1948491210, + 1489215087, + 94425788 + ] + }, + { + "data": [ + 1472227604, + 777167222, + 1014016292, + 2108428626, + 1572854930, + 936945519, + 1087148360, + 239564935 + ] + }, + { + "data": [ + 1895618555, + 1937165542, + 1924755874, + 1626462217, + 1247425034, + 1370792545, + 10993310, + 259827571 + ] + }, + { + "data": [ + 1633133337, + 1030126208, + 1634656506, + 1714432182, + 923722773, + 1592896262, + 1045605719, + 1004996167 + ] + }, + { + "data": [ + 1142655319, + 976140656, + 1227333160, + 2129498013, + 867861598, + 1790717609, + 866871241, + 1774362003 + ] + }, + { + "data": [ + 310323031, + 1437920355, + 878272104, + 722971220, + 1015159963, + 409582600, + 512066076, + 854680398 + ] + }, + { + "data": [ + 562156248, + 407660686, + 123830526, + 1475625115, + 2009710949, + 611229674, + 227976023, + 1979398321 + ] + }, + { + "data": [ + 1317260158, + 1861218639, + 1224575160, + 421550610, + 765270751, + 1827053147, + 289080869, + 538182924 + ] + }, + { + "data": [ + 1373100955, + 1325801240, + 242486397, + 1594220983, + 1224055938, + 1039685355, + 1369882156, + 2105201870 + ] + }, + { + "data": [ + 882140143, + 972100438, + 1911957230, + 1132707040, + 1927731179, + 1221461913, + 7060907, + 1965761976 + ] + }, + { + "data": [ + 1512587243, + 1963077188, + 1954992331, + 1545360150, + 1178760012, + 1515958126, + 705452917, + 2114456876 + ] + }, + { + "data": [ + 2049583212, + 1094272227, + 1039456282, + 787530139, + 1640279372, + 1330559514, + 240146549, + 1313599913 + ] + }, + { + "data": [ + 1405173867, + 1816274123, + 914189354, + 1390194868, + 1875873291, + 729884524, + 1391291848, + 712390226 + ] + }, + { + "data": [ + 598163318, + 669166894, + 2095183582, + 2020485494, + 1353088122, + 288048132, + 219781410, + 2002759719 + ] + }, + { + "data": [ + 545406936, + 53426137, + 424114470, + 1111280438, + 618160273, + 1802384583, + 1667812411, + 783526014 + ] + }, + { + "data": [ + 880153551, + 1844784642, + 958326447, + 604503257, + 1004388263, + 410341879, + 1891463524, + 1293918805 + ] + }, + { + "data": [ + 501558010, + 740311244, + 1050231400, + 540493697, + 1939025889, + 865101441, + 450874438, + 556623367 + ] + }, + { + "data": [ + 14617393, + 1741137626, + 1232402672, + 1297835855, + 180473396, + 296331666, + 678243236, + 1349472775 + ] + }, + { + "data": [ + 1555733412, + 183459902, + 1845209457, + 294206894, + 487746799, + 1581300684, + 1098936144, + 1463828258 + ] + }, + { + "data": [ + 359145510, + 91313123, + 46174230, + 237526521, + 2072622391, + 1873031895, + 797080303, + 1306795272 + ] + }, + { + "data": [ + 150143807, + 1240854373, + 2107166892, + 1239041875, + 199965884, + 143519120, + 877927432, + 463537849 + ] + }, + { + "data": [ + 1279960983, + 1843209453, + 1821049971, + 1629719253, + 1678547126, + 1467417183, + 2126155977, + 29064399 + ] + }, + { + "data": [ + 2087574544, + 1055748285, + 1878614231, + 639879396, + 1324171225, + 673620585, + 1341934876, + 558856819 + ] + }, + { + "data": [ + 1584783469, + 1012855306, + 401348507, + 2091865749, + 1821226690, + 741147898, + 257249483, + 1263361762 + ] + }, + { + "data": [ + 1067822535, + 1891280943, + 813430117, + 1276106738, + 1193107083, + 1477156918, + 1539220620, + 96504038 + ] + }, + { + "data": [ + 2020927885, + 18361940, + 653582762, + 1686120847, + 1597265575, + 28912714, + 443462147, + 870096418 + ] + }, + { + "data": [ + 1393564690, + 2099610787, + 1433569094, + 1415341075, + 667347082, + 534542891, + 2086215618, + 311924734 + ] + }, + { + "data": [ + 1605236949, + 127566776, + 21238712, + 434461941, + 1022175801, + 1240317127, + 1122138289, + 1008747176 + ] + }, + { + "data": [ + 855427493, + 1207280363, + 122948393, + 1858476858, + 717680189, + 297650565, + 1852129145, + 498572861 + ] + }, + { + "data": [ + 123969828, + 595339923, + 285763600, + 1913417170, + 1555092419, + 2103200507, + 568212685, + 726567164 + ] + }, + { + "data": [ + 479007558, + 124272114, + 1475575201, + 470022555, + 470436106, + 1311385231, + 790477535, + 123444182 + ] + }, + { + "data": [ + 1851254867, + 1287818641, + 1976227070, + 2000161771, + 366470125, + 1781542280, + 130581790, + 1069901828 + ] + }, + { + "data": [ + 1246563804, + 1413964901, + 928709195, + 135099607, + 1933313698, + 942190559, + 1583299962, + 405273537 + ] + }, + { + "data": [ + 15025568, + 798031475, + 1319692199, + 626923173, + 639980038, + 1042881228, + 1087868684, + 1534522887 + ] + }, + { + "data": [ + 236889586, + 1945633712, + 888366492, + 1363228903, + 1535081845, + 1843716973, + 870982648, + 2828223 + ] + }, + { + "data": [ + 668744770, + 1762680521, + 777619164, + 842438923, + 2088233233, + 1413163967, + 2016710389, + 1505732360 + ] + }, + { + "data": [ + 428137177, + 180423701, + 422464102, + 2076710433, + 1729838960, + 535452591, + 908577683, + 35856264 + ] + }, + { + "data": [ + 1967540513, + 1519776050, + 2036007845, + 893366942, + 2089822704, + 856708567, + 52673874, + 1680933072 + ] + }, + { + "data": [ + 981208312, + 1084818190, + 677102979, + 2063847281, + 1364366814, + 1457677810, + 1899058168, + 1563619590 + ] + }, + { + "data": [ + 1291148586, + 1081265798, + 1526996412, + 391781492, + 1711281276, + 1313014433, + 1384242624, + 623027609 + ] + }, + { + "data": [ + 200567419, + 388962948, + 476521573, + 687024916, + 1833415520, + 1730904880, + 443398259, + 436157135 + ] + }, + { + "data": [ + 707230432, + 1154831848, + 281037294, + 1768624406, + 430016495, + 1598391594, + 533135163, + 1005066409 + ] + }, + { + "data": [ + 1575751610, + 754993504, + 976076502, + 28864881, + 203441435, + 1815755927, + 2032423475, + 1425030338 + ] + }, + { + "data": [ + 1562339170, + 1308262206, + 27630180, + 395740765, + 2013010851, + 1820364392, + 1927685629, + 104952625 + ] + }, + { + "data": [ + 1337781915, + 946317826, + 736367261, + 1158536580, + 1619326108, + 291263633, + 543599065, + 2111323116 + ] + }, + { + "data": [ + 252041760, + 1817414500, + 1222652907, + 729393086, + 1201147464, + 204350039, + 1423174574, + 766473914 + ] + }, + { + "data": [ + 35290624, + 543962937, + 163333824, + 329618781, + 896461446, + 346705117, + 690957194, + 1923687483 + ] + }, + { + "data": [ + 2113056063, + 1632595436, + 1471738161, + 732872543, + 1177191142, + 1142007075, + 1194993142, + 1559467243 + ] + }, + { + "data": [ + 1982272307, + 992599898, + 912060509, + 756026196, + 1317365254, + 900172012, + 1887616520, + 86671560 + ] + }, + { + "data": [ + 2046630080, + 888883591, + 84025767, + 77874323, + 1407868461, + 443546501, + 203936347, + 412038982 + ] + }, + { + "data": [ + 1135271242, + 490412616, + 1384616381, + 596851392, + 371643044, + 98437837, + 390163320, + 374739258 + ] + }, + { + "data": [ + 1465916835, + 1762678201, + 1111080241, + 460605314, + 1685336972, + 120785414, + 1657833535, + 1990363046 + ] + }, + { + "data": [ + 1622723517, + 1868134833, + 1357640922, + 2093289686, + 1317449817, + 1456398689, + 865029087, + 990587183 + ] + }, + { + "data": [ + 989042468, + 1523246160, + 349574826, + 1340646482, + 1579176982, + 725957665, + 514279194, + 572149168 + ] + } + ] } - ] + } } }, "_info": { - "hash": "0x5f7869a9585bd146ac848c4df3ef584b6c52c70fb6a3f6bc0ca6f91cbc6c0d11", + "hash": "0xfd453261ae39cec510a775702a42457e8aefbf018d4e32cad082b20bede7a8ae", "comment": "`leanSpec` generated test", "testId": "tests/consensus/devnet/verify_signatures/test_valid_signatures.py::test_proposer_signature[fork_Devnet]", - "description": "Test valid proposer signature in SignedBlockWithAttestation.\n\n Scenario\n --------\n - Single block at slot 1\n - No additional attestations (only proposer attestation)\n\n Expected Behavior\n -----------------\n 1. Proposer's signature in SignedBlockWithAttestation can be verified against\n the validator's pubkey in the state\n\n Why This Matters\n ----------------\n This is the most basic signature generation test. It verifies:\n - XMSS key generation works\n - Signature aggregation includes proposer signature", + "description": "Test valid proposer signature in SignedBlockWithAttestation.\n\nScenario\n--------\n- Single block at slot 1\n- No additional attestations (only proposer attestation)\n\nExpected Behavior\n-----------------\n1. Proposer's signature in SignedBlockWithAttestation can be verified against\n the validator's pubkey in the state\n\nWhy This Matters\n----------------\nThis is the most basic signature generation test. It verifies:\n- XMSS key generation works\n- Signature aggregation includes proposer signature", "fixtureFormat": "verify_signatures_test" } } diff --git a/lean_client/validator/Cargo.toml b/lean_client/validator/Cargo.toml index 81ad309..f38a6d6 100644 --- a/lean_client/validator/Cargo.toml +++ b/lean_client/validator/Cargo.toml @@ -9,7 +9,6 @@ xmss-signing = ["leansig"] [dependencies] env-config = { path = "../env-config", default-features = false } -serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.9" containers = { path = "../containers" } fork-choice = { path = "../fork_choice" } diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index e1dfd2d..f337066 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -258,7 +258,7 @@ impl ValidatorService { let attestation_signatures = { let mut list = ssz::PersistentList::default(); for proof in signatures { - list.push(proof.proof_data) + list.push(proof) .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; } list From 9b453ba54eb7bf1e7ac34f86beb030e1fe6308a4 Mon Sep 17 00:00:00 2001 From: Nojus Sandovas Date: Fri, 23 Jan 2026 00:10:54 +0200 Subject: [PATCH 24/24] Changes based on PR #50 comments --- lean_client/Cargo.lock | 57 ++--------------------- lean_client/containers/src/attestation.rs | 45 +++++++++++------- lean_client/containers/src/state.rs | 3 +- 3 files changed, 33 insertions(+), 72 deletions(-) diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index 4173794..1b55950 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -668,15 +668,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -923,6 +914,8 @@ dependencies = [ name = "containers" version = "0.1.0" dependencies = [ + "alloy-primitives", + "anyhow", "env-config", "hex", "lean-multisig", @@ -1272,7 +1265,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", "crypto-common", "subtle", @@ -1669,12 +1662,7 @@ version = "0.1.0" dependencies = [ "containers", "env-config", - "serde", - "serde_json", "ssz", - "ssz_derive", - "ssz_rs", - "typenum", ] [[package]] @@ -5083,19 +5071,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.9" @@ -5313,31 +5288,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "ssz_rs" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057291e5631f280978fa9c8009390663ca4613359fc1318e36a8c24c392f6d1f" -dependencies = [ - "bitvec", - "hex", - "num-bigint", - "serde", - "sha2 0.9.9", - "ssz_rs_derive", -] - -[[package]] -name = "ssz_rs_derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07d54c4d01a1713eb363b55ba51595da15f6f1211435b71466460da022aa140" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5968,7 +5918,6 @@ dependencies = [ "env-config", "fork-choice", "leansig", - "serde", "serde_yaml", "tracing", "typenum", diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index eebffce..ee95ec9 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -1,12 +1,15 @@ use crate::{Checkpoint, Slot, Uint64}; +use leansig::serialization::Serializable; use serde::{Deserialize, Serialize}; use ssz::BitList; use ssz::ByteVector; +use ssz::{SszHash, H256}; use ssz_derive::Ssz; +use std::collections::HashSet; use typenum::{Prod, Sum, U100, U1024, U12, U31}; // Type-level number for 1 MiB (1048576 = 1024 * 1024) -pub type U1048576 = Prod; +type U1048576 = Prod; pub type U3100 = Prod; @@ -46,8 +49,10 @@ pub struct MultisigAggregatedSignature( impl MultisigAggregatedSignature { /// Create a new MultisigAggregatedSignature from proof bytes. - pub fn new(proof: Vec) -> Self { - Self(ssz::ByteList::try_from(proof).expect("proof exceeds 1 MiB limit")) + pub fn new(proof: Vec) -> Result { + ssz::ByteList::try_from(proof) + .map(Self) + .map_err(|_| AggregationError::AggregationFailed) } /// Get the proof bytes. @@ -89,7 +94,7 @@ impl MultisigAggregatedSignature { lean_multisig::xmss_aggregate_signatures(public_keys, signatures, message, epoch) .map_err(|_| AggregationError::AggregationFailed)?; - Ok(Self::new(proof_bytes)) + Self::new(proof_bytes) } /// Verify the aggregated signature proof against the given public keys and message. @@ -132,16 +137,24 @@ impl MultisigAggregatedSignature { message: &[u8; 32], epoch: u64, ) -> Result<(), AggregationError> { - // NOTE: This stub matches Python leanSpec behavior (test_mode=True). - // Python also uses test_mode=True with TODO: "Remove test_mode once leanVM - // supports correct signature encoding." - // Once leanVM/lean-multisig supports proper signature encoding: - // 1. Extract public keys from validators - // 2. Convert message bytes to field element format - // 3. Call lean_multisig::xmss_verify_aggregated_signatures - let _ = (validators, message, epoch); - - Ok(()) + // Extract public keys from validators + let mut public_keys = Vec::new(); + for validator in validators { + // Convert PublicKey to lean_multisig::XmssPublicKey + let lean_sig_pk = validator.pubkey.as_lean_sig() + .map_err(|_| AggregationError::VerificationFailed)?; + let pk_bytes = lean_sig_pk.to_bytes(); + // TODO: Implement proper conversion from PublicKey bytes to lean_multisig::XmssPublicKey + // Once lean-multisig API is clarified, convert pk_bytes to XmssPublicKey + todo!("Convert PublicKey to lean_multisig::XmssPublicKey and implement message field conversion"); + } + + // Convert 32-byte message to 8 field elements + // TODO: Implement proper conversion from 32 bytes to 8 field elements + let message_fields = todo!("Convert 32-byte message to [lean_multisig::F; 8]"); + + // Call verify with extracted keys and converted message + self.verify(&public_keys, message_fields, epoch) } } @@ -348,9 +361,7 @@ impl AggregatedAttestation { /// Returns true if the provided list contains duplicate AttestationData. pub fn has_duplicate_data(attestations: &AggregatedAttestations) -> bool { - use ssz::SszHash; - use std::collections::HashSet; - let mut seen: HashSet = HashSet::new(); + let mut seen: HashSet = HashSet::new(); for attestation in attestations { let root = attestation.data.hash_tree_root(); if !seen.insert(root) { diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index c894d8c..5bb8a9e 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -788,7 +788,8 @@ impl State { // Create proof placeholder (matches Python test_mode behavior) // TODO: Call actual aggregation when lean-multisig supports proper encoding - let proof_data = crate::MultisigAggregatedSignature::new(Vec::new()); + let proof_data = crate::MultisigAggregatedSignature::new(Vec::new()) + .expect("Empty proof should always be valid"); let proof = crate::AggregatedSignatureProof::new(participants.clone(), proof_data); results.push((