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 011bb4e..29e8ecd 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -5,12 +5,16 @@ edition = "2021" [features] xmss-verify = ["leansig"] +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 6ad0f56..b4f5c9a 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; @@ -19,13 +20,66 @@ 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; + +#[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,29 +111,80 @@ 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). 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. - /// + /// /// 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() + } +} + +/// 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 { + 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 + } +} + /// 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..0acf1b2 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -1,9 +1,13 @@ -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, SszHash}; +use typenum::U4096; +use crate::attestation::{AggregatedAttestations, AttestationSignatures}; +use crate::validator::BlsPublicKey; /// The body of a block, containing payload data. /// @@ -11,6 +15,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: AggregatedAttestations, + #[cfg(feature = "devnet1")] #[serde(with = "crate::serde_helpers")] pub attestations: Attestations, } @@ -45,6 +52,12 @@ pub struct BlockWithAttestation { pub proposer_attestation: Attestation, } +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Ssz, 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 +67,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, } @@ -112,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; @@ -123,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 { @@ -133,7 +150,7 @@ impl SignedBlockWithAttestation { } i += 1; } - + // Append proposer attestation all_attestations.push(self.message.proposer_attestation.clone()); @@ -155,25 +172,14 @@ impl SignedBlockWithAttestation { // The ordering must be preserved: // 1. Block body attestations, // 2. The proposer attestation. - assert!( - signatures_vec.len() == all_attestations.len(), + 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()) { @@ -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/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..01604e5 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,12 @@ 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::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 { @@ -197,79 +200,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 +304,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::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 4eb0ffd..d6c7cf8 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -1,13 +1,15 @@ 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, 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; +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 @@ -47,7 +49,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 +69,6 @@ impl State { validator_list.push(v).expect("Failed to add validator"); } - Self { config: Config { genesis_time: genesis_time.0, @@ -133,18 +137,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 @@ -206,7 +199,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 +227,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) } @@ -295,11 +296,13 @@ impl State { pub fn process_block(&self, block: &Block) -> Result { let state = self.process_block_header(block)?; - let state_after_ops = state.process_attestations(&block.body.attestations); - - // State root validation is handled by state_transition_with_validation when needed + + #[cfg(feature = "devnet2")] + 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 { @@ -314,7 +317,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")); @@ -389,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 { @@ -409,127 +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(); - // 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 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)); + } - let count = votes.iter().filter(|&&v| v).count(); - if 3 * count >= 2 * num_validators as usize { - latest_justified = vote.target; + 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, + ); + } - // 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; + self.finalize_attestation_processing(justifications, latest_justified, latest_finalized, justified_slots_working) + } - justifications.remove(&target_root); + /// 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; + } - 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 !justifications.contains_key(&target_root) { + justifications.insert(target_root, vec![false; self.validator_count()]); + } - if is_finalizable { - latest_finalized = vote.source; - } - } + 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 new_state = self.clone().with_justifications(justifications); + 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(); + + 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 } @@ -554,6 +570,7 @@ impl State { /// # Returns /// /// Tuple of (Block, post-State, collected attestations, signatures) + #[cfg(feature = "devnet1")] pub fn build_block( &self, slot: Slot, @@ -562,10 +579,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 +598,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 +685,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<&[SignedAttestation]>, + _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)] @@ -682,7 +716,7 @@ mod tests { config: st.config.clone(), ..st.clone() } - .is_proposer(ValidatorIndex(0))); + .is_proposer(ValidatorIndex(0))); } #[test] @@ -726,14 +760,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 +778,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 +825,7 @@ mod tests { }, }, }; - + let result = genesis_state.build_block( Slot(1), ValidatorIndex(1), @@ -792,45 +834,49 @@ 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] + #[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, - ); - + 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 +884,46 @@ 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] + #[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, - ); - + 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/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 9e7ef36..bf23138 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!( @@ -584,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())?; @@ -603,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/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 77c2dd5..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, BlockSignatures, 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 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(List::default), + 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: BlockSignatures::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 91edfa7..a18ac61 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -1,12 +1,13 @@ // 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, BlockSignatures + Attestation, Slot, }; use pretty_assertions::assert_eq; use rstest::fixture; +use ssz::PersistentList; #[path = "common.rs"] mod common; @@ -23,11 +24,13 @@ 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(); + let expected_state = state_after_header.process_attestations(&block.body.attestations); let block_with_correct_root = Block { @@ -43,7 +46,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); } @@ -53,11 +58,13 @@ 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(); + let expected_state = state_after_header.process_attestations(&block.body.attestations); let block_with_correct_root = Block { @@ -78,12 +85,14 @@ 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(); 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()); @@ -93,10 +102,48 @@ 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); 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(); + + 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), + ..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); +} 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..badc834 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/devnet2"] + [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 e2d230a..50bd240 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; @@ -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(); @@ -299,10 +300,11 @@ fn convert_test_anchor_block(test_block: &TestAnchorBlock) -> SignedBlockWithAtt block, proposer_attestation, }, - signature: BlockSignatures::default(), + 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(); @@ -329,7 +331,7 @@ fn convert_test_block(test_block_with_att: &TestBlockWithAttestation) -> SignedB block, proposer_attestation, }, - signature: BlockSignatures::default(), + signature: 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..0584a0e 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/devnet2"] + [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 396c8f7..d1c3e24 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -1,14 +1,15 @@ use clap::Parser; -use containers::ssz::SszHash; +use containers::block::BlockSignatures; +use containers::ssz::{PersistentList, SszHash}; use containers::{ - attestation::{Attestation, AttestationData, BlockSignatures}, + attestation::{Attestation, AttestationData}, block::{Block, BlockBody, BlockWithAttestation, SignedBlockWithAttestation}, checkpoint::Checkpoint, config::Config, 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, }, - signature: BlockSignatures::default(), + #[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..ab09109 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/devnet2"] [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() }