diff --git a/bin/ream/Cargo.toml b/bin/ream/Cargo.toml index 78751a76d..cce80f2c6 100644 --- a/bin/ream/Cargo.toml +++ b/bin/ream/Cargo.toml @@ -14,6 +14,11 @@ version.workspace = true name = "ream" path = "src/main.rs" +[features] +default = ["devnet1"] +devnet1 = [] +devnet2 = [] + [dependencies] alloy-primitives.workspace = true anyhow.workspace = true diff --git a/bin/ream/src/main.rs b/bin/ream/src/main.rs index 22e72f79c..53c06ad9d 100644 --- a/bin/ream/src/main.rs +++ b/bin/ream/src/main.rs @@ -35,8 +35,14 @@ use ream_chain_lean::{ messages::LeanChainServiceMessage, p2p_request::LeanP2PRequest, service::LeanChainService, }; use ream_checkpoint_sync::initialize_db_from_checkpoint; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::attestation::AggregatedAttestations; +#[cfg(feature = "devnet1")] +use ream_consensus_lean::attestation::Attestation; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::block::BlockSignatures; use ream_consensus_lean::{ - attestation::{Attestation, AttestationData}, + attestation::AttestationData, block::{BlockWithAttestation, SignedBlockWithAttestation}, checkpoint::Checkpoint, validator::Validator, @@ -61,6 +67,8 @@ use ream_p2p::{ }, network::lean::{LeanNetworkConfig, LeanNetworkService}, }; +#[cfg(feature = "devnet2")] +use ream_post_quantum_crypto::leansig::signature::Signature; use ream_post_quantum_crypto::leansig::{ private_key::PrivateKey as LeanSigPrivateKey, public_key::PublicKey, }; @@ -217,6 +225,7 @@ pub async fn run_lean_node(config: LeanNodeConfig, executor: ReamExecutor, ream_ SignedBlockWithAttestation { message: BlockWithAttestation { block: genesis_block, + #[cfg(feature = "devnet1")] proposer_attestation: Attestation { validator_id: 0, data: AttestationData { @@ -226,8 +235,24 @@ pub async fn run_lean_node(config: LeanNodeConfig, executor: ReamExecutor, ream_ source: Checkpoint::default(), }, }, + #[cfg(feature = "devnet2")] + proposer_attestation: AggregatedAttestations { + validator_id: 0, + data: AttestationData { + slot: 0, + head: Checkpoint::default(), + target: Checkpoint::default(), + source: Checkpoint::default(), + }, + }, }, + #[cfg(feature = "devnet1")] signature: VariableList::default(), + #[cfg(feature = "devnet2")] + signature: BlockSignatures { + attestation_signatures: VariableList::default(), + proposer_signature: Signature::blank(), + }, }, genesis_state, lean_db, diff --git a/crates/common/chain/lean/Cargo.toml b/crates/common/chain/lean/Cargo.toml index 4627477d6..9ee38835e 100644 --- a/crates/common/chain/lean/Cargo.toml +++ b/crates/common/chain/lean/Cargo.toml @@ -9,6 +9,11 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = ["devnet1"] +devnet1 = [] +devnet2 = [] + [dependencies] alloy-primitives.workspace = true anyhow.workspace = true diff --git a/crates/common/chain/lean/src/service.rs b/crates/common/chain/lean/src/service.rs index 10a2f8520..f3025f18f 100644 --- a/crates/common/chain/lean/src/service.rs +++ b/crates/common/chain/lean/src/service.rs @@ -163,6 +163,7 @@ impl LeanChainService { } LeanChainServiceMessage::ProcessAttestation { signed_attestation, need_gossip } => { if enabled!(Level::DEBUG) { + #[cfg(feature = "devnet1")] debug!( slot = signed_attestation.message.slot(), head = ?signed_attestation.message.head(), @@ -171,7 +172,17 @@ impl LeanChainService { "Processing attestation by Validator {}", signed_attestation.message.validator_id, ); + #[cfg(feature = "devnet2")] + debug!( + slot = signed_attestation.message.slot, + head = ?signed_attestation.message.head, + source = ?signed_attestation.message.source, + target = ?signed_attestation.message.target, + "Processing attestation by Validator {}", + signed_attestation.validator_id, + ); } else { + #[cfg(feature = "devnet1")] info!( slot = signed_attestation.message.slot(), source_slot = signed_attestation.message.source().slot, @@ -179,6 +190,15 @@ impl LeanChainService { "Processing attestation by Validator {}", signed_attestation.message.validator_id, ); + #[cfg(feature = "devnet2")] + debug!( + slot = signed_attestation.message.slot, + head = ?signed_attestation.message.head, + source = ?signed_attestation.message.source, + target = ?signed_attestation.message.target, + "Processing attestation by Validator {}", + signed_attestation.validator_id, + ); } if let Err(err) = self.handle_process_attestation(*signed_attestation.clone()).await { diff --git a/crates/common/consensus/lean/src/attestation.rs b/crates/common/consensus/lean/src/attestation.rs index dd9df000c..d8f533ac1 100644 --- a/crates/common/consensus/lean/src/attestation.rs +++ b/crates/common/consensus/lean/src/attestation.rs @@ -8,7 +8,7 @@ use tree_hash_derive::TreeHash; use crate::checkpoint::Checkpoint; /// Attestation content describing the validator's observed chain view. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, Hash)] pub struct AttestationData { pub slot: u64, pub head: Checkpoint, @@ -17,12 +17,14 @@ pub struct AttestationData { } /// Validator specific attestation wrapping shared attestation data. +#[cfg(feature = "devnet1")] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] pub struct Attestation { pub validator_id: u64, pub data: AttestationData, } +#[cfg(feature = "devnet1")] impl Attestation { /// Return the attested slot. pub fn slot(&self) -> u64 { @@ -45,6 +47,36 @@ impl Attestation { } } +#[cfg(feature = "devnet2")] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +pub struct AggregatedAttestations { + pub validator_id: u64, + pub data: AttestationData, +} + +#[cfg(feature = "devnet2")] +impl AggregatedAttestation { + /// Return the attested slot. + pub fn slot(&self) -> u64 { + self.message.slot + } + + /// Return the attested head checkpoint. + pub fn head(&self) -> Checkpoint { + self.message.head + } + + /// Return the attested target checkpoint. + pub fn target(&self) -> Checkpoint { + self.message.target + } + + /// Return the attested source checkpoint. + pub fn source(&self) -> Checkpoint { + self.message.source + } +} + /// Validator attestation bundled with its signature. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] pub struct SignedAttestation { @@ -76,7 +108,6 @@ pub struct SignedAggregatedAttestation { #[cfg(test)] mod tests { - use alloy_primitives::hex; use ssz::{Decode, Encode}; @@ -86,6 +117,7 @@ mod tests { #[test] fn test_encode_decode_signed_attestation_roundtrip() -> anyhow::Result<()> { let signed_attestation = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation { validator_id: 0, data: AttestationData { @@ -95,6 +127,15 @@ mod tests { source: Checkpoint::default(), }, }, + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: 1, + head: Checkpoint::default(), + target: Checkpoint::default(), + source: Checkpoint::default(), + }, + #[cfg(feature = "devnet2")] + validator_id: 0, signature: Signature { inner: FixedBytes::default(), }, diff --git a/crates/common/consensus/lean/src/block.rs b/crates/common/consensus/lean/src/block.rs index 57d06ddcd..d95b1ae0a 100644 --- a/crates/common/consensus/lean/src/block.rs +++ b/crates/common/consensus/lean/src/block.rs @@ -8,7 +8,13 @@ use ssz_types::{VariableList, typenum::U4096}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; -use crate::{attestation::Attestation, state::LeanState}; +#[cfg(feature = "devnet2")] +use crate::attestation::AggregatedAttestation; +#[cfg(feature = "devnet2")] +use crate::attestation::AggregatedAttestations; +#[cfg(feature = "devnet1")] +use crate::attestation::Attestation; +use crate::state::LeanState; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode)] pub struct BlockSignatures { @@ -34,18 +40,34 @@ impl SignedBlockWithAttestation { ) -> anyhow::Result { let block = &self.message.block; let signatures = &self.signature; + #[cfg(feature = "devnet1")] let mut all_attestations = block.body.attestations.to_vec(); + #[cfg(feature = "devnet2")] + let aggregated_attestations = &block.body.attestations; + #[cfg(feature = "devnet2")] + let attestation_signatures = &signatures.attestation_signatures; + #[cfg(feature = "devnet1")] all_attestations.push(self.message.proposer_attestation.clone()); + #[cfg(feature = "devnet1")] ensure!( signatures.len() == all_attestations.len(), "Number of signatures {} does not match number of attestations {}", signatures.len(), all_attestations.len(), ); + #[cfg(feature = "devnet2")] + ensure!( + attestation_signatures.len() == aggregated_attestations.len(), + "Number of signatures {} does not match number of attestations {}", + attestation_signatures.len(), + aggregated_attestations.len(), + ); + let validators = &parent_state.validators; + #[cfg(feature = "devnet1")] for (attestation, signature) in all_attestations.iter().zip(signatures.iter()) { ensure!( attestation.validator_id < validators.len() as u64, @@ -69,6 +91,73 @@ impl SignedBlockWithAttestation { } } + #[cfg(feature = "devnet2")] + { + let mut signature_iter = attestation_signatures.iter(); + for aggregated_attestation in aggregated_attestations.iter() { + let validator_ids: Vec = aggregated_attestation + .aggregation_bits + .iter() + .enumerate() + .filter(|(_, bit)| *bit) + .map(|(index, _)| index) + .collect(); + + let attestation_root = aggregated_attestation.message.tree_hash_root(); + + for validator_id in validator_ids { + let signature = signature_iter.next().ok_or_else(|| { + anyhow!("Missing signature for validator index {validator_id}") + })?; + + ensure!( + validator_id < validators.len(), + "Validator index out of range" + ); + + let validator = validators + .get(validator_id) + .ok_or_else(|| anyhow!("Failed to get validator"))?; + + if verify_signatures { + let timer = start_timer(&PQ_SIGNATURE_ATTESTATION_VERIFICATION_TIME, &[]); + ensure!( + signature.verify( + &validator.public_key, + aggregated_attestation.message.slot as u32, + &attestation_root, + )?, + "Attestation signature verification failed" + ); + stop_timer(timer); + } + } + } + + let proposer_attestation = &self.message.proposer_attestation; + let proposer_signature = &signatures.proposer_signature; + + ensure!( + proposer_attestation.validator_id < validators.len() as u64, + "Proposer index out of range" + ); + + let proposer = validators + .get(proposer_attestation.validator_id as usize) + .ok_or_else(|| anyhow!("Failed to get proposer validator"))?; + + if verify_signatures { + ensure!( + proposer_signature.verify( + &proposer.public_key, + proposer_attestation.data.slot as u32, + &proposer_attestation.data.tree_hash_root(), + )?, + "Failed to verify" + ); + } + } + Ok(true) } } @@ -77,7 +166,10 @@ impl SignedBlockWithAttestation { #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode)] pub struct BlockWithAttestation { pub block: Block, + #[cfg(feature = "devnet1")] pub proposer_attestation: Attestation, + #[cfg(feature = "devnet2")] + pub proposer_attestation: AggregatedAttestations, } /// Represents a block in the Lean chain. @@ -117,10 +209,10 @@ impl From for BlockHeader { /// Represents the body of a block in the Lean chain. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] pub struct BlockBody { - #[cfg(feature = "devnet2")] - pub attestations: VariableList, #[cfg(feature = "devnet1")] pub attestations: VariableList, + #[cfg(feature = "devnet2")] + pub attestations: VariableList, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] @@ -151,6 +243,7 @@ mod tests { attestations: Default::default(), }, }, + #[cfg(feature = "devnet1")] proposer_attestation: Attestation { validator_id: 0, data: AttestationData { @@ -160,8 +253,24 @@ mod tests { source: Checkpoint::default(), }, }, + #[cfg(feature = "devnet2")] + proposer_attestation: AggregatedAttestations { + validator_id: 0, + data: AttestationData { + slot: 0, + head: Checkpoint::default(), + target: Checkpoint::default(), + source: Checkpoint::default(), + }, + }, }, + #[cfg(feature = "devnet1")] signature: VariableList::default(), + #[cfg(feature = "devnet2")] + signature: BlockSignatures { + attestation_signatures: VariableList::default(), + proposer_signature: Signature::blank(), + }, }; let encode = signed_block_with_attestation.as_ssz_bytes(); diff --git a/crates/common/consensus/lean/src/checkpoint.rs b/crates/common/consensus/lean/src/checkpoint.rs index 3b65294db..fa8025e33 100644 --- a/crates/common/consensus/lean/src/checkpoint.rs +++ b/crates/common/consensus/lean/src/checkpoint.rs @@ -8,7 +8,18 @@ use tree_hash_derive::TreeHash; /// See the [Lean specification](https://github.com/leanEthereum/leanSpec/blob/main/docs/client/containers.md#checkpoint) /// for detailed protocol information. #[derive( - Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Encode, Decode, TreeHash, + Debug, + Default, + PartialEq, + Eq, + Clone, + Copy, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + Hash, )] pub struct Checkpoint { pub root: B256, diff --git a/crates/common/consensus/lean/src/state.rs b/crates/common/consensus/lean/src/state.rs index 06816a1d1..2c2f51e30 100644 --- a/crates/common/consensus/lean/src/state.rs +++ b/crates/common/consensus/lean/src/state.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +#[cfg(feature = "devnet2")] +use std::collections::HashSet; use alloy_primitives::B256; use anyhow::{Context, anyhow, ensure}; @@ -19,8 +21,11 @@ use tracing::info; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +#[cfg(feature = "devnet2")] +use crate::attestation::AggregatedAttestation; +#[cfg(feature = "devnet1")] +use crate::attestation::Attestation; use crate::{ - attestation::Attestation, block::{Block, BlockBody, BlockHeader}, checkpoint::Checkpoint, config::Config, @@ -130,8 +135,22 @@ impl LeanState { let timer = start_timer(&STATE_TRANSITION_BLOCK_PROCESSING_TIME, &[]); self.process_block_header(block)?; + #[cfg(feature = "devnet1")] self.process_attestations(&block.body.attestations)?; + #[cfg(feature = "devnet2")] + { + let mut attestations_data = HashSet::new(); + for aggregated_attestation in &block.body.attestations { + ensure!( + attestations_data.insert(&aggregated_attestation.message), + "Block contains duplicate AttestationData" + ); + } + + self.process_attestations(&block.body.attestations)?; + } + stop_timer(timer); Ok(()) } @@ -221,7 +240,11 @@ impl LeanState { Ok(()) } - pub fn process_attestations(&mut self, attestations: &[Attestation]) -> anyhow::Result<()> { + pub fn process_attestations( + &mut self, + #[cfg(feature = "devnet1")] attestations: &[Attestation], + #[cfg(feature = "devnet2")] attestations: &[AggregatedAttestation], + ) -> anyhow::Result<()> { let timer = start_timer(&STATE_TRANSITION_ATTESTATIONS_PROCESSING_TIME, &[]); let mut justifications_map = HashMap::new(); @@ -263,6 +286,7 @@ impl LeanState { .get(attestation.source().slot as usize) .map_err(|err| anyhow!("Failed to get justified slot: {err:?}"))? { + #[cfg(feature = "devnet1")] info!( reason = "Source slot not justified", source_slot = attestation.source().slot, @@ -270,6 +294,14 @@ impl LeanState { "Skipping attestations by Validator {}", attestation.validator_id, ); + #[cfg(feature = "devnet2")] + info!( + reason = "Source slot not justified", + source_slot = attestation.source().slot, + target_slot = attestation.target().slot, + "Skipping attestations by Validator {}", + attestation.aggregation_bits, + ); continue; } @@ -282,6 +314,7 @@ impl LeanState { .get(attestation.target().slot as usize) .map_err(|err| anyhow!("Failed to get justified slot: {err:?}"))? { + #[cfg(feature = "devnet1")] info!( reason = "Target slot already justified", source_slot = attestation.source().slot, @@ -289,6 +322,14 @@ impl LeanState { "Skipping attestations by Validator {}", attestation.validator_id, ); + #[cfg(feature = "devnet2")] + info!( + reason = "Target slot already justified", + source_slot = attestation.source().slot, + target_slot = attestation.target().slot, + "Skipping attestations by Validator {}", + attestation.aggregation_bits, + ); continue; } @@ -298,6 +339,7 @@ impl LeanState { .get(attestation.source().slot as usize) .ok_or(anyhow!("Source slot not found in historical_block_hashes"))? { + #[cfg(feature = "devnet1")] info!( reason = "Source block not in historical block hashes", source_slot = attestation.source().slot, @@ -305,6 +347,14 @@ impl LeanState { "Skipping attestations by Validator {}", attestation.validator_id, ); + #[cfg(feature = "devnet2")] + info!( + reason = "Source block not in historical block hashes", + source_slot = attestation.source().slot, + target_slot = attestation.target().slot, + "Skipping attestations by Validator {}", + attestation.aggregation_bits, + ); continue; } @@ -314,17 +364,27 @@ impl LeanState { .get(attestation.target().slot as usize) .ok_or(anyhow!("Target slot not found in historical_block_hashes"))? { + #[cfg(feature = "devnet1")] info!( reason = "Target block not in historical block hashes", source_slot = attestation.source().slot, target_slot = attestation.target().slot, "Skipping attestations by Validator {}", - attestation.validator_id, + attestation.validator_id + ); + #[cfg(feature = "devnet2")] + info!( + reason = "Target block not in historical block hashes", + source_slot = attestation.source().slot, + target_slot = attestation.target().slot, + "Skipping attestations by Validator {}", + attestation.aggregation_bits ); continue; } if attestation.target().slot <= attestation.source().slot { + #[cfg(feature = "devnet1")] info!( reason = "Target slot not greater than source slot", source_slot = attestation.source().slot, @@ -332,10 +392,19 @@ impl LeanState { "Skipping attestations by Validator {}", attestation.validator_id, ); + #[cfg(feature = "devnet2")] + info!( + reason = "Target slot not greater than source slot", + source_slot = attestation.source().slot, + target_slot = attestation.target().slot, + "Skipping attestations by Validator {}", + attestation.aggregation_bits, + ); continue; } if !is_justifiable_slot(self.latest_finalized.slot, attestation.target().slot) { + #[cfg(feature = "devnet1")] info!( reason = "Target slot not justifiable", source_slot = attestation.source().slot, @@ -343,6 +412,14 @@ impl LeanState { "Skipping attestations by Validator {}", attestation.validator_id, ); + #[cfg(feature = "devnet2")] + info!( + reason = "Target slot not justifiable", + source_slot = attestation.source().slot, + target_slot = attestation.target().slot, + "Skipping attestations by Validator {}", + attestation.aggregation_bits, + ); continue; } @@ -358,6 +435,7 @@ impl LeanState { })?, ); + #[cfg(feature = "devnet1")] justifications .set(attestation.validator_id as usize, true) .map_err(|err| { @@ -368,6 +446,17 @@ impl LeanState { ) })?; + #[cfg(feature = "devnet2")] + for (validator_id, signed) in attestation.aggregation_bits.iter().enumerate() { + if signed { + if !justifications.get(validator_id).unwrap_or(false) { + justifications.set(validator_id, true).map_err(|err| { + anyhow!("Failed to set validator {validator_id}: {err:?}") + })?; + } + } + } + let count = justifications.num_set_bits(); // If 2/3 attestations for the same new valid hash to justify diff --git a/crates/common/fork_choice/lean/Cargo.toml b/crates/common/fork_choice/lean/Cargo.toml index 6d381d1f0..dbfdc2eeb 100644 --- a/crates/common/fork_choice/lean/Cargo.toml +++ b/crates/common/fork_choice/lean/Cargo.toml @@ -9,6 +9,11 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = ["devnet1"] +devnet1 = [] +devnet2 = [] + [dependencies] alloy-consensus.workspace = true alloy-primitives.workspace = true diff --git a/crates/common/fork_choice/lean/src/store.rs b/crates/common/fork_choice/lean/src/store.rs index e1acb5d12..cb131534e 100644 --- a/crates/common/fork_choice/lean/src/store.rs +++ b/crates/common/fork_choice/lean/src/store.rs @@ -2,8 +2,14 @@ use std::{collections::HashMap, sync::Arc}; use alloy_primitives::B256; use anyhow::{anyhow, ensure}; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::attestation::AggregatedAttestation; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::attestation::AggregatedAttestations; +#[cfg(feature = "devnet1")] +use ream_consensus_lean::attestation::Attestation; use ream_consensus_lean::{ - attestation::{Attestation, AttestationData, SignedAttestation}, + attestation::{AttestationData, SignedAttestation}, block::{Block, BlockBody, BlockWithSignatures, SignedBlockWithAttestation}, checkpoint::Checkpoint, state::LeanState, @@ -24,6 +30,8 @@ use ream_storage::{ tables::{field::REDBField, table::REDBTable}, }; use ream_sync::rwlock::{Reader, Writer}; +#[cfg(feature = "devnet2")] +use ssz_types::BitList; use ssz_types::{VariableList, typenum::U4096}; use tokio::sync::Mutex; use tree_hash::TreeHash; @@ -124,8 +132,12 @@ impl Store { for attestation in attestations { let attestation = attestation?; + #[cfg(feature = "devnet1")] let mut current_root = attestation.message.data.head.root; + #[cfg(feature = "devnet2")] + let mut current_root = attestation.message.head.root; + while let Some(block) = block_provider.get(current_root)? { let block = block.message.block; @@ -394,7 +406,10 @@ impl Store { slot: u64, proposer_index: u64, parent_root: B256, - attestations: Option>, + #[cfg(feature = "devnet1")] attestations: Option>, + #[cfg(feature = "devnet2")] attestations: Option< + VariableList, + >, ) -> anyhow::Result<(Block, Vec, LeanState)> { let (state_provider, latest_known_attestation_provider, block_provider) = { let db = self.store.lock().await; @@ -409,41 +424,104 @@ impl Store { let head_state = state_provider .get(parent_root)? .ok_or(anyhow!("State not found for head root"))?; - + #[cfg(feature = "devnet1")] let mut attestations: VariableList = attestations.unwrap_or_else(VariableList::empty); + + #[cfg(feature = "devnet2")] + let mut attestations: VariableList = + attestations.unwrap_or_else(VariableList::empty); + let mut signatures: Vec = Vec::new(); let (mut candidate_block, signatures, post_state) = loop { + #[cfg(feature = "devnet2")] + let attestations_list: VariableList = { + let mut groups: HashMap> = HashMap::new(); + for attestation in attestations.iter() { + groups + .entry(attestation.data.clone()) + .or_default() + .push(attestation.validator_id); + } + + VariableList::new( + groups + .into_iter() + .map(|(message, ids)| { + let mut bits = BitList::::with_capacity( + ids.iter().max().map_or(0, |&id| id as usize + 1), + ) + .map_err(|err| anyhow!("BitList error: {err:?}"))?; + + for id in ids { + bits.set(id as usize, true) + .map_err(|err| anyhow!("BitList error: {err:?}"))?; + } + Ok(AggregatedAttestation { + aggregation_bits: bits, + message, + }) + }) + .collect::>>()?, + ) + .map_err(|err| anyhow!("Limit exceeded: {err:?}"))? + }; let candidate_block = Block { slot, proposer_index, parent_root, state_root: B256::ZERO, + #[cfg(feature = "devnet1")] body: BlockBody { attestations: attestations.clone(), }, + #[cfg(feature = "devnet2")] + body: BlockBody { + attestations: attestations_list.clone(), + }, }; let mut advanced_state = head_state.clone(); advanced_state.process_slots(slot)?; advanced_state.process_block(&candidate_block)?; - + #[cfg(feature = "devnet1")] let mut new_attestations: VariableList = VariableList::empty(); + #[cfg(feature = "devnet2")] + let mut new_attestations: VariableList = + VariableList::empty(); let mut new_signatures: Vec = Vec::new(); for signed_attestation in available_signed_attestations.values() { + #[cfg(feature = "devnet1")] let data = &signed_attestation.message.data; + #[cfg(feature = "devnet2")] + let data = &signed_attestation.message; + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestations { + validator_id: signed_attestation.validator_id, + data: data.clone(), + }; + if !block_provider.contains_key(data.head.root) { continue; } if data.source != advanced_state.latest_justified { continue; } + #[cfg(feature = "devnet1")] if !attestations.contains(&signed_attestation.message) { new_attestations .push(signed_attestation.message.clone()) .map_err(|err| anyhow!("Could not append attestation: {err:?}"))?; new_signatures.push(signed_attestation.signature); } + + #[cfg(feature = "devnet2")] + if !attestations.contains(&attestation) { + new_attestations + .push(attestation) + .map_err(|err| anyhow!("Could not append attestation: {err:?}"))?; + new_signatures.push(signed_attestation.signature); + } } if new_attestations.is_empty() { break (candidate_block, signatures, advanced_state); @@ -520,6 +598,7 @@ impl Store { ) }; let block = &signed_block_with_attestation.message.block; + #[cfg(feature = "devnet1")] let signatures = &signed_block_with_attestation.signature; let proposer_attestation = &signed_block_with_attestation.message.proposer_attestation; let block_root = block.tree_hash_root(); @@ -562,6 +641,7 @@ impl Store { latest_finalized_provider.insert(latest_finalized)?; *self.network_state.finalized_checkpoint.write() = latest_finalized; + #[cfg(feature = "devnet1")] for (attestation, signature) in signed_block_with_attestation .message .block @@ -580,14 +660,71 @@ impl Store { .await?; } + #[cfg(feature = "devnet2")] + { + let aggregated_attestations = &signed_block_with_attestation + .message + .block + .body + .attestations; + let attestation_signatures = &signed_block_with_attestation + .signature + .attestation_signatures; + + ensure!( + aggregated_attestations.len() == attestation_signatures.len(), + "Attestation signature groups must match aggregated attestations" + ); + + for (aggregated_attestation, aggregated_signature) in aggregated_attestations + .into_iter() + .zip(attestation_signatures) + { + let validator_ids: Vec = aggregated_attestation + .aggregation_bits + .iter() + .enumerate() + .filter(|(_, bit)| *bit) + .map(|(index, _)| index as u64) + .collect(); + + ensure!( + validator_ids.len() == aggregated_signature.inner.len(), + "Aggregated attestation signature count mismatch" + ); + + for (validator_id, signature) in + validator_ids.into_iter().zip(attestation_signatures) + { + self.on_attestation( + SignedAttestation { + validator_id, + message: aggregated_attestation.message.clone(), + signature: *signature, + }, + true, + ) + .await?; + } + } + } + self.update_head().await?; self.on_attestation( SignedAttestation { + #[cfg(feature = "devnet1")] message: proposer_attestation.clone(), + #[cfg(feature = "devnet1")] signature: *signatures .get(block.body.attestations.len()) .ok_or(anyhow!("Failed to get attestation"))?, + #[cfg(feature = "devnet2")] + message: proposer_attestation.data.clone(), + #[cfg(feature = "devnet2")] + signature: signed_block_with_attestation.signature.proposer_signature, + #[cfg(feature = "devnet2")] + validator_id: proposer_attestation.validator_id, }, false, ) @@ -601,7 +738,10 @@ impl Store { &self, signed_attestation: &SignedAttestation, ) -> anyhow::Result<()> { + #[cfg(feature = "devnet1")] let data = &signed_attestation.message.data; + #[cfg(feature = "devnet2")] + let data = &signed_attestation.message; let block_provider = self.store.lock().await.block_provider(); // Validate attestation targets exist in store @@ -683,18 +823,32 @@ impl Store { } } + #[cfg(feature = "devnet1")] let validator_id = signed_attestation.message.validator_id; + #[cfg(feature = "devnet1")] let attestation_slot = signed_attestation.message.data.slot; + + #[cfg(feature = "devnet2")] + let validator_id = signed_attestation.validator_id; + #[cfg(feature = "devnet2")] + let attestation_slot = signed_attestation.message.slot; + if is_from_block { let latest_known = match latest_known_attestations_provider.get(validator_id)? { + #[cfg(feature = "devnet1")] Some(latest_known) => latest_known.message.data.slot < attestation_slot, + #[cfg(feature = "devnet2")] + Some(latest_known) => latest_known.message.slot < attestation_slot, None => true, }; if latest_known { latest_known_attestations_provider.insert(validator_id, signed_attestation)?; } let remove = match latest_new_attestations_provider.get(validator_id)? { + #[cfg(feature = "devnet1")] Some(new_new) => new_new.message.data.slot <= attestation_slot, + #[cfg(feature = "devnet2")] + Some(new_new) => new_new.message.slot <= attestation_slot, None => false, }; if remove { @@ -707,7 +861,10 @@ impl Store { "Attestation from future slot {attestation_slot} <= {time_slots}", ); let latest_new = match latest_new_attestations_provider.get(validator_id)? { + #[cfg(feature = "devnet1")] Some(latest_new) => latest_new.message.data.slot < attestation_slot, + #[cfg(feature = "devnet2")] + Some(latest_new) => latest_new.message.slot < attestation_slot, None => true, }; if latest_new { @@ -749,8 +906,15 @@ impl Store { #[cfg(test)] mod tests { use alloy_primitives::{B256, FixedBytes}; + #[cfg(feature = "devnet1")] + use ream_consensus_lean::attestation::Attestation; + #[cfg(feature = "devnet2")] use ream_consensus_lean::{ - attestation::{Attestation, AttestationData, SignedAttestation}, + attestation::{AggregatedAttestation, AggregatedAttestations}, + block::BlockSignatures, + }; + use ream_consensus_lean::{ + attestation::{AttestationData, SignedAttestation}, block::{ Block, BlockBody, BlockHeader, BlockWithAttestation, BlockWithSignatures, SignedBlockWithAttestation, @@ -824,13 +988,25 @@ mod tests { signatures.push(Signature::blank()).unwrap(); SignedBlockWithAttestation { message: BlockWithAttestation { + #[cfg(feature = "devnet1")] proposer_attestation: Attestation { validator_id: block.proposer_index, data: attestation_data, }, + #[cfg(feature = "devnet2")] + proposer_attestation: AggregatedAttestations { + validator_id: block.proposer_index, + data: attestation_data, + }, block, }, + #[cfg(feature = "devnet1")] signature: signatures, + #[cfg(feature = "devnet2")] + signature: BlockSignatures { + attestation_signatures: signatures, + proposer_signature: Signature::blank(), + }, } } @@ -900,6 +1076,7 @@ mod tests { let attestation_target = store.get_attestation_target().await.unwrap(); let attestation_1 = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation { validator_id: 5, data: AttestationData { @@ -912,10 +1089,23 @@ mod tests { source: attestation_target, }, }, + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: head_block.message.block.slot, + head: Checkpoint { + root: head, + slot: head_block.message.block.slot, + }, + target: justified_checkpoint, + source: attestation_target, + }, signature: Signature::blank(), + #[cfg(feature = "devnet2")] + validator_id: 5, }; let attestation_2 = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation { validator_id: 6, data: AttestationData { @@ -928,7 +1118,19 @@ mod tests { source: attestation_target, }, }, + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: head_block.message.block.slot, + head: Checkpoint { + root: head, + slot: head_block.message.block.slot, + }, + target: justified_checkpoint, + source: attestation_target, + }, signature: Signature::blank(), + #[cfg(feature = "devnet2")] + validator_id: 5, }; latest_known_attestations .batch_insert([(5, attestation_1), (6, attestation_2)]) @@ -1001,6 +1203,7 @@ mod tests { let head_block = block_provider.get(head).unwrap().unwrap(); let attestation = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation { validator_id: 7, data: AttestationData { @@ -1013,7 +1216,19 @@ mod tests { source: store.get_attestation_target().await.unwrap(), }, }, + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: head_block.message.block.slot, + head: Checkpoint { + root: head, + slot: head_block.message.block.slot, + }, + target: latest_justified_provider.get().unwrap(), + source: store.get_attestation_target().await.unwrap(), + }, signature: Signature::blank(), + #[cfg(feature = "devnet2")] + validator_id: 7, }; latest_known_attestations.insert(7, attestation).unwrap(); @@ -1058,10 +1273,17 @@ mod tests { .get() .unwrap(); + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id, data: store.produce_attestation_data(slot).await.unwrap(), }; + + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestations { + validator_id, + data: store.produce_attestation_data(slot).await.unwrap(), + }; assert_eq!(attestation.validator_id, validator_id); assert_eq!(attestation.data.slot, slot); assert_eq!(attestation.data.source, latest_justified_checkpoint); @@ -1075,10 +1297,17 @@ mod tests { let (store, _) = sample_store(10).await; let block_provider = store.store.lock().await.block_provider(); + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id: 8, data: store.produce_attestation_data(slot).await.unwrap(), }; + + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestations { + validator_id: 8, + data: store.produce_attestation_data(slot).await.unwrap(), + }; let head = store.get_proposal_head(slot).await.unwrap(); assert_eq!(attestation.data.head.root, head); @@ -1091,10 +1320,17 @@ mod tests { #[tokio::test] pub async fn test_produce_attestation_target_calculation() { let (store, _) = sample_store(10).await; + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id: 9, data: store.produce_attestation_data(3).await.unwrap(), }; + + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestations { + validator_id: 9, + data: store.produce_attestation_data(3).await.unwrap(), + }; let expected_target = store.get_attestation_target().await.unwrap(); assert_eq!(attestation.data.target.root, expected_target.root); assert_eq!(attestation.data.target.slot, expected_target.slot); @@ -1108,10 +1344,16 @@ mod tests { let mut attestations = Vec::new(); for validator_id in 0..5 { + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id, data: store.produce_attestation_data(slot).await.unwrap(), }; + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestations { + validator_id, + data: store.produce_attestation_data(slot).await.unwrap(), + }; assert_eq!(attestation.validator_id, validator_id); assert_eq!(attestation.data.slot, slot); @@ -1129,21 +1371,41 @@ mod tests { /// Test attestation production across sequential slots. #[tokio::test] pub async fn test_produce_attestation_sequential_slots() { + #[cfg(feature = "devnet1")] let validator_id = 3; let (store, _) = sample_store(10).await; let latest_justified_provider = store.store.lock().await.latest_justified_provider(); + #[cfg(feature = "devnet2")] + let mut aggregation_bits = BitList::::with_capacity(32).unwrap(); + #[cfg(feature = "devnet2")] + aggregation_bits.set(0, true).unwrap(); + + #[cfg(feature = "devnet1")] let attestation_1 = Attestation { validator_id, data: store.produce_attestation_data(1).await.unwrap(), }; + #[cfg(feature = "devnet1")] let attestation_2 = Attestation { validator_id, data: store.produce_attestation_data(2).await.unwrap(), }; + #[cfg(feature = "devnet2")] + let attestation_1 = AggregatedAttestation { + aggregation_bits: aggregation_bits.clone(), + message: store.produce_attestation_data(1).await.unwrap(), + }; + + #[cfg(feature = "devnet2")] + let attestation_2 = AggregatedAttestation { + aggregation_bits, + message: store.produce_attestation_data(2).await.unwrap(), + }; + assert_ne!(attestation_1.slot(), attestation_2.slot()); assert_eq!(attestation_1.source(), attestation_2.source()); assert_eq!( @@ -1161,11 +1423,23 @@ mod tests { (db.latest_justified_provider(), db.block_provider()) }; + #[cfg(feature = "devnet2")] + let mut aggregation_bits = BitList::::with_capacity(32).unwrap(); + #[cfg(feature = "devnet2")] + aggregation_bits.set(0, true).unwrap(); + + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id: 2, data: store.produce_attestation_data(5).await.unwrap(), }; + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestation { + aggregation_bits, + message: store.produce_attestation_data(5).await.unwrap(), + }; + assert_eq!( attestation.source(), latest_justified_provider.get().unwrap() @@ -1205,17 +1479,38 @@ mod tests { store.update_head().await.unwrap(); + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id: 7, data: store.produce_attestation_data(2).await.unwrap(), }; + #[cfg(feature = "devnet2")] + let mut aggregation_bits = BitList::::with_capacity(32).unwrap(); + #[cfg(feature = "devnet2")] + aggregation_bits.set(0, true).unwrap(); + + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestation { + aggregation_bits, + message: store.produce_attestation_data(2).await.unwrap(), + }; + + #[cfg(feature = "devnet1")] assert_eq!(attestation.validator_id, 7); + #[cfg(feature = "devnet2")] + assert_eq!(attestation.aggregation_bits, attestation.aggregation_bits); assert_eq!(attestation.slot(), 2); + #[cfg(feature = "devnet1")] assert_eq!( attestation.data.source, latest_justified_provider.get().unwrap() ); + #[cfg(feature = "devnet2")] + assert_eq!( + attestation.message.source, + latest_justified_provider.get().unwrap() + ); } /// Test producing a block then creating attestation for it. @@ -1248,10 +1543,17 @@ mod tests { let mut attestations = Vec::new(); for i in 2..6 { + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id: i, data: store.produce_attestation_data(2).await.unwrap(), }; + + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestations { + validator_id: i, + data: store.produce_attestation_data(2).await.unwrap(), + }; attestations.push(attestation); } @@ -1318,12 +1620,26 @@ mod tests { .await .unwrap(); + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id: 9, data: store.produce_attestation_data(10).await.unwrap(), }; + #[cfg(feature = "devnet2")] + let mut aggregation_bits = BitList::::with_capacity(32).unwrap(); + #[cfg(feature = "devnet2")] + aggregation_bits.set(0, true).unwrap(); + + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestation { + aggregation_bits, + message: store.produce_attestation_data(10).await.unwrap(), + }; + #[cfg(feature = "devnet1")] assert_eq!(attestation.validator_id, 9); + #[cfg(feature = "devnet2")] + assert_eq!(attestation.aggregation_bits, attestation.aggregation_bits); assert_eq!(attestation.slot(), 10); } @@ -1457,10 +1773,17 @@ mod tests { // shoudl fail assert!(!is_proposer(1000000, 1000000, 10)); + #[cfg(feature = "devnet1")] let attestation = Attestation { validator_id: 1000000, data: store.produce_attestation_data(1).await.unwrap(), }; + + #[cfg(feature = "devnet2")] + let attestation = AggregatedAttestations { + validator_id: 1000000, + data: store.produce_attestation_data(1).await.unwrap(), + }; assert_eq!(attestation.validator_id, 1000000); } @@ -1596,6 +1919,7 @@ mod tests { let justified_provider = db.latest_justified_provider(); let justified_checkpoint = justified_provider.get().unwrap(); let signed_attestation = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation { validator_id: 5, data: AttestationData { @@ -1605,12 +1929,27 @@ mod tests { source: justified_checkpoint, }, }, + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: 1, + head: justified_checkpoint, + target: test_checkpoint, + source: justified_checkpoint, + }, + #[cfg(feature = "devnet2")] + validator_id: 5, signature: Signature::blank(), }; let db_table = db.latest_new_attestations_provider(); + #[cfg(feature = "devnet1")] db_table .insert(signed_attestation.message.validator_id, signed_attestation) .unwrap(); + + #[cfg(feature = "devnet2")] + db_table + .insert(signed_attestation.validator_id, signed_attestation) + .unwrap(); }; for interval in 0..INTERVALS_PER_SLOT { @@ -1703,6 +2042,7 @@ mod tests { let justified_provider = db.latest_justified_provider(); let justified_checkpoint = justified_provider.get().unwrap(); let signed_attestation = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation { validator_id: 5, data: AttestationData { @@ -1712,12 +2052,27 @@ mod tests { source: justified_checkpoint, }, }, + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: 1, + head: justified_checkpoint, + target: checkpoint, + source: justified_checkpoint, + }, + #[cfg(feature = "devnet2")] + validator_id: 5, signature: Signature::blank(), }; let db_table = db.latest_new_attestations_provider(); + #[cfg(feature = "devnet1")] db_table .insert(signed_attestation.message.validator_id, signed_attestation) .unwrap(); + + #[cfg(feature = "devnet2")] + db_table + .insert(signed_attestation.validator_id, signed_attestation) + .unwrap(); }; let latest_new_attestations_provider = { store.store.lock().await.latest_new_attestations_provider() }; @@ -1783,6 +2138,7 @@ mod tests { let justified_provider = db.latest_justified_provider(); let justified_checkpoint = justified_provider.get().unwrap(); let signed_attestation = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation { validator_id: i, data: AttestationData { @@ -1792,12 +2148,27 @@ mod tests { source: justified_checkpoint, }, }, + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: i, + head: justified_checkpoint, + target: *checkpoint, + source: justified_checkpoint, + }, + #[cfg(feature = "devnet2")] + validator_id: i, signature: Signature::blank(), }; let db_table = db.latest_new_attestations_provider(); + #[cfg(feature = "devnet1")] db_table .insert(signed_attestation.message.validator_id, signed_attestation) .unwrap(); + + #[cfg(feature = "devnet2")] + db_table + .insert(signed_attestation.validator_id, signed_attestation) + .unwrap(); } let latest_known_attestations_provider = { @@ -1829,6 +2200,7 @@ mod tests { assert!(new_attestations_length == 0); assert!(latest_known_attestations_length == 5); + #[cfg(feature = "devnet1")] for (i, checkpoint) in checkpoints.iter().enumerate().map(|(i, c)| (i as u64, c)) { let stored_checkpoint = latest_known_attestations_provider .get(i) @@ -1839,6 +2211,17 @@ mod tests { .target; assert!(stored_checkpoint == *checkpoint); } + + #[cfg(feature = "devnet2")] + for (i, checkpoint) in checkpoints.iter().enumerate().map(|(i, c)| (i as u64, c)) { + let stored_checkpoint = latest_known_attestations_provider + .get(i) + .unwrap() + .unwrap() + .message + .target; + assert!(stored_checkpoint == *checkpoint); + } } // Test accepting new attestations when there are none. @@ -1928,6 +2311,7 @@ mod tests { let justified_provider = db.latest_justified_provider(); let justified_checkpoint = justified_provider.get().unwrap(); let signed_attestation = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation { validator_id: 10, data: AttestationData { @@ -1937,12 +2321,27 @@ mod tests { source: justified_checkpoint, }, }, + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: 10, + head: justified_checkpoint, + target: checkpoint, + source: justified_checkpoint, + }, + #[cfg(feature = "devnet2")] + validator_id: 10, signature: Signature::blank(), }; let db_table = db.latest_new_attestations_provider(); + #[cfg(feature = "devnet1")] db_table .insert(signed_attestation.message.validator_id, signed_attestation) .unwrap(); + + #[cfg(feature = "devnet2")] + db_table + .insert(signed_attestation.validator_id, signed_attestation) + .unwrap(); }; store.get_proposal_head(1).await.unwrap(); @@ -1958,6 +2357,7 @@ mod tests { .count() }; + #[cfg(feature = "devnet1")] let known_attestations_correct_checkpoint = { store .store @@ -1973,6 +2373,21 @@ mod tests { .target }; + #[cfg(feature = "devnet2")] + let known_attestations_correct_checkpoint = { + store + .store + .lock() + .await + .latest_known_attestations_provider() + .get_all_attestations() + .unwrap() + .get(&10) + .unwrap() + .message + .target + }; + assert!(new_attestations_length == 0); assert!(known_attestations_correct_checkpoint.slot == 10); assert!(known_attestations_correct_checkpoint == checkpoint); diff --git a/crates/common/validator/lean/Cargo.toml b/crates/common/validator/lean/Cargo.toml index e0eb896a5..f19eca607 100644 --- a/crates/common/validator/lean/Cargo.toml +++ b/crates/common/validator/lean/Cargo.toml @@ -9,6 +9,11 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = ["devnet1"] +devnet1 = [] +devnet2 = [] + [dependencies] alloy-primitives.workspace = true anyhow.workspace = true diff --git a/crates/common/validator/lean/src/service.rs b/crates/common/validator/lean/src/service.rs index b8f9f4d50..9b14b6314 100644 --- a/crates/common/validator/lean/src/service.rs +++ b/crates/common/validator/lean/src/service.rs @@ -1,7 +1,17 @@ use anyhow::anyhow; use ream_chain_lean::{clock::create_lean_clock_interval, messages::LeanChainServiceMessage}; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::attestation::AggregatedAttestations; +#[cfg(feature = "devnet1")] +use ream_consensus_lean::attestation::Attestation; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::attestation::AttestationData; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::block::BlockSignatures; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::checkpoint::Checkpoint; use ream_consensus_lean::{ - attestation::{Attestation, SignedAttestation}, + attestation::SignedAttestation, block::{BlockWithAttestation, BlockWithSignatures, SignedBlockWithAttestation}, }; use ream_keystore::lean_keystore::ValidatorKeystore; @@ -10,6 +20,8 @@ use ream_metrics::{ stop_timer, }; use ream_network_spec::networks::lean_network_spec; +#[cfg(feature = "devnet2")] +use ream_post_quantum_crypto::leansig::signature::Signature; use tokio::sync::{mpsc, oneshot}; use tracing::{Level, debug, enabled, info}; use tree_hash::TreeHash; @@ -82,7 +94,10 @@ impl ValidatorService { .expect("Failed to send attestation to LeanChainService"); let attestation_data = rx.await.expect("Failed to receive attestation data from LeanChainService"); + #[cfg(feature = "devnet1")] let message = Attestation { validator_id: keystore.index, data: attestation_data }; + #[cfg(feature = "devnet2")] + let message = AggregatedAttestations { validator_id: keystore.index, data: attestation_data }; let timer = start_timer(&PQ_SIGNATURE_ATTESTATION_SIGNING_TIME, &[]); let signature = keystore.private_key.sign(&message.tree_hash_root(), slot as u32)?; @@ -94,7 +109,13 @@ impl ValidatorService { block: block.clone(), proposer_attestation: message, }, + #[cfg(feature = "devnet1")] signature: signatures, + #[cfg(feature = "devnet2")] + signature: BlockSignatures { + attestation_signatures: signatures, + proposer_signature: Signature::blank(), + }, }; // Send block to the LeanChainService. @@ -140,16 +161,26 @@ impl ValidatorService { // TODO: Sign the attestation with the keystore. let mut signed_attestations = vec![]; for (_, keystore) in self.keystores.iter().enumerate().filter(|(index, _)| *index as u64 != slot % lean_network_spec().num_validators) { + #[cfg(feature = "devnet1")] let message = Attestation { validator_id: keystore.index, data: attestation_data.clone() }; + #[cfg(feature = "devnet2")] + let message = AttestationData { + slot: 0, + head: Checkpoint::default(), + target: Checkpoint::default(), + source: Checkpoint::default(), + }; let timer = start_timer(&PQ_SIGNATURE_ATTESTATION_SIGNING_TIME, &[]); let signature =keystore.private_key.sign(&message.tree_hash_root(), slot as u32)?; stop_timer(timer); signed_attestations.push(SignedAttestation { signature, message, + #[cfg(feature = "devnet2")] + validator_id: keystore.index, }); } diff --git a/crates/networking/p2p/Cargo.toml b/crates/networking/p2p/Cargo.toml index 972d122e5..7db8bf119 100644 --- a/crates/networking/p2p/Cargo.toml +++ b/crates/networking/p2p/Cargo.toml @@ -9,6 +9,11 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[features] +default = ["devnet1"] +devnet1 = [] +devnet2 = [] + [dependencies] alloy-primitives.workspace = true anyhow.workspace = true diff --git a/crates/networking/p2p/src/network/lean/mod.rs b/crates/networking/p2p/src/network/lean/mod.rs index 109211235..e47a54c3c 100644 --- a/crates/networking/p2p/src/network/lean/mod.rs +++ b/crates/networking/p2p/src/network/lean/mod.rs @@ -298,16 +298,29 @@ impl LeanNetworkService { signed_attestation.as_ssz_bytes(), ) { + #[cfg(feature = "devnet1")] warn!( slot = signed_attestation.message.slot(), error = ?err, "Publish attestation failed" ); + #[cfg(feature = "devnet2")] + warn!( + slot = signed_attestation.message.slot, + error = ?err, + "Publish attestation failed" + ); } else { + #[cfg(feature = "devnet1")] info!( slot = signed_attestation.message.slot(), "Broadcasted attestation" ); + #[cfg(feature = "devnet2")] + info!( + slot = signed_attestation.message.slot, + "Broadcasted attestation" + ); } } } @@ -449,7 +462,10 @@ impl LeanNetworkService { } } Ok(LeanGossipsubMessage::Attestation(signed_attestation)) => { + #[cfg(feature = "devnet1")] let slot = signed_attestation.message.slot(); + #[cfg(feature = "devnet2")] + let slot = signed_attestation.message.slot; if let Err(err) = self.chain_message_sender.send( LeanChainServiceMessage::ProcessAttestation { diff --git a/testing/lean-spec-tests/Cargo.toml b/testing/lean-spec-tests/Cargo.toml index 60d53e4b1..2bccda44e 100644 --- a/testing/lean-spec-tests/Cargo.toml +++ b/testing/lean-spec-tests/Cargo.toml @@ -11,6 +11,9 @@ version.workspace = true [features] lean-spec-tests = [] +default = ["devnet1"] +devnet1 = [] +devnet2 = [] [dependencies] alloy-primitives.workspace = true diff --git a/testing/lean-spec-tests/src/fork_choice.rs b/testing/lean-spec-tests/src/fork_choice.rs index 40a4164c8..c000af9a1 100644 --- a/testing/lean-spec-tests/src/fork_choice.rs +++ b/testing/lean-spec-tests/src/fork_choice.rs @@ -1,8 +1,14 @@ use std::path::Path; use anyhow::{anyhow, bail, ensure}; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::attestation::AggregatedAttestations; +#[cfg(feature = "devnet1")] +use ream_consensus_lean::attestation::Attestation; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::block::BlockSignatures; use ream_consensus_lean::{ - attestation::{Attestation, AttestationData, SignedAttestation}, + attestation::{AttestationData, SignedAttestation}, block::{Block, BlockWithAttestation, SignedBlockWithAttestation}, checkpoint::Checkpoint, state::LeanState, @@ -83,6 +89,7 @@ pub async fn run_fork_choice_test(test_name: &str, test: ForkChoiceTest) -> anyh let mut store = Store::get_forkchoice_store( SignedBlockWithAttestation { message: BlockWithAttestation { + #[cfg(feature = "devnet1")] proposer_attestation: Attestation { validator_id: block.proposer_index, data: AttestationData { @@ -95,9 +102,28 @@ pub async fn run_fork_choice_test(test_name: &str, test: ForkChoiceTest) -> anyh source: state.latest_finalized, }, }, + #[cfg(feature = "devnet2")] + proposer_attestation: AggregatedAttestations { + validator_id: block.proposer_index, + data: AttestationData { + slot: block.slot, + head: Checkpoint { + root: block.tree_hash_root(), + slot: block.slot, + }, + target: state.latest_justified, + source: state.latest_finalized, + }, + }, block, }, + #[cfg(feature = "devnet1")] signature: VariableList::empty(), + #[cfg(feature = "devnet2")] + signature: BlockSignatures { + attestation_signatures: VariableList::empty(), + proposer_signature: Signature::blank(), + }, }, state, db, @@ -164,6 +190,7 @@ pub async fn run_fork_choice_test(test_name: &str, test: ForkChoiceTest) -> anyh .on_block( &SignedBlockWithAttestation { message: BlockWithAttestation { + #[cfg(feature = "devnet1")] proposer_attestation: Attestation { validator_id: ream_block.proposer_index, data: AttestationData { @@ -179,9 +206,31 @@ pub async fn run_fork_choice_test(test_name: &str, test: ForkChoiceTest) -> anyh source: source_checkpoint, }, }, + #[cfg(feature = "devnet2")] + proposer_attestation: AggregatedAttestations { + validator_id: ream_block.proposer_index, + data: AttestationData { + slot: ream_block.slot, + head: Checkpoint { + root: ream_block.tree_hash_root(), + slot: ream_block.slot, + }, + target: Checkpoint { + root: ream_block.parent_root, + slot: parent_slot, + }, + source: source_checkpoint, + }, + }, block: ream_block, }, + #[cfg(feature = "devnet1")] signature: signatures, + #[cfg(feature = "devnet2")] + signature: BlockSignatures { + attestation_signatures: signatures, + proposer_signature: Signature::blank(), + }, }, false, // Don't verify signatures in spec tests (we use blank signatures) ) @@ -215,15 +264,30 @@ pub async fn run_fork_choice_test(test_name: &str, test: ForkChoiceTest) -> anyh ); let signed_attestation = SignedAttestation { + #[cfg(feature = "devnet1")] message: Attestation::from(attestation), + #[cfg(feature = "devnet2")] + message: AttestationData { + slot: 0, + head: Checkpoint::default(), + target: Checkpoint::default(), + source: Checkpoint::default(), + }, signature: Signature::blank(), + #[cfg(feature = "devnet2")] + validator_id: 0, }; // Add attestation to new attestations let db = store.store.lock().await; + #[cfg(feature = "devnet1")] let result = db .latest_new_attestations_provider() .insert(signed_attestation.message.validator_id, signed_attestation); + #[cfg(feature = "devnet2")] + let result = db + .latest_new_attestations_provider() + .insert(signed_attestation.validator_id, signed_attestation); if *valid { result.map_err(|err| { diff --git a/testing/lean-spec-tests/src/types/mod.rs b/testing/lean-spec-tests/src/types/mod.rs index 618d79a74..7cf75fe6c 100644 --- a/testing/lean-spec-tests/src/types/mod.rs +++ b/testing/lean-spec-tests/src/types/mod.rs @@ -5,8 +5,12 @@ use std::collections::HashMap; use alloy_primitives::{B256, hex}; use anyhow::{anyhow, bail}; +#[cfg(feature = "devnet2")] +use ream_consensus_lean::attestation::AggregatedAttestations as ReamAttestation; +#[cfg(feature = "devnet1")] +use ream_consensus_lean::attestation::Attestation as ReamAttestation; use ream_consensus_lean::{ - attestation::{Attestation as ReamAttestation, AttestationData}, + attestation::AttestationData, block::{Block as ReamBlock, BlockBody as ReamBlockBody, BlockHeader as ReamBlockHeader}, checkpoint::Checkpoint as ReamCheckpoint, config::Config as ReamConfig, @@ -14,7 +18,11 @@ use ream_consensus_lean::{ }; use ream_post_quantum_crypto::leansig::public_key::PublicKey; use serde::Deserialize; +#[cfg(feature = "devnet2")] +use ssz_types::BitList; use ssz_types::VariableList; +#[cfg(feature = "devnet2")] +use ssz_types::typenum::U4096; /// A leanSpec test fixture file contains a map of test IDs to test cases pub type TestFixture = HashMap; @@ -78,7 +86,16 @@ pub struct Block { /// Block body #[derive(Debug, Deserialize)] pub struct BlockBody { + #[cfg(feature = "devnet1")] pub attestations: DataList, + #[cfg(feature = "devnet2")] + pub attestations: DataList, +} + +#[derive(Debug, Deserialize)] +pub struct AggregatedAttestationJSON { + pub aggregation_bits: String, + pub message: AttestationData, } /// Attestation @@ -186,23 +203,44 @@ impl TryFrom<&Block> for ReamBlock { type Error = anyhow::Error; fn try_from(block: &Block) -> anyhow::Result { - let attestations: Vec = block - .body - .attestations - .data - .iter() - .map(ReamAttestation::from) - .collect(); + #[cfg(feature = "devnet1")] + let attestations = { + let list: Vec = block + .body + .attestations + .data + .iter() + .map(ReamAttestation::from) + .collect(); + VariableList::try_from(list) + .map_err(|err| anyhow!("Failed to create attestations VariableList: {err}"))? + }; + + #[cfg(feature = "devnet2")] + let attestations = { + let mut list = Vec::new(); + for aggregated in &block.body.attestations.data { + let bytes = hex::decode(aggregated.aggregation_bits.trim_start_matches("0x")) + .map_err(|err| anyhow!("Failed to decode hex: {err}"))?; + + let aggregation_bits = BitList::::from_bytes(bytes.into()) + .map_err(|err| anyhow!("BitList error: {err:?}"))?; + + list.push(ream_consensus_lean::attestation::AggregatedAttestation { + aggregation_bits, + message: aggregated.message.clone(), + }); + } + VariableList::try_from(list) + .map_err(|err| anyhow!("Failed to create attestations VariableList: {err}"))? + }; Ok(ReamBlock { slot: block.slot, proposer_index: block.proposer_index, parent_root: block.parent_root, state_root: block.state_root, - body: ReamBlockBody { - attestations: VariableList::try_from(attestations) - .map_err(|err| anyhow!("Failed to create attestations VariableList: {err}"))?, - }, + body: ReamBlockBody { attestations }, }) } }