diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..60309d6 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,24 @@ +name: CI +on: + - pull_request + +jobs: + ci: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Check formatting + run: make check-format + + - name: Build client + run: make build + + - name: Run tests + run: make test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c1f2b7f --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: build format check-format test + +all: + $(MAKE) -C lean_client all + +build: + $(MAKE) -C lean_client build + +format: + $(MAKE) -C lean_client format + +check-format: + $(MAKE) -C lean_client check-format + +test: + $(MAKE) -C lean_client test diff --git a/lean_client/Makefile b/lean_client/Makefile new file mode 100644 index 0000000..7430615 --- /dev/null +++ b/lean_client/Makefile @@ -0,0 +1,15 @@ +.PHONY: build format check-format test + +all: build check-format test + +build: + cargo build --release + +format: + cargo fmt + +check-format: + cargo fmt --check + +test: + cargo test --workspace --all-features --no-fail-fast diff --git a/lean_client/chain/src/config.rs b/lean_client/chain/src/config.rs index b8be4f1..ca4edb2 100644 --- a/lean_client/chain/src/config.rs +++ b/lean_client/chain/src/config.rs @@ -4,9 +4,16 @@ pub struct BasisPoint(pub u64); impl BasisPoint { pub const MAX: u64 = 10_000; pub const fn new(value: u64) -> Option { - if value <= Self::MAX { Some(BasisPoint(value)) } else { None } + if value <= Self::MAX { + Some(BasisPoint(value)) + } else { + None + } + } + #[inline] + pub fn get(&self) -> u64 { + self.0 } - #[inline] pub fn get(&self) -> u64 { self.0 } } pub const INTERVALS_PER_SLOT: u64 = 4; @@ -15,12 +22,24 @@ pub const SECONDS_PER_SLOT: u64 = SLOT_DURATION_MS / 1_000; pub const SECONDS_PER_INTERVAL: u64 = SECONDS_PER_SLOT / INTERVALS_PER_SLOT; pub const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3; -pub const PROPOSER_REORG_CUTOFF_BPS: BasisPoint = match BasisPoint::new(2_500) { Some(x) => x, None => panic!() }; -pub const VOTE_DUE_BPS: BasisPoint = match BasisPoint::new(5_000) { Some(x) => x, None => panic!() }; -pub const FAST_CONFIRM_DUE_BPS: BasisPoint = match BasisPoint::new(7_500) { Some(x) => x, None => panic!() }; -pub const VIEW_FREEZE_CUTOFF_BPS: BasisPoint= match BasisPoint::new(7_500) { Some(x) => x, None => panic!() }; +pub const PROPOSER_REORG_CUTOFF_BPS: BasisPoint = match BasisPoint::new(2_500) { + Some(x) => x, + None => panic!(), +}; +pub const VOTE_DUE_BPS: BasisPoint = match BasisPoint::new(5_000) { + Some(x) => x, + None => panic!(), +}; +pub const FAST_CONFIRM_DUE_BPS: BasisPoint = match BasisPoint::new(7_500) { + Some(x) => x, + None => panic!(), +}; +pub const VIEW_FREEZE_CUTOFF_BPS: BasisPoint = match BasisPoint::new(7_500) { + Some(x) => x, + None => panic!(), +}; -pub const HISTORICAL_ROOTS_LIMIT: u64 = 1u64 << 18; +pub const HISTORICAL_ROOTS_LIMIT: u64 = 1u64 << 18; pub const VALIDATOR_REGISTRY_LIMIT: u64 = 1u64 << 12; #[derive(Clone, Debug)] @@ -51,9 +70,10 @@ pub const DEVNET_CONFIG: ChainConfig = ChainConfig { #[cfg(test)] mod tests { use super::*; - #[test] fn time_math_is_consistent() { + #[test] + fn time_math_is_consistent() { assert_eq!(SLOT_DURATION_MS, 4_000); assert_eq!(SECONDS_PER_SLOT, 4); assert_eq!(SECONDS_PER_INTERVAL, 1); } -} \ No newline at end of file +} diff --git a/lean_client/containers/Cargo.toml b/lean_client/containers/Cargo.toml index 011bb4e..2d5b0ff 100644 --- a/lean_client/containers/Cargo.toml +++ b/lean_client/containers/Cargo.toml @@ -11,8 +11,8 @@ name = "containers" path = "src/lib.rs" [dependencies] -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 } +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" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index 6ad0f56..ae1b88c 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -2,7 +2,7 @@ use crate::{Checkpoint, Slot, Uint64}; use serde::{Deserialize, Serialize}; 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; diff --git a/lean_client/containers/src/block.rs b/lean_client/containers/src/block.rs index 9c0a1de..5df3b22 100644 --- a/lean_client/containers/src/block.rs +++ b/lean_client/containers/src/block.rs @@ -1,4 +1,6 @@ -use crate::{Attestation, Attestations, BlockSignatures, Bytes32, Signature, Slot, State, ValidatorIndex}; +use crate::{ + Attestation, Attestations, BlockSignatures, Bytes32, Signature, Slot, State, ValidatorIndex, +}; use serde::{Deserialize, Serialize}; use ssz_derive::Ssz; @@ -123,7 +125,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 +135,7 @@ impl SignedBlockWithAttestation { } i += 1; } - + // Append proposer attestation all_attestations.push(self.message.proposer_attestation.clone()); @@ -193,49 +195,58 @@ 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; - + use leansig::signature::SignatureScheme; + // 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); + 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); + 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); + eprintln!( + "XMSS signature verification failed at slot {:?}", + attestation.data.slot + ); return false; } } - + #[cfg(not(feature = "xmss-verify"))] { // Placeholder: XMSS verification disabled @@ -249,4 +260,4 @@ impl SignedBlockWithAttestation { true } -} \ No newline at end of file +} diff --git a/lean_client/containers/src/checkpoint.rs b/lean_client/containers/src/checkpoint.rs index e635ab1..1b36f31 100644 --- a/lean_client/containers/src/checkpoint.rs +++ b/lean_client/containers/src/checkpoint.rs @@ -1,9 +1,9 @@ use crate::{Bytes32, Slot}; -use ssz_derive::Ssz; use serde::{Deserialize, Serialize}; +use ssz_derive::Ssz; /// Represents a checkpoint in the chain's history. -/// +/// /// A checkpoint marks a specific moment in the chain. It combines a block /// identifier with a slot number. Checkpoints are used for justification and /// finalization. @@ -45,4 +45,4 @@ mod tests { }; assert_eq!(cp1, cp2); } -} \ No newline at end of file +} diff --git a/lean_client/containers/src/config.rs b/lean_client/containers/src/config.rs index 83bf459..fed2b7e 100644 --- a/lean_client/containers/src/config.rs +++ b/lean_client/containers/src/config.rs @@ -1,5 +1,5 @@ -use ssz_derive::Ssz; use serde::{Deserialize, Serialize}; +use ssz_derive::Ssz; use std::fs::File; use std::io::BufReader; use std::path::Path; @@ -26,4 +26,4 @@ impl GenesisConfig { let config = serde_yaml::from_reader(reader)?; Ok(config) } -} \ No newline at end of file +} diff --git a/lean_client/containers/src/lib.rs b/lean_client/containers/src/lib.rs index 511db23..28b13d1 100644 --- a/lean_client/containers/src/lib.rs +++ b/lean_client/containers/src/lib.rs @@ -22,8 +22,8 @@ pub use slot::Slot; pub use state::State; pub use status::Status; pub use types::{ - Bytes32, HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, Validators, - Uint64, ValidatorIndex, + Bytes32, HistoricalBlockHashes, JustificationRoots, JustificationsValidators, JustifiedSlots, + Uint64, ValidatorIndex, Validators, }; pub use types::Bytes32 as Root; diff --git a/lean_client/containers/src/serde_helpers.rs b/lean_client/containers/src/serde_helpers.rs index aff4d60..7cff787 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,10 @@ 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::{BlockSignatures, Signature}; use serde_json::Value; - + use ssz::PersistentList; + /// Structured XMSS signature format from test vectors #[derive(Deserialize, Clone)] struct XmssSignature { @@ -197,55 +198,53 @@ 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 @@ -253,22 +252,23 @@ pub mod block_signatures { 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 where S: Serializer, @@ -285,7 +285,7 @@ pub mod block_signatures { Err(_) => break, } } - + let wrapper = DataWrapper { data: sigs }; wrapper.serialize(serializer) } diff --git a/lean_client/containers/src/slot.rs b/lean_client/containers/src/slot.rs index d845ec3..17f5439 100644 --- a/lean_client/containers/src/slot.rs +++ b/lean_client/containers/src/slot.rs @@ -1,5 +1,5 @@ -use ssz_derive::Ssz; use serde::{Deserialize, Serialize}; +use ssz_derive::Ssz; use std::cmp::Ordering; #[derive(Clone, Copy, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] @@ -37,15 +37,18 @@ impl Slot { /// /// Panics if this slot is earlier than the finalized slot. pub fn is_justifiable_after(self, finalized: Slot) -> bool { - assert!(self >= finalized, "Candidate slot must not be before finalized slot"); + assert!( + self >= finalized, + "Candidate slot must not be before finalized slot" + ); let delta = self.0 - finalized.0; - + // Rule 1: The first 5 slots after finalization are always justifiable. // Examples: delta = 0, 1, 2, 3, 4, 5 if delta <= 5 { return true; } - + // Rule 2: Slots at perfect square distances are justifiable. // Examples: delta = 1, 4, 9, 16, 25, 36, 49, 64, ... // Check: integer square root squared equals delta @@ -53,7 +56,7 @@ impl Slot { if sqrt * sqrt == delta { return true; } - + // Rule 3: Slots at pronic number distances are justifiable. // Pronic numbers have the form n(n+1): 2, 6, 12, 20, 30, 42, 56, ... // Mathematical insight: For pronic delta = n(n+1), we have: @@ -64,7 +67,7 @@ impl Slot { if test_sqrt * test_sqrt == test && test_sqrt % 2 == 1 { return true; } - + false } } @@ -89,32 +92,32 @@ mod tests { fn test_is_justifiable_perfect_squares() { let finalized = Slot(0); // Rule 2: Perfect square distances - assert!(Slot(1).is_justifiable_after(finalized)); // delta = 1 = 1^2 - assert!(Slot(4).is_justifiable_after(finalized)); // delta = 4 = 2^2 - assert!(Slot(9).is_justifiable_after(finalized)); // delta = 9 = 3^2 - assert!(Slot(16).is_justifiable_after(finalized)); // delta = 16 = 4^2 - assert!(Slot(25).is_justifiable_after(finalized)); // delta = 25 = 5^2 - assert!(Slot(36).is_justifiable_after(finalized)); // delta = 36 = 6^2 + assert!(Slot(1).is_justifiable_after(finalized)); // delta = 1 = 1^2 + assert!(Slot(4).is_justifiable_after(finalized)); // delta = 4 = 2^2 + assert!(Slot(9).is_justifiable_after(finalized)); // delta = 9 = 3^2 + assert!(Slot(16).is_justifiable_after(finalized)); // delta = 16 = 4^2 + assert!(Slot(25).is_justifiable_after(finalized)); // delta = 25 = 5^2 + assert!(Slot(36).is_justifiable_after(finalized)); // delta = 36 = 6^2 } #[test] fn test_is_justifiable_pronic() { let finalized = Slot(0); // Rule 3: Pronic numbers (n(n+1)) - assert!(Slot(2).is_justifiable_after(finalized)); // delta = 2 = 1*2 - assert!(Slot(6).is_justifiable_after(finalized)); // delta = 6 = 2*3 - assert!(Slot(12).is_justifiable_after(finalized)); // delta = 12 = 3*4 - assert!(Slot(20).is_justifiable_after(finalized)); // delta = 20 = 4*5 - assert!(Slot(30).is_justifiable_after(finalized)); // delta = 30 = 5*6 - assert!(Slot(42).is_justifiable_after(finalized)); // delta = 42 = 6*7 + assert!(Slot(2).is_justifiable_after(finalized)); // delta = 2 = 1*2 + assert!(Slot(6).is_justifiable_after(finalized)); // delta = 6 = 2*3 + assert!(Slot(12).is_justifiable_after(finalized)); // delta = 12 = 3*4 + assert!(Slot(20).is_justifiable_after(finalized)); // delta = 20 = 4*5 + assert!(Slot(30).is_justifiable_after(finalized)); // delta = 30 = 5*6 + assert!(Slot(42).is_justifiable_after(finalized)); // delta = 42 = 6*7 } #[test] fn test_is_not_justifiable() { let finalized = Slot(0); // Not justifiable: not in first 5, not perfect square, not pronic - assert!(!Slot(7).is_justifiable_after(finalized)); // delta = 7 - assert!(!Slot(8).is_justifiable_after(finalized)); // delta = 8 + assert!(!Slot(7).is_justifiable_after(finalized)); // delta = 7 + assert!(!Slot(8).is_justifiable_after(finalized)); // delta = 8 assert!(!Slot(10).is_justifiable_after(finalized)); // delta = 10 assert!(!Slot(11).is_justifiable_after(finalized)); // delta = 11 } @@ -126,4 +129,4 @@ mod tests { let candidate = Slot(50); candidate.is_justifiable_after(finalized); } -} \ No newline at end of file +} diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 4eb0ffd..7ff8456 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -1,11 +1,14 @@ use crate::validator::Validator; use crate::{ block::{hash_tree_root, Block, BlockBody, BlockHeader, SignedBlockWithAttestation}, - Attestation, Attestations, BlockSignatures, Bytes32, Checkpoint, Config, Slot, Uint64, ValidatorIndex, + Attestation, Attestations, BlockSignatures, Bytes32, Checkpoint, Config, Slot, Uint64, + ValidatorIndex, +}; +use crate::{ + 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; use ssz_derive::Ssz; use std::collections::BTreeMap; @@ -47,7 +50,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 +70,6 @@ impl State { validator_list.push(v).expect("Failed to add validator"); } - Self { config: Config { genesis_time: genesis_time.0, @@ -206,7 +211,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 +239,11 @@ impl State { } // updated for fork choice tests - pub fn state_transition(&self, signed_block: SignedBlockWithAttestation, valid_signatures: bool) -> Result { + pub fn state_transition( + &self, + signed_block: SignedBlockWithAttestation, + valid_signatures: bool, + ) -> Result { self.state_transition_with_validation(signed_block, valid_signatures, true) } @@ -314,7 +327,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")); @@ -581,7 +594,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,7 +681,9 @@ 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))?; } } } @@ -729,11 +746,11 @@ mod tests { 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 +760,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 +807,7 @@ mod tests { }, }, }; - + let result = genesis_state.build_block( Slot(1), ValidatorIndex(1), @@ -792,45 +816,48 @@ mod tests { None, None, ); - - assert!(result.is_ok(), "Building block with attestations should succeed"); + + assert!( + result.is_ok(), + "Building block with attestations should succeed" + ); let (block, _post_state, attestations, _signatures) = result.unwrap(); - + // Verify attestation was included assert_eq!(attestations.len(), 1); assert_eq!(attestations[0].validator_id, Uint64(0)); // Check that attestation list has one element - assert!(block.body.attestations.get(0).is_ok(), "Block should contain attestation"); - assert!(block.body.attestations.get(1).is_err(), "Block should have only one attestation"); + assert!( + block.body.attestations.get(0).is_ok(), + "Block should contain attestation" + ); + assert!( + block.body.attestations.get(1).is_err(), + "Block should have only one attestation" + ); } #[test] fn test_build_block_advances_state() { // Create genesis state let genesis_state = State::generate_genesis(Uint64(0), Uint64(10)); - + // Compute parent root after advancing to target slot let pre_state = genesis_state.process_slots(Slot(5)).unwrap(); let parent_root = hash_tree_root(&pre_state.latest_block_header); - + // Build block at slot 5 // Proposer for slot 5 with 10 validators is (5 % 10) = 5 - let result = genesis_state.build_block( - Slot(5), - ValidatorIndex(5), - parent_root, - None, - None, - None, - ); - + let result = + genesis_state.build_block(Slot(5), ValidatorIndex(5), parent_root, None, None, None); + assert!(result.is_ok()); let (block, post_state, _, _) = result.unwrap(); - + // Verify state advanced through slots assert_eq!(post_state.slot, Slot(5)); assert_eq!(block.slot, Slot(5)); - + // Verify block can be applied to genesis state let transition_result = genesis_state.state_transition_with_validation( SignedBlockWithAttestation { @@ -843,44 +870,40 @@ mod tests { true, // signatures are considered valid (not validating, just marking as valid) true, ); - - assert!(transition_result.is_ok(), "Built block should be valid for state transition"); + + assert!( + transition_result.is_ok(), + "Built block should be valid for state transition" + ); } #[test] fn test_build_block_state_root_matches() { // Create genesis state let genesis_state = State::generate_genesis(Uint64(0), Uint64(3)); - + // Compute parent root after advancing to target slot let pre_state = genesis_state.process_slots(Slot(1)).unwrap(); let parent_root = hash_tree_root(&pre_state.latest_block_header); - + // Build a block // Proposer for slot 1 with 3 validators is (1 % 3) = 1 - let result = genesis_state.build_block( - Slot(1), - ValidatorIndex(1), - parent_root, - None, - None, - None, - ); - + let result = + genesis_state.build_block(Slot(1), ValidatorIndex(1), parent_root, None, None, None); + assert!(result.is_ok()); let (block, post_state, _, _) = result.unwrap(); - + // Verify the state root in block matches the computed post-state let computed_state_root = hash_tree_root(&post_state); assert_eq!( - block.state_root, - computed_state_root, + block.state_root, computed_state_root, "Block state root should match computed post-state root" ); - + // Verify it's not zero assert_ne!( - block.state_root, + block.state_root, Bytes32(ssz::H256::zero()), "State root should not be zero" ); diff --git a/lean_client/containers/src/status.rs b/lean_client/containers/src/status.rs index da05ba1..d68c7c3 100644 --- a/lean_client/containers/src/status.rs +++ b/lean_client/containers/src/status.rs @@ -1,6 +1,6 @@ use crate::Checkpoint; -use ssz_derive::Ssz; use serde::{Deserialize, Serialize}; +use ssz_derive::Ssz; #[derive(Clone, Debug, PartialEq, Eq, Ssz, Default, Serialize, Deserialize)] pub struct Status { diff --git a/lean_client/containers/src/types.rs b/lean_client/containers/src/types.rs index bb2a64b..7d9aa4d 100644 --- a/lean_client/containers/src/types.rs +++ b/lean_client/containers/src/types.rs @@ -38,8 +38,8 @@ impl fmt::Display for Bytes32 { } // Type-level constants for SSZ collection limits -use typenum::{Prod, U4, U1000, U4096, U262144, U1073741824}; use crate::validator::Validator; +use typenum::{Prod, U1000, U1073741824, U262144, U4, U4096}; // 2^18, 4096 * 262144 /// Type-level number for 4000 bytes (signature size) = 4 * 1000 diff --git a/lean_client/containers/src/validator.rs b/lean_client/containers/src/validator.rs index 513b09d..2649f55 100644 --- a/lean_client/containers/src/validator.rs +++ b/lean_client/containers/src/validator.rs @@ -23,10 +23,7 @@ impl Serialize for BlsPublicKey { // ByteVector might have to_vec() or similar // For now, use unsafe to access the underlying bytes let bytes = unsafe { - std::slice::from_raw_parts( - &self.0 as *const ByteVector as *const u8, - 52 - ) + std::slice::from_raw_parts(&self.0 as *const ByteVector as *const u8, 52) }; let hex_string = format!("0x{}", hex::encode(bytes)); serializer.serialize_str(&hex_string) @@ -40,7 +37,7 @@ impl<'de> Deserialize<'de> for BlsPublicKey { { let s = String::deserialize(deserializer)?; let s = s.strip_prefix("0x").unwrap_or(&s); - + let decoded = hex::decode(s).map_err(serde::de::Error::custom)?; if decoded.len() != 52 { return Err(serde::de::Error::custom(format!( @@ -48,14 +45,14 @@ impl<'de> Deserialize<'de> for BlsPublicKey { decoded.len() ))); } - + // Create ByteVector from decoded bytes using unsafe let mut byte_vec = ByteVector::default(); unsafe { let dest = &mut byte_vec as *mut ByteVector as *mut u8; std::ptr::copy_nonoverlapping(decoded.as_ptr(), dest, 52); } - + Ok(BlsPublicKey(byte_vec)) } } diff --git a/lean_client/containers/tests/debug_deserialize.rs b/lean_client/containers/tests/debug_deserialize.rs index 0b5a6de..1d28df1 100644 --- a/lean_client/containers/tests/debug_deserialize.rs +++ b/lean_client/containers/tests/debug_deserialize.rs @@ -4,45 +4,52 @@ use std::fs; #[test] fn debug_deserialize_state() { let json_content = fs::read_to_string( - "../tests/test_vectors/test_blocks/test_process_first_block_after_genesis.json" - ).expect("Failed to read test vector file"); - + "../tests/test_vectors/test_blocks/test_process_first_block_after_genesis.json", + ) + .expect("Failed to read test vector file"); + // Try to deserialize just to see where it fails let result: Result = serde_json::from_str(&json_content); - + match result { Ok(value) => { println!("✓ JSON is valid"); - + // Try to extract just the pre state if let Some(tests) = value.as_object() { if let Some((test_name, test_case)) = tests.iter().next() { println!("✓ Found test: {}", test_name); - + if let Some(pre) = test_case.get("pre") { println!("✓ Found pre state"); - + // Try deserializing field by field if let Some(pre_obj) = pre.as_object() { for (field_name, field_value) in pre_obj.iter() { println!("\nTrying to deserialize field: {}", field_name); - println!("Field value type: {}", match field_value { - serde_json::Value::Null => "null", - serde_json::Value::Bool(_) => "bool", - serde_json::Value::Number(_) => "number", - serde_json::Value::String(_) => "string", - serde_json::Value::Array(_) => "array", - serde_json::Value::Object(_) => "object", - }); - + println!( + "Field value type: {}", + match field_value { + serde_json::Value::Null => "null", + serde_json::Value::Bool(_) => "bool", + serde_json::Value::Number(_) => "number", + serde_json::Value::String(_) => "string", + serde_json::Value::Array(_) => "array", + serde_json::Value::Object(_) => "object", + } + ); + if field_value.is_object() { if let Some(obj) = field_value.as_object() { - println!("Object keys: {:?}", obj.keys().collect::>()); + println!( + "Object keys: {:?}", + obj.keys().collect::>() + ); } } } } - + // Now try to deserialize the whole state let state_result: Result = serde_json::from_value(pre.clone()); match state_result { diff --git a/lean_client/containers/tests/main.rs b/lean_client/containers/tests/main.rs index 96deacd..ee67df1 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 mod debug_deserialize; +mod test_vectors; mod unit_tests; -mod test_vectors; \ No newline at end of file diff --git a/lean_client/containers/tests/test_vectors/block_processing.rs b/lean_client/containers/tests/test_vectors/block_processing.rs index 4dcd641..d2e1c9e 100644 --- a/lean_client/containers/tests/test_vectors/block_processing.rs +++ b/lean_client/containers/tests/test_vectors/block_processing.rs @@ -11,8 +11,7 @@ fn test_process_first_block_after_genesis() { #[test] fn test_blocks_with_gaps() { let test_path = "../tests/test_vectors/test_blocks/test_blocks_with_gaps.json"; - TestRunner::run_block_processing_test(test_path) - .expect("test_blocks_with_gaps failed"); + TestRunner::run_block_processing_test(test_path).expect("test_blocks_with_gaps failed"); } #[test] @@ -25,15 +24,13 @@ fn test_linear_chain_multiple_blocks() { #[test] fn test_block_extends_deep_chain() { let test_path = "../tests/test_vectors/test_blocks/test_block_extends_deep_chain.json"; - TestRunner::run_block_processing_test(test_path) - .expect("test_block_extends_deep_chain failed"); + TestRunner::run_block_processing_test(test_path).expect("test_block_extends_deep_chain failed"); } #[test] fn test_empty_blocks() { let test_path = "../tests/test_vectors/test_blocks/test_empty_blocks.json"; - TestRunner::run_block_processing_test(test_path) - .expect("test_empty_blocks failed"); + TestRunner::run_block_processing_test(test_path).expect("test_empty_blocks failed"); } #[test] diff --git a/lean_client/containers/tests/test_vectors/genesis.rs b/lean_client/containers/tests/test_vectors/genesis.rs index 0b1d3d3..92acf25 100644 --- a/lean_client/containers/tests/test_vectors/genesis.rs +++ b/lean_client/containers/tests/test_vectors/genesis.rs @@ -4,20 +4,17 @@ use super::runner::TestRunner; #[test] fn test_genesis_default_configuration() { let test_path = "../tests/test_vectors/test_genesis/test_genesis_default_configuration.json"; - TestRunner::run_genesis_test(test_path) - .expect("test_genesis_default_configuration failed"); + TestRunner::run_genesis_test(test_path).expect("test_genesis_default_configuration failed"); } #[test] fn test_genesis_custom_time() { let test_path = "../tests/test_vectors/test_genesis/test_genesis_custom_time.json"; - TestRunner::run_genesis_test(test_path) - .expect("test_genesis_custom_time failed"); + TestRunner::run_genesis_test(test_path).expect("test_genesis_custom_time failed"); } #[test] fn test_genesis_custom_validator_set() { let test_path = "../tests/test_vectors/test_genesis/test_genesis_custom_validator_set.json"; - TestRunner::run_genesis_test(test_path) - .expect("test_genesis_custom_validator_set failed"); + TestRunner::run_genesis_test(test_path).expect("test_genesis_custom_validator_set failed"); } diff --git a/lean_client/containers/tests/test_vectors/mod.rs b/lean_client/containers/tests/test_vectors/mod.rs index 8859847..acdc055 100644 --- a/lean_client/containers/tests/test_vectors/mod.rs +++ b/lean_client/containers/tests/test_vectors/mod.rs @@ -1,15 +1,13 @@ // Test vector modules -pub mod runner; pub mod block_processing; pub mod genesis; +pub mod runner; pub mod verify_signatures; +use containers::{block::Block, block::SignedBlockWithAttestation, state::State, Slot}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::collections::HashMap; -use containers::{ - Slot, block::Block, block::SignedBlockWithAttestation, state::State -}; /// Custom deserializer that handles both plain values and {"data": T} wrapper format fn deserialize_flexible<'de, D, T>(deserializer: D) -> Result @@ -18,21 +16,22 @@ where T: serde::de::DeserializeOwned, { use serde::de::Error; - + // Deserialize as a generic Value first to inspect the structure let value = Value::deserialize(deserializer)?; - + // Check if it's an object with a "data" field if let Value::Object(ref map) = value { if map.contains_key("data") && map.len() == 1 { // Extract just the data field if let Some(data_value) = map.get("data") { - return serde_json::from_value(data_value.clone()) - .map_err(|e| D::Error::custom(format!("Failed to deserialize from data wrapper: {}", e))); + return serde_json::from_value(data_value.clone()).map_err(|e| { + D::Error::custom(format!("Failed to deserialize from data wrapper: {}", e)) + }); } } } - + // Otherwise, deserialize as a plain value serde_json::from_value(value) .map_err(|e| D::Error::custom(format!("Failed to deserialize plain value: {}", e))) diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index 9e7ef36..3f1b7dd 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -6,14 +6,19 @@ use std::path::Path; pub struct TestRunner; impl TestRunner { - pub fn run_sequential_block_processing_tests>(path: P) -> Result<(), Box> { + pub fn run_sequential_block_processing_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; println!("Running test: {}", test_name); @@ -78,7 +83,8 @@ impl TestRunner { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } // Only check validator count if specified in post-state @@ -95,14 +101,15 @@ impl TestRunner { Err(_) => break, } } - + if num_validators as usize != expected_count { return Err(format!( "Post-state validator count mismatch: expected {}, got {}", expected_count, num_validators - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); println!(" Validator count: {}", num_validators); @@ -111,38 +118,48 @@ impl TestRunner { println!(" Final slot: {:?}", state.slot); } } - + println!("\n✓✓✓ PASS: All blocks processed successfully with matching roots ✓✓✓"); } - + Ok(()) } - pub fn run_single_block_with_slot_gap_tests>(path: P) -> Result<(), Box> { + pub fn run_single_block_with_slot_gap_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; - + // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("Running test: {}", test_name); println!("Description: {}", test_case.info.description); if let Some(ref blocks) = test_case.blocks { let mut state = test_case.pre.clone(); - + for (idx, block) in blocks.iter().enumerate() { - println!("\nProcessing block {}: slot {:?} (gap from slot {:?})", idx + 1, block.slot, state.slot); - + println!( + "\nProcessing block {}: slot {:?} (gap from slot {:?})", + idx + 1, + block.slot, + state.slot + ); + // Advance state to the block's slot (this handles the slot gap) let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { return Err(format!( @@ -152,19 +169,19 @@ impl TestRunner { computed_parent_root ).into()); } - + println!(" ✓ Parent root matches: {:?}", computed_parent_root); - + // Process the block header let result = state_after_slots.process_block_header(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the vector if block.state_root != computed_state_root { return Err(format!( @@ -174,59 +191,69 @@ impl TestRunner { computed_state_root ).into()); } - + println!(" ✓ State root matches: {:?}", computed_state_root); - println!(" ✓ Block {} processed successfully (with {} empty slots)", idx + 1, block.slot.0 - test_case.pre.slot.0 - idx as u64); + println!( + " ✓ Block {} processed successfully (with {} empty slots)", + idx + 1, + block.slot.0 - test_case.pre.slot.0 - idx as u64 + ); } Err(e) => { return Err(format!("Block {} processing failed: {:?}", idx + 1, e).into()); } } } - + // Verify post-state conditions if let Some(post) = test_case.post { if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); } - + println!("\n✓✓✓ PASS: Block with slot gap processed successfully ✓✓✓"); } - + Ok(()) } - pub fn run_single_empty_block_tests>(path: P) -> Result<(), Box> { + pub fn run_single_empty_block_tests>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path)?; - + // Parse using the new TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("Running test: {}", test_name); println!("Description: {}", test_case.info.description); if let Some(ref blocks) = test_case.blocks { let mut state = test_case.pre.clone(); - + // Should be exactly one block if blocks.len() != 1 { return Err(format!("Expected 1 block, found {}", blocks.len()).into()); } - + let block = &blocks[0]; println!("\nProcessing single empty block at slot {:?}", block.slot); - + // Verify it's an empty block (no attestations) let attestation_count = { let mut count = 0u64; @@ -238,18 +265,22 @@ impl TestRunner { } count }; - + if attestation_count > 0 { - return Err(format!("Expected empty block, but found {} attestations", attestation_count).into()); + return Err(format!( + "Expected empty block, but found {} attestations", + attestation_count + ) + .into()); } println!(" ✓ Confirmed: Block has no attestations (empty block)"); - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { return Err(format!( @@ -258,19 +289,19 @@ impl TestRunner { computed_parent_root ).into()); } - + println!(" ✓ Parent root matches: {:?}", computed_parent_root); - + // Process the block header let result = state_after_slots.process_block_header(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the vector if block.state_root != computed_state_root { return Err(format!( @@ -279,7 +310,7 @@ impl TestRunner { computed_state_root ).into()); } - + println!(" ✓ State root matches: {:?}", computed_state_root); println!(" ✓ Empty block processed successfully"); } @@ -287,38 +318,44 @@ impl TestRunner { return Err(format!("Block processing failed: {:?}", e).into()); } } - + // Verify post-state conditions if let Some(post) = test_case.post { if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + println!("\n✓ All post-state checks passed"); println!(" Final slot: {:?}", state.slot); } - + println!("\n✓✓✓ PASS: Single empty block processed successfully ✓✓✓"); } - + Ok(()) } /// Generic test runner for block processing test vectors /// Handles all test vectors from test_blocks directory - pub fn run_block_processing_test>(path: P) -> Result<(), Box> { + pub fn run_block_processing_test>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the TestVectorFile structure with camelCase let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); // Check if this is an invalid/exception test @@ -338,7 +375,7 @@ impl TestRunner { } let mut state = test_case.pre.clone(); - + for (idx, block) in blocks.iter().enumerate() { // Check if this is a gap (missed slots) let gap_size = if idx == 0 { @@ -346,19 +383,24 @@ impl TestRunner { } else { block.slot.0 - state.slot.0 - 1 }; - + if gap_size > 0 { - println!(" Block {}: slot {} (gap: {} empty slots)", idx + 1, block.slot.0, gap_size); + println!( + " Block {}: slot {} (gap: {} empty slots)", + idx + 1, + block.slot.0, + gap_size + ); } else { println!(" Block {}: slot {}", idx + 1, block.slot.0); } - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Compute the parent root from our current latest_block_header let computed_parent_root = hash_tree_root(&state_after_slots.latest_block_header); - + // Verify the block's parent_root matches what we computed if block.parent_root != computed_parent_root { println!(" \x1b[31m✗ FAIL: Parent root mismatch\x1b[0m"); @@ -366,7 +408,7 @@ impl TestRunner { println!(" Got: {:?}\n", computed_parent_root); return Err(format!("Block {} parent_root mismatch", idx + 1).into()); } - + // Check if block is empty (no attestations) let attestation_count = { let mut count = 0u64; @@ -378,17 +420,17 @@ impl TestRunner { } count }; - + // Process the full block (header + operations) let result = state_after_slots.process_block(block); match result { Ok(new_state) => { state = new_state; - + // Compute the state root after processing let computed_state_root = hash_tree_root(&state); - + // Verify the computed state_root matches the expected one from the block if block.state_root != computed_state_root { println!(" \x1b[31m✗ FAIL: State root mismatch\x1b[0m"); @@ -396,7 +438,7 @@ impl TestRunner { println!(" Got: {:?}\n", computed_state_root); return Err(format!("Block {} state_root mismatch", idx + 1).into()); } - + if attestation_count > 0 { println!(" ✓ Processed with {} attestation(s)", attestation_count); } else { @@ -410,13 +452,13 @@ impl TestRunner { } } } - + // Verify post-state conditions Self::verify_post_state(&state, &test_case)?; - + println!("\n\x1b[32m✓ PASS\x1b[0m\n"); } - + Ok(()) } @@ -424,18 +466,21 @@ impl TestRunner { /// Handles test vectors from test_genesis directory pub fn run_genesis_test>(path: P) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the TestVectorFile structure let test_file: TestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); let state = &test_case.pre; - + // Count validators let mut num_validators: u64 = 0; let mut i: u64 = 0; @@ -448,37 +493,48 @@ impl TestRunner { Err(_) => break, } } - println!(" Genesis time: {}, slot: {}, validators: {}", state.config.genesis_time, state.slot.0, num_validators); - + println!( + " Genesis time: {}, slot: {}, validators: {}", + state.config.genesis_time, state.slot.0, num_validators + ); + // Verify it's at genesis (slot 0) if state.slot.0 != 0 { return Err(format!("Expected genesis at slot 0, got slot {}", state.slot.0).into()); } - + // Verify checkpoint initialization if state.latest_justified.slot.0 != 0 { - return Err(format!("Expected latest_justified at slot 0, got {}", state.latest_justified.slot.0).into()); + return Err(format!( + "Expected latest_justified at slot 0, got {}", + state.latest_justified.slot.0 + ) + .into()); } - + if state.latest_finalized.slot.0 != 0 { - return Err(format!("Expected latest_finalized at slot 0, got {}", state.latest_finalized.slot.0).into()); + return Err(format!( + "Expected latest_finalized at slot 0, got {}", + state.latest_finalized.slot.0 + ) + .into()); } - + // Verify empty historical data let has_history = state.historical_block_hashes.get(0).is_ok(); if has_history { return Err("Expected empty historical block hashes at genesis".into()); } - + println!(" ✓ Genesis state validated"); - + // Verify post-state if present if test_case.post.is_some() { Self::verify_post_state(state, &test_case)?; } - + println!("\n\x1b[32m✓ PASS\x1b[0m\n"); - + Ok(()) } @@ -495,10 +551,10 @@ impl TestRunner { for (idx, block) in blocks.iter().enumerate() { println!(" Block {}: slot {}", idx + 1, block.slot.0); - + // Advance state to the block's slot let state_after_slots = state.process_slots(block.slot)?; - + // Try to process the full block (header + body) - we expect this to fail let result = state_after_slots.process_block(block); @@ -506,14 +562,16 @@ impl TestRunner { Ok(new_state) => { // Block processing succeeded, now validate state root let computed_state_root = hash_tree_root(&new_state); - + if block.state_root != computed_state_root { error_occurred = true; println!(" ✓ Correctly rejected: Invalid block state root"); break; // Stop at first error } else { println!(" \x1b[31m✗ FAIL: Block processed successfully - but should have failed!\x1b[0m\n"); - return Err("Expected block processing to fail, but it succeeded".into()); + return Err( + "Expected block processing to fail, but it succeeded".into() + ); } } Err(e) => { @@ -523,36 +581,40 @@ impl TestRunner { } } } - + if !error_occurred { return Err("Expected an exception but all blocks processed successfully".into()); } } - + Ok(()) } /// Helper: Verify genesis state only (no blocks) fn verify_genesis_state(test_case: TestCase) -> Result<(), Box> { let state = &test_case.pre; - + // Verify post-state if present Self::verify_post_state(state, &test_case)?; - + Ok(()) } /// Helper: Verify post-state conditions - fn verify_post_state(state: &State, test_case: &TestCase) -> Result<(), Box> { + fn verify_post_state( + state: &State, + test_case: &TestCase, + ) -> Result<(), Box> { if let Some(ref post) = test_case.post { // Verify slot if state.slot != post.slot { return Err(format!( "Post-state slot mismatch: expected {:?}, got {:?}", post.slot, state.slot - ).into()); + ) + .into()); } - + // Verify validator count if specified if let Some(expected_count) = post.validator_count { let mut num_validators: u64 = 0; @@ -566,54 +628,75 @@ impl TestRunner { Err(_) => break, } } - + if num_validators as usize != expected_count { return Err(format!( "Post-state validator count mismatch: expected {}, got {}", expected_count, num_validators - ).into()); + ) + .into()); } - println!(" ✓ Post-state verified: slot {}, {} validators", state.slot.0, num_validators); + println!( + " ✓ Post-state verified: slot {}, {} validators", + state.slot.0, num_validators + ); } else { println!(" ✓ Post-state verified: slot {}", state.slot.0); } } - + Ok(()) } /// Test runner for verify_signatures test vectors /// Tests XMSS signature verification on SignedBlockWithAttestation - pub fn run_verify_signatures_test>(path: P) -> Result<(), Box> { + pub fn run_verify_signatures_test>( + path: P, + ) -> Result<(), Box> { let json_content = fs::read_to_string(path.as_ref())?; - + // Parse using the VerifySignaturesTestVectorFile structure let test_file: VerifySignaturesTestVectorFile = serde_json::from_str(&json_content)?; - + // Get the first (and only) test case from the file - let (test_name, test_case) = test_file.tests.into_iter().next() + let (test_name, test_case) = test_file + .tests + .into_iter() + .next() .ok_or("No test case found in JSON")?; - + println!("\n{}: {}", test_name, test_case.info.description); - + let anchor_state = test_case.anchor_state; let signed_block = test_case.signed_block_with_attestation; - + // Print some debug info about what we're verifying println!(" Block slot: {}", signed_block.message.block.slot.0); - println!(" Proposer index: {}", signed_block.message.block.proposer_index.0); - + println!( + " Proposer index: {}", + signed_block.message.block.proposer_index.0 + ); + // Count attestations let mut attestation_count = 0u64; loop { - match signed_block.message.block.body.attestations.get(attestation_count) { + match signed_block + .message + .block + .body + .attestations + .get(attestation_count) + { Ok(_) => attestation_count += 1, Err(_) => break, } } println!(" Attestations in block: {}", attestation_count); - println!(" Proposer attestation validator: {}", signed_block.message.proposer_attestation.validator_id.0); - + println!( + " Proposer attestation validator: {}", + signed_block.message.proposer_attestation.validator_id.0 + ); + // Count signatures let mut signature_count = 0u64; loop { @@ -623,14 +706,14 @@ impl TestRunner { } } println!(" Signatures: {}", signature_count); - + // Check if we expect this test to fail if let Some(ref exception) = test_case.expect_exception { println!(" Expecting exception: {}", exception); - + // Verify signatures - we expect this to fail (return false) let result = signed_block.verify_signatures(anchor_state); - + if result { println!(" \x1b[31m✗ FAIL: Signatures verified successfully but should have failed!\x1b[0m\n"); return Err("Expected signature verification to fail, but it succeeded".into()); @@ -641,7 +724,7 @@ impl TestRunner { } else { // Valid test case - signatures should verify successfully let result = signed_block.verify_signatures(anchor_state); - + if result { println!(" ✓ All signatures verified successfully"); println!("\n\x1b[32m✓ PASS\x1b[0m\n"); @@ -650,8 +733,7 @@ impl TestRunner { return Err("Signature verification failed".into()); } } - + Ok(()) } - } diff --git a/lean_client/containers/tests/test_vectors/verify_signatures.rs b/lean_client/containers/tests/test_vectors/verify_signatures.rs index 2bca4ca..81b965f 100644 --- a/lean_client/containers/tests/test_vectors/verify_signatures.rs +++ b/lean_client/containers/tests/test_vectors/verify_signatures.rs @@ -17,8 +17,7 @@ use super::runner::TestRunner; #[test] fn test_proposer_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_valid_signatures/test_proposer_signature.json"; - TestRunner::run_verify_signatures_test(test_path) - .expect("test_proposer_signature failed"); + TestRunner::run_verify_signatures_test(test_path).expect("test_proposer_signature failed"); } #[test] @@ -37,8 +36,7 @@ fn test_proposer_and_attester_signatures() { #[ignore = "Requires xmss-verify feature for actual signature validation. Run with: cargo test --features xmss-verify"] fn test_invalid_signature() { let test_path = "../tests/test_vectors/test_verify_signatures/test_invalid_signatures/test_invalid_signature.json"; - TestRunner::run_verify_signatures_test(test_path) - .expect("test_invalid_signature failed"); + TestRunner::run_verify_signatures_test(test_path).expect("test_invalid_signature failed"); } #[test] diff --git a/lean_client/containers/tests/unit_tests/common.rs b/lean_client/containers/tests/unit_tests/common.rs index 77c2dd5..1535732 100644 --- a/lean_client/containers/tests/unit_tests/common.rs +++ b/lean_client/containers/tests/unit_tests/common.rs @@ -1,5 +1,11 @@ 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 + block::{hash_tree_root, Block, BlockBody, BlockHeader}, + checkpoint::Checkpoint, + slot::Slot, + state::State, + types::{Bytes32, ValidatorIndex}, + Attestation, Attestations, BlockSignatures, BlockWithAttestation, Config, + SignedBlockWithAttestation, Validators, }; use ssz::PersistentList as List; @@ -10,7 +16,11 @@ pub const TEST_VALIDATOR_COUNT: usize = 4; // Actual validator count used in tes const _: [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT] = [(); DEVNET_CONFIG_VALIDATOR_REGISTRY_LIMIT - TEST_VALIDATOR_COUNT]; -pub fn create_block(slot: u64, parent_header: &mut BlockHeader, attestations: Option) -> SignedBlockWithAttestation { +pub fn create_block( + slot: u64, + parent_header: &mut BlockHeader, + attestations: Option, +) -> SignedBlockWithAttestation { let body = BlockBody { attestations: attestations.unwrap_or_else(List::default), }; @@ -64,8 +74,11 @@ pub fn base_state(config: Config) -> State { } pub fn base_state_with_validators(config: Config, num_validators: usize) -> State { - use containers::{HistoricalBlockHashes, JustificationRoots, JustifiedSlots, JustificationsValidators, validator::Validator, Uint64}; - + use containers::{ + validator::Validator, HistoricalBlockHashes, JustificationRoots, JustificationsValidators, + JustifiedSlots, Uint64, + }; + // Create validators list with the specified number of validators let mut validators = Validators::default(); for i in 0..num_validators { @@ -75,7 +88,7 @@ pub fn base_state_with_validators(config: Config, num_validators: usize) -> Stat }; validators.push(validator).expect("within limit"); } - + State { config, slot: Slot(0), @@ -91,7 +104,5 @@ pub fn base_state_with_validators(config: Config, num_validators: usize) -> Stat } pub fn sample_config() -> Config { - Config { - genesis_time: 0, - } -} \ No newline at end of file + Config { genesis_time: 0 } +} diff --git a/lean_client/containers/tests/unit_tests/state_basic.rs b/lean_client/containers/tests/unit_tests/state_basic.rs index 5fa16e1..085384a 100644 --- a/lean_client/containers/tests/unit_tests/state_basic.rs +++ b/lean_client/containers/tests/unit_tests/state_basic.rs @@ -1,5 +1,10 @@ // tests/state_basic.rs -use containers::{block::{BlockBody, hash_tree_root}, state::State, types::Uint64, ValidatorIndex}; +use containers::{ + block::{hash_tree_root, BlockBody}, + state::State, + types::Uint64, + ValidatorIndex, +}; use pretty_assertions::assert_eq; #[path = "common.rs"] @@ -14,8 +19,13 @@ fn test_generate_genesis() { assert_eq!(state.config, config); assert_eq!(state.slot.0, 0); - let empty_body = BlockBody { attestations: ssz::PersistentList::default() }; - assert_eq!(state.latest_block_header.body_root, hash_tree_root(&empty_body)); + let empty_body = BlockBody { + attestations: ssz::PersistentList::default(), + }; + assert_eq!( + state.latest_block_header.body_root, + hash_tree_root(&empty_body) + ); // Check that collections are empty by trying to get the first element assert!(state.historical_block_hashes.get(0).is_err()); @@ -41,7 +51,9 @@ fn test_slot_justifiability_rules() { #[test] fn test_hash_tree_root() { - let body = BlockBody { attestations: ssz::PersistentList::default() }; + let body = BlockBody { + attestations: ssz::PersistentList::default(), + }; let block = containers::block::Block { slot: containers::slot::Slot(1), proposer_index: ValidatorIndex(0), @@ -52,4 +64,4 @@ fn test_hash_tree_root() { let root = hash_tree_root(&block); assert_ne!(root, containers::types::Bytes32(ssz::H256::zero())); -} \ No newline at end of file +} diff --git a/lean_client/containers/tests/unit_tests/state_justifications.rs b/lean_client/containers/tests/unit_tests/state_justifications.rs index 9a7b0cc..afdd220 100644 --- a/lean_client/containers/tests/unit_tests/state_justifications.rs +++ b/lean_client/containers/tests/unit_tests/state_justifications.rs @@ -1,18 +1,12 @@ // tests/state_justifications.rs -use containers::{ - state::State, - types::Bytes32, - Config -}; +use containers::{state::State, types::Bytes32, Config}; use pretty_assertions::assert_eq; use rstest::{fixture, rstest}; use ssz::PersistentList as List; #[path = "common.rs"] mod common; -use common::{ - base_state, create_attestations, sample_config, TEST_VALIDATOR_COUNT, -}; +use common::{base_state, create_attestations, sample_config, TEST_VALIDATOR_COUNT}; #[fixture] fn config() -> Config { @@ -49,7 +43,7 @@ fn test_get_justifications_single_root() { let mut roots_list = List::default(); roots_list.push(root1).unwrap(); state.justifications_roots = roots_list; - + // Convert Vec to BitList let mut bitlist = ssz::BitList::with_length(TEST_VALIDATOR_COUNT); for (i, &val) in votes1.iter().enumerate() { @@ -88,7 +82,7 @@ fn test_get_justifications_multiple_roots() { roots_list.push(root2).unwrap(); roots_list.push(root3).unwrap(); state.justifications_roots = roots_list; - + // Convert Vec to BitList let mut bitlist = ssz::BitList::with_length(all_votes.len()); for (i, &val) in all_votes.iter().enumerate() { @@ -113,16 +107,20 @@ fn test_with_justifications_empty() { let mut initial_state = base_state(config.clone()); let mut roots_list = List::default(); - roots_list.push(Bytes32(ssz::H256::from_slice(&[1u8;32]))).unwrap(); + roots_list + .push(Bytes32(ssz::H256::from_slice(&[1u8; 32]))) + .unwrap(); initial_state.justifications_roots = roots_list; - + let mut bitlist = ssz::BitList::with_length(TEST_VALIDATOR_COUNT); for i in 0..TEST_VALIDATOR_COUNT { bitlist.set(i, true); } initial_state.justifications_validators = bitlist; - let new_state = initial_state.clone().with_justifications(std::collections::BTreeMap::new()); + let new_state = initial_state + .clone() + .with_justifications(std::collections::BTreeMap::new()); assert!(new_state.justifications_roots.get(0).is_err()); assert!(new_state.justifications_validators.get(0).is_none()); @@ -149,11 +147,15 @@ fn test_with_justifications_deterministic_order() { // Expected roots in sorted order (root1 < root2) assert_eq!(new_state.justifications_roots.get(0).ok(), Some(&root1)); assert_eq!(new_state.justifications_roots.get(1).ok(), Some(&root2)); - + // Verify the bitlist contains the concatenated votes let expected_validators = [votes1, votes2].concat(); for (i, &expected_val) in expected_validators.iter().enumerate() { - let actual_val = new_state.justifications_validators.get(i).map(|b| *b).unwrap_or(false); + let actual_val = new_state + .justifications_validators + .get(i) + .map(|b| *b) + .unwrap_or(false); assert_eq!(actual_val, expected_val); } } diff --git a/lean_client/containers/tests/unit_tests/state_process.rs b/lean_client/containers/tests/unit_tests/state_process.rs index 7db1849..f423818 100644 --- a/lean_client/containers/tests/unit_tests/state_process.rs +++ b/lean_client/containers/tests/unit_tests/state_process.rs @@ -1,6 +1,6 @@ // tests/state_process.rs use containers::{ - block::{Block, BlockBody, hash_tree_root}, + block::{hash_tree_root, Block, BlockBody}, checkpoint::Checkpoint, slot::Slot, state::State, @@ -26,15 +26,24 @@ pub fn genesis_state() -> State { fn test_process_slot() { let genesis_state = genesis_state(); - assert_eq!(genesis_state.latest_block_header.state_root, Bytes32(ssz::H256::zero())); + assert_eq!( + genesis_state.latest_block_header.state_root, + Bytes32(ssz::H256::zero()) + ); let state_after_slot = genesis_state.process_slot(); let expected_root = hash_tree_root(&genesis_state); - assert_eq!(state_after_slot.latest_block_header.state_root, expected_root); + assert_eq!( + state_after_slot.latest_block_header.state_root, + expected_root + ); let state_after_second_slot = state_after_slot.process_slot(); - assert_eq!(state_after_second_slot.latest_block_header.state_root, expected_root); + assert_eq!( + state_after_second_slot.latest_block_header.state_root, + expected_root + ); } #[test] @@ -45,7 +54,10 @@ fn test_process_slots() { let new_state = genesis_state.process_slots(target_slot).unwrap(); assert_eq!(new_state.slot, target_slot); - assert_eq!(new_state.latest_block_header.state_root, hash_tree_root(&genesis_state)); + assert_eq!( + new_state.latest_block_header.state_root, + hash_tree_root(&genesis_state) + ); } #[test] @@ -68,11 +80,21 @@ fn test_process_block_header_valid() { assert_eq!(new_state.latest_finalized.root, genesis_header_root); assert_eq!(new_state.latest_justified.root, genesis_header_root); - assert_eq!(new_state.historical_block_hashes.get(0).ok(), Some(&genesis_header_root)); - let justified_slot_0 = new_state.justified_slots.get(0).map(|b| *b).unwrap_or(false); + assert_eq!( + new_state.historical_block_hashes.get(0).ok(), + Some(&genesis_header_root) + ); + let justified_slot_0 = new_state + .justified_slots + .get(0) + .map(|b| *b) + .unwrap_or(false); assert_eq!(justified_slot_0, true); assert_eq!(new_state.latest_block_header.slot, Slot(1)); - assert_eq!(new_state.latest_block_header.state_root, Bytes32(ssz::H256::zero())); + assert_eq!( + new_state.latest_block_header.state_root, + Bytes32(ssz::H256::zero()) + ); } #[rstest] @@ -95,7 +117,9 @@ fn test_process_block_header_invalid( proposer_index: ValidatorIndex(bad_proposer), parent_root: bad_parent_root.unwrap_or(parent_root), state_root: Bytes32(ssz::H256::zero()), - body: BlockBody { attestations: List::default() }, + body: BlockBody { + attestations: List::default(), + }, }; let result = state_at_slot_1.process_block_header(&block); @@ -114,13 +138,17 @@ fn test_process_attestations_justification_and_finalization() { let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); let block1 = create_block(1, &mut state_at_slot_1.latest_block_header, None); // Use process_block_header and process_operations separately to avoid state root validation - let state_after_header1 = state_at_slot_1.process_block_header(&block1.message.block).unwrap(); + let state_after_header1 = state_at_slot_1 + .process_block_header(&block1.message.block) + .unwrap(); state = state_after_header1.process_attestations(&block1.message.block.body.attestations); // Process slot 4 and block let mut state_at_slot_4 = state.process_slots(Slot(4)).unwrap(); let block4 = create_block(4, &mut state_at_slot_4.latest_block_header, None); - let state_after_header4 = state_at_slot_4.process_block_header(&block4.message.block).unwrap(); + let state_after_header4 = state_at_slot_4 + .process_block_header(&block4.message.block) + .unwrap(); state = state_after_header4.process_attestations(&block4.message.block.body.attestations); // Advance to slot 5 @@ -150,15 +178,21 @@ fn test_process_attestations_justification_and_finalization() { // Convert Vec to PersistentList let mut attestations_list: List<_, U4096> = List::default(); - for a in attestations_for_4 { - attestations_list.push(a).unwrap(); + for a in attestations_for_4 { + attestations_list.push(a).unwrap(); } let new_state = state.process_attestations(&attestations_list); assert_eq!(new_state.latest_justified, checkpoint4); - let justified_slot_4 = new_state.justified_slots.get(4).map(|b| *b).unwrap_or(false); + let justified_slot_4 = new_state + .justified_slots + .get(4) + .map(|b| *b) + .unwrap_or(false); assert_eq!(justified_slot_4, true); assert_eq!(new_state.latest_finalized, genesis_checkpoint); - assert!(!new_state.get_justifications().contains_key(&checkpoint4.root)); -} \ No newline at end of file + assert!(!new_state + .get_justifications() + .contains_key(&checkpoint4.root)); +} diff --git a/lean_client/containers/tests/unit_tests/state_transition.rs b/lean_client/containers/tests/unit_tests/state_transition.rs index 91edfa7..e530dde 100644 --- a/lean_client/containers/tests/unit_tests/state_transition.rs +++ b/lean_client/containers/tests/unit_tests/state_transition.rs @@ -1,9 +1,9 @@ // tests/state_transition.rs use containers::{ - block::{Block, SignedBlockWithAttestation, BlockWithAttestation, hash_tree_root}, + block::{hash_tree_root, Block, BlockWithAttestation, SignedBlockWithAttestation}, state::State, types::{Bytes32, Uint64}, - Slot, Attestation, BlockSignatures + Attestation, BlockSignatures, Slot, }; use pretty_assertions::assert_eq; use rstest::fixture; @@ -23,7 +23,8 @@ 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 @@ -43,7 +44,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,7 +56,8 @@ 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 @@ -83,7 +87,8 @@ fn test_state_transition_bad_state_root() { let state = genesis_state(); let mut state_at_slot_1 = state.process_slots(Slot(1)).unwrap(); - let signed_block_with_attestation = create_block(1, &mut state_at_slot_1.latest_block_header, None); + let signed_block_with_attestation = + create_block(1, &mut state_at_slot_1.latest_block_header, None); let mut block = signed_block_with_attestation.message.block.clone(); block.state_root = Bytes32(ssz::H256::zero()); @@ -99,4 +104,4 @@ fn test_state_transition_bad_state_root() { let result = state.state_transition(final_signed_block_with_attestation, true); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Invalid block state root"); -} \ No newline at end of file +} diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 618c8c9..3b1d1a6 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 { @@ -55,9 +52,13 @@ pub fn on_attestation( if store .latest_known_attestations .get(&validator_id) - .map_or(true, |existing| existing.message.data.slot < attestation_slot) + .map_or(true, |existing| { + existing.message.data.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 @@ -71,9 +72,13 @@ pub fn on_attestation( if store .latest_new_attestations .get(&validator_id) - .map_or(true, |existing| existing.message.data.slot < attestation_slot) + .map_or(true, |existing| { + existing.message.data.slot < attestation_slot + }) { - store.latest_new_attestations.insert(validator_id, signed_attestation); + store + .latest_new_attestations + .insert(validator_id, signed_attestation); } } Ok(()) @@ -125,8 +130,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()); diff --git a/lean_client/fork_choice/src/store.rs b/lean_client/fork_choice/src/store.rs index 4c746d4..715c90d 100644 --- a/lean_client/fork_choice/src/store.rs +++ b/lean_client/fork_choice/src/store.rs @@ -1,7 +1,6 @@ use containers::{ - attestation::SignedAttestation, - block::SignedBlockWithAttestation, checkpoint::Checkpoint, config::Config, state::State, - Bytes32, Root, Slot, ValidatorIndex, + attestation::SignedAttestation, block::SignedBlockWithAttestation, checkpoint::Checkpoint, + config::Config, state::State, Bytes32, Root, Slot, ValidatorIndex, }; use ssz::SszHash; use std::collections::HashMap; @@ -182,7 +181,8 @@ pub fn update_safe_target(store: &mut Store) { let min_score = (n_validators * 2 + 2) / 3; let root = store.latest_justified.root; - store.safe_target = get_fork_choice_head(store, root, &store.latest_new_attestations, min_score); + store.safe_target = + get_fork_choice_head(store, root, &store.latest_new_attestations, min_score); } pub fn accept_new_attestations(store: &mut Store) { diff --git a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs index e2d230a..d35c0bf 100644 --- a/lean_client/fork_choice/tests/fork_choice_test_vectors.rs +++ b/lean_client/fork_choice/tests/fork_choice_test_vectors.rs @@ -4,8 +4,11 @@ use fork_choice::{ }; use containers::{ - attestation::{Attestation, AttestationData, BlockSignatures, SignedAttestation, Signature}, - block::{hash_tree_root, Block, BlockBody, BlockHeader, BlockWithAttestation, SignedBlockWithAttestation}, + attestation::{Attestation, AttestationData, BlockSignatures, Signature, SignedAttestation}, + block::{ + hash_tree_root, Block, BlockBody, BlockHeader, BlockWithAttestation, + SignedBlockWithAttestation, + }, checkpoint::Checkpoint, config::Config, state::State, @@ -303,7 +306,9 @@ fn convert_test_anchor_block(test_block: &TestAnchorBlock) -> SignedBlockWithAtt } } -fn convert_test_block(test_block_with_att: &TestBlockWithAttestation) -> SignedBlockWithAttestation { +fn convert_test_block( + test_block_with_att: &TestBlockWithAttestation, +) -> SignedBlockWithAttestation { let test_block = &test_block_with_att.block; let mut attestations = ssz::PersistentList::default(); @@ -416,7 +421,7 @@ fn verify_checks( Some(c) => c, None => return Ok(()), }; - + if let Some(expected_slot) = checks.head_slot { let actual_slot = store.blocks[&store.head].message.block.slot.0; if actual_slot != expected_slot { @@ -529,7 +534,8 @@ fn run_single_test(_test_name: &str, test: TestVector) -> Result<(), String> { // Advance time to the block's slot to ensure attestations are processable // SECONDS_PER_SLOT is 4 (not 12) - let block_time = store.config.genesis_time + (signed_block.message.block.slot.0 * 4); + let block_time = + store.config.genesis_time + (signed_block.message.block.slot.0 * 4); on_tick(&mut store, block_time, false); on_block(&mut store, signed_block)?; diff --git a/lean_client/fork_choice/tests/unit_tests.rs b/lean_client/fork_choice/tests/unit_tests.rs index b4490da..480ba3f 100644 --- a/lean_client/fork_choice/tests/unit_tests.rs +++ b/lean_client/fork_choice/tests/unit_tests.rs @@ -1,6 +1,6 @@ mod unit_tests { pub mod common; + pub mod fork_choice; pub mod time; pub mod votes; - pub mod fork_choice; } diff --git a/lean_client/fork_choice/tests/unit_tests/common.rs b/lean_client/fork_choice/tests/unit_tests/common.rs index 9539c34..dc1a762 100644 --- a/lean_client/fork_choice/tests/unit_tests/common.rs +++ b/lean_client/fork_choice/tests/unit_tests/common.rs @@ -1,4 +1,3 @@ -use fork_choice::store::{get_forkchoice_store, Store}; use containers::{ attestation::Attestation, block::{Block, BlockBody, BlockWithAttestation, SignedBlockWithAttestation}, @@ -7,19 +6,16 @@ use containers::{ validator::Validator, Bytes32, Slot, Uint64, ValidatorIndex, }; +use fork_choice::store::{get_forkchoice_store, Store}; use ssz::SszHash; pub fn create_test_store() -> Store { - let config = Config { - genesis_time: 1000, - }; - - let validators = vec![ - Validator::default(); 10 - ]; - + let config = Config { genesis_time: 1000 }; + + let validators = vec![Validator::default(); 10]; + let state = State::generate_genesis_with_validators(Uint64(1000), validators); - + let block = Block { slot: Slot(0), proposer_index: ValidatorIndex(0), @@ -27,7 +23,7 @@ pub fn create_test_store() -> Store { state_root: Bytes32(state.hash_tree_root()), body: BlockBody::default(), }; - + let block_with_attestation = BlockWithAttestation { block: block.clone(), proposer_attestation: Attestation::default(), diff --git a/lean_client/fork_choice/tests/unit_tests/fork_choice.rs b/lean_client/fork_choice/tests/unit_tests/fork_choice.rs index fc1e7e6..d2b3833 100644 --- a/lean_client/fork_choice/tests/unit_tests/fork_choice.rs +++ b/lean_client/fork_choice/tests/unit_tests/fork_choice.rs @@ -1,12 +1,12 @@ use super::common::create_test_store; -use fork_choice::store::{get_proposal_head, get_vote_target}; use containers::Slot; +use fork_choice::store::{get_proposal_head, get_vote_target}; #[test] fn test_get_proposal_head_basic() { let mut store = create_test_store(); let head = get_proposal_head(&mut store, Slot(0)); - + assert_eq!(head, store.head); } @@ -14,9 +14,9 @@ fn test_get_proposal_head_basic() { fn test_get_proposal_head_advances_time() { let mut store = create_test_store(); let initial_time = store.time; - + get_proposal_head(&mut store, Slot(5)); - + assert!(store.time >= initial_time); } diff --git a/lean_client/fork_choice/tests/unit_tests/time.rs b/lean_client/fork_choice/tests/unit_tests/time.rs index 03260c7..ff99491 100644 --- a/lean_client/fork_choice/tests/unit_tests/time.rs +++ b/lean_client/fork_choice/tests/unit_tests/time.rs @@ -1,7 +1,7 @@ use super::common::create_test_store; +use containers::{Slot, Uint64}; use fork_choice::handlers::on_tick; use fork_choice::store::{tick_interval, INTERVALS_PER_SLOT, SECONDS_PER_SLOT}; -use containers::{Slot, Uint64}; #[test] fn test_on_tick_basic() { @@ -31,7 +31,7 @@ fn test_on_tick_already_current() { let initial_time = store.time; let current_target = store.config.genesis_time + initial_time; - // Try to advance to current time + // Try to advance to current time on_tick(&mut store, current_target, true); // Should not change significantly @@ -86,7 +86,7 @@ fn test_tick_interval_sequence() { #[test] fn test_tick_interval_actions_by_phase() { let mut store = create_test_store(); - + // Reset store time to 0 relative to genesis for clean testing store.time = 0; @@ -101,11 +101,10 @@ fn test_tick_interval_actions_by_phase() { } } - #[test] fn test_slot_time_calculations() { let genesis_time = 1000; - + // Slot 0 let slot_0_time = genesis_time + (0 * SECONDS_PER_SLOT); assert_eq!(slot_0_time, genesis_time); diff --git a/lean_client/fork_choice/tests/unit_tests/votes.rs b/lean_client/fork_choice/tests/unit_tests/votes.rs index 805e785..3cdaabb 100644 --- a/lean_client/fork_choice/tests/unit_tests/votes.rs +++ b/lean_client/fork_choice/tests/unit_tests/votes.rs @@ -1,21 +1,34 @@ use super::common::create_test_store; -use fork_choice::handlers::on_attestation; -use fork_choice::store::{accept_new_attestations, INTERVALS_PER_SLOT}; use containers::{ - attestation::{Attestation, AttestationData, SignedAttestation, Signature}, + attestation::{Attestation, AttestationData, Signature, SignedAttestation}, checkpoint::Checkpoint, Bytes32, Slot, Uint64, ValidatorIndex, }; +use fork_choice::handlers::on_attestation; +use fork_choice::store::{accept_new_attestations, INTERVALS_PER_SLOT}; -fn create_signed_attestation(validator_id: u64, slot: Slot, head_root: Bytes32) -> SignedAttestation { +fn create_signed_attestation( + validator_id: u64, + slot: Slot, + head_root: Bytes32, +) -> SignedAttestation { SignedAttestation { message: Attestation { validator_id: Uint64(validator_id), data: AttestationData { slot, - head: Checkpoint { root: head_root, slot }, - target: Checkpoint { root: head_root, slot }, - source: Checkpoint { root: Bytes32::default(), slot: Slot(0) }, + head: Checkpoint { + root: head_root, + slot, + }, + target: Checkpoint { + root: head_root, + slot, + }, + source: Checkpoint { + root: Bytes32::default(), + slot: Slot(0), + }, }, }, signature: Signature::default(), @@ -31,53 +44,58 @@ fn test_accept_new_attestations() { let val2 = ValidatorIndex(2); let val3 = ValidatorIndex(3); - store.latest_known_attestations.insert( - val1, - create_signed_attestation(1, Slot(0), store.head), - ); + store + .latest_known_attestations + .insert(val1, create_signed_attestation(1, Slot(0), store.head)); // Val1 updates their attestation to Slot 1 - store.latest_new_attestations.insert( - val1, - create_signed_attestation(1, Slot(1), store.head), - ); + store + .latest_new_attestations + .insert(val1, create_signed_attestation(1, Slot(1), store.head)); // Val2 casts a new attestation for Slot 1 - store.latest_new_attestations.insert( - val2, - create_signed_attestation(2, Slot(1), store.head), - ); + store + .latest_new_attestations + .insert(val2, create_signed_attestation(2, Slot(1), store.head)); // Val3 casts a new attestation for Slot 2 - store.latest_new_attestations.insert( - val3, - create_signed_attestation(3, Slot(2), store.head), - ); + store + .latest_new_attestations + .insert(val3, create_signed_attestation(3, Slot(2), store.head)); accept_new_attestations(&mut store); assert_eq!(store.latest_new_attestations.len(), 0); assert_eq!(store.latest_known_attestations.len(), 3); - assert_eq!(store.latest_known_attestations[&val1].message.data.slot, Slot(1)); - assert_eq!(store.latest_known_attestations[&val2].message.data.slot, Slot(1)); - assert_eq!(store.latest_known_attestations[&val3].message.data.slot, Slot(2)); + assert_eq!( + store.latest_known_attestations[&val1].message.data.slot, + Slot(1) + ); + assert_eq!( + store.latest_known_attestations[&val2].message.data.slot, + Slot(1) + ); + assert_eq!( + store.latest_known_attestations[&val3].message.data.slot, + Slot(2) + ); } #[test] fn test_accept_new_attestations_multiple() { let mut store = create_test_store(); - + for i in 0..5 { store.latest_new_attestations.insert( ValidatorIndex(i), create_signed_attestation(i, Slot(i), store.head), ); } - + assert_eq!(store.latest_new_attestations.len(), 5); assert_eq!(store.latest_known_attestations.len(), 0); - + accept_new_attestations(&mut store); - + assert_eq!(store.latest_new_attestations.len(), 0); assert_eq!(store.latest_known_attestations.len(), 5); } @@ -86,9 +104,9 @@ fn test_accept_new_attestations_multiple() { fn test_accept_new_attestations_empty() { let mut store = create_test_store(); let initial_known = store.latest_known_attestations.len(); - + accept_new_attestations(&mut store); - + assert_eq!(store.latest_new_attestations.len(), 0); assert_eq!(store.latest_known_attestations.len(), initial_known); } @@ -103,36 +121,62 @@ fn test_on_attestation_lifecycle() { // 1. Attestation from network (gossip) let signed_attestation_gossip = create_signed_attestation(1, slot_0, store.head); - on_attestation(&mut store, signed_attestation_gossip.clone(), false).expect("Gossip attestation valid"); - + on_attestation(&mut store, signed_attestation_gossip.clone(), false) + .expect("Gossip attestation valid"); + // Should be in new_attestations, not known_attestations assert!(store.latest_new_attestations.contains_key(&validator_idx)); assert!(!store.latest_known_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, slot_0); + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + slot_0 + ); // 2. Same attestation included in a block on_attestation(&mut store, signed_attestation_gossip, true).expect("Block attestation valid"); - + assert!(store.latest_known_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, slot_0); + assert_eq!( + store.latest_known_attestations[&validator_idx] + .message + .data + .slot, + slot_0 + ); // 3. Newer attestation from network store.time = 1 * INTERVALS_PER_SLOT; // Advance time let signed_attestation_next = create_signed_attestation(1, slot_1, store.head); - on_attestation(&mut store, signed_attestation_next, false).expect("Next gossip attestation valid"); + on_attestation(&mut store, signed_attestation_next, false) + .expect("Next gossip attestation valid"); // Should update new_attestations - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, slot_1); + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + slot_1 + ); // Known attestations should still be at slot 0 until accepted - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, slot_0); + assert_eq!( + store.latest_known_attestations[&validator_idx] + .message + .data + .slot, + slot_0 + ); } #[test] fn test_on_attestation_future_slot() { let mut store = create_test_store(); let future_slot = Slot(100); // Far in the future - + let signed_attestation = create_signed_attestation(1, future_slot, store.head); let result = on_attestation(&mut store, signed_attestation, false); @@ -143,61 +187,86 @@ fn test_on_attestation_future_slot() { fn test_on_attestation_update_vote() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); - + // First attestation at slot 0 let signed_attestation1 = create_signed_attestation(1, Slot(0), store.head); - + on_attestation(&mut store, signed_attestation1, false).expect("First attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(0)); - + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(0) + ); + // Advance time to allow slot 1 store.time = 1 * INTERVALS_PER_SLOT; - + // Second attestation at slot 1 let signed_attestation2 = create_signed_attestation(1, Slot(1), store.head); - + on_attestation(&mut store, signed_attestation2, false).expect("Second attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(1)); + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(1) + ); } #[test] fn test_on_attestation_ignore_old_vote() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); - + // Advance time store.time = 2 * INTERVALS_PER_SLOT; - + // Newer attestation first let signed_attestation_new = create_signed_attestation(1, Slot(2), store.head); - + on_attestation(&mut store, signed_attestation_new, false).expect("New attestation valid"); - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(2)); - + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(2) + ); + // Older attestation second let signed_attestation_old = create_signed_attestation(1, Slot(1), store.head); - - on_attestation(&mut store, signed_attestation_old, false).expect("Old attestation processed but ignored"); + + on_attestation(&mut store, signed_attestation_old, false) + .expect("Old attestation processed but ignored"); // Should still be slot 2 - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(2)); + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(2) + ); } #[test] fn test_on_attestation_from_block_supersedes_new() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); - + // First, add attestation via gossip let signed_attestation1 = create_signed_attestation(1, Slot(0), store.head); on_attestation(&mut store, signed_attestation1, false).expect("Gossip attestation valid"); - + assert!(store.latest_new_attestations.contains_key(&validator_idx)); assert!(!store.latest_known_attestations.contains_key(&validator_idx)); - + // Then, add same attestation via block (on-chain) let signed_attestation2 = create_signed_attestation(1, Slot(0), store.head); on_attestation(&mut store, signed_attestation2, true).expect("Block attestation valid"); - + // Should move from new to known assert!(!store.latest_new_attestations.contains_key(&validator_idx)); assert!(store.latest_known_attestations.contains_key(&validator_idx)); @@ -207,19 +276,31 @@ fn test_on_attestation_from_block_supersedes_new() { fn test_on_attestation_newer_from_block_removes_older_new() { let mut store = create_test_store(); let validator_idx = ValidatorIndex(1); - + // Add older attestation via gossip let signed_attestation_gossip = create_signed_attestation(1, Slot(0), store.head); on_attestation(&mut store, signed_attestation_gossip, false).expect("Gossip attestation valid"); - - assert_eq!(store.latest_new_attestations[&validator_idx].message.data.slot, Slot(0)); - + + assert_eq!( + store.latest_new_attestations[&validator_idx] + .message + .data + .slot, + Slot(0) + ); + // Add newer attestation via block (on-chain) store.time = 1 * INTERVALS_PER_SLOT; let signed_attestation_block = create_signed_attestation(1, Slot(1), store.head); on_attestation(&mut store, signed_attestation_block, true).expect("Block attestation valid"); - + // New attestation should be removed (superseded by newer on-chain one) assert!(!store.latest_new_attestations.contains_key(&validator_idx)); - assert_eq!(store.latest_known_attestations[&validator_idx].message.data.slot, Slot(1)); + assert_eq!( + store.latest_known_attestations[&validator_idx] + .message + .data + .slot, + Slot(1) + ); } diff --git a/lean_client/networking/src/gossipsub/config.rs b/lean_client/networking/src/gossipsub/config.rs index 05488c2..67061bc 100644 --- a/lean_client/networking/src/gossipsub/config.rs +++ b/lean_client/networking/src/gossipsub/config.rs @@ -15,9 +15,9 @@ impl GossipsubConfig { pub fn new() -> Self { let justification_lookback_slots: u64 = 3; let seconds_per_slot: u64 = 12; - + let seen_ttl_secs = seconds_per_slot * justification_lookback_slots * 2; - + let config = ConfigBuilder::default() // leanSpec: heartbeat_interval_secs = 0.7 .heartbeat_interval(Duration::from_millis(700)) diff --git a/lean_client/networking/src/gossipsub/message.rs b/lean_client/networking/src/gossipsub/message.rs index b578e75..4ac1ae1 100644 --- a/lean_client/networking/src/gossipsub/message.rs +++ b/lean_client/networking/src/gossipsub/message.rs @@ -1,8 +1,8 @@ use crate::gossipsub::topic::GossipsubKind; use crate::gossipsub::topic::GossipsubTopic; +use containers::SignedAttestation; use containers::SignedBlockWithAttestation; use containers::ssz::SszReadDefault; -use containers::{SignedAttestation}; use libp2p::gossipsub::TopicHash; pub enum GossipsubMessage { @@ -14,7 +14,8 @@ impl GossipsubMessage { pub fn decode(topic: &TopicHash, data: &[u8]) -> Result { match GossipsubTopic::decode(topic)?.kind { GossipsubKind::Block => Ok(Self::Block( - SignedBlockWithAttestation::from_ssz_default(data).map_err(|e| format!("{:?}", e))?, + SignedBlockWithAttestation::from_ssz_default(data) + .map_err(|e| format!("{:?}", e))?, )), GossipsubKind::Attestation => Ok(Self::Attestation( SignedAttestation::from_ssz_default(data).map_err(|e| format!("{:?}", e))?, diff --git a/lean_client/networking/src/gossipsub/tests/config.rs b/lean_client/networking/src/gossipsub/tests/config.rs index e788d81..4fa245d 100644 --- a/lean_client/networking/src/gossipsub/tests/config.rs +++ b/lean_client/networking/src/gossipsub/tests/config.rs @@ -1,5 +1,5 @@ use crate::gossipsub::config::GossipsubConfig; -use crate::gossipsub::topic::{get_topics, GossipsubKind}; +use crate::gossipsub::topic::{GossipsubKind, get_topics}; #[test] fn test_default_parameters() { @@ -24,8 +24,14 @@ fn test_default_parameters() { assert_eq!(config.config.gossip_lazy(), 6); // d_lazy = 6 assert_eq!(config.config.history_length(), 6); // mcache_len = 6 assert_eq!(config.config.history_gossip(), 3); // mcache_gossip = 3 - assert_eq!(config.config.fanout_ttl(), std::time::Duration::from_secs(60)); // fanout_ttl_secs = 60 - assert_eq!(config.config.heartbeat_interval(), std::time::Duration::from_millis(700)); // heartbeat_interval_secs = 0.7 + assert_eq!( + config.config.fanout_ttl(), + std::time::Duration::from_secs(60) + ); // fanout_ttl_secs = 60 + assert_eq!( + config.config.heartbeat_interval(), + std::time::Duration::from_millis(700) + ); // heartbeat_interval_secs = 0.7 assert!(config.topics.is_empty()); } diff --git a/lean_client/networking/src/gossipsub/tests/message.rs b/lean_client/networking/src/gossipsub/tests/message.rs index ce062be..9fd25dd 100644 --- a/lean_client/networking/src/gossipsub/tests/message.rs +++ b/lean_client/networking/src/gossipsub/tests/message.rs @@ -1,5 +1,7 @@ use crate::gossipsub::message::GossipsubMessage; -use crate::gossipsub::topic::{ATTESTATION_TOPIC, BLOCK_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX}; +use crate::gossipsub::topic::{ + ATTESTATION_TOPIC, BLOCK_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX, +}; use libp2p::gossipsub::TopicHash; #[test] diff --git a/lean_client/networking/src/gossipsub/tests/message_id.rs b/lean_client/networking/src/gossipsub/tests/message_id.rs index 4eb3302..17a0b51 100644 --- a/lean_client/networking/src/gossipsub/tests/message_id.rs +++ b/lean_client/networking/src/gossipsub/tests/message_id.rs @@ -1,5 +1,7 @@ use crate::gossipsub::config::compute_message_id; -use crate::gossipsub::topic::{ATTESTATION_TOPIC, BLOCK_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX}; +use crate::gossipsub::topic::{ + ATTESTATION_TOPIC, BLOCK_TOPIC, SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX, +}; use crate::types::MESSAGE_DOMAIN_VALID_SNAPPY; use libp2p::gossipsub::{Message, TopicHash}; use sha2::{Digest, Sha256}; @@ -134,7 +136,7 @@ fn test_message_id_uses_valid_snappy_domain() { let topic_bytes = topic.as_bytes(); let topic_len = topic_bytes.len() as u64; - + let mut digest_input = Vec::new(); digest_input.extend_from_slice(MESSAGE_DOMAIN_VALID_SNAPPY); diff --git a/lean_client/networking/src/gossipsub/tests/mod.rs b/lean_client/networking/src/gossipsub/tests/mod.rs index 351a897..15f330a 100644 --- a/lean_client/networking/src/gossipsub/tests/mod.rs +++ b/lean_client/networking/src/gossipsub/tests/mod.rs @@ -1,4 +1,4 @@ mod config; -mod message_id; mod message; -mod topic; \ No newline at end of file +mod message_id; +mod topic; diff --git a/lean_client/networking/src/gossipsub/tests/topic.rs b/lean_client/networking/src/gossipsub/tests/topic.rs index cdd09df..7e3d70b 100644 --- a/lean_client/networking/src/gossipsub/tests/topic.rs +++ b/lean_client/networking/src/gossipsub/tests/topic.rs @@ -1,6 +1,6 @@ use crate::gossipsub::topic::{ - get_topics, GossipsubKind, GossipsubTopic, ATTESTATION_TOPIC, BLOCK_TOPIC, - SSZ_SNAPPY_ENCODING_POSTFIX, TOPIC_PREFIX, + ATTESTATION_TOPIC, BLOCK_TOPIC, GossipsubKind, GossipsubTopic, SSZ_SNAPPY_ENCODING_POSTFIX, + TOPIC_PREFIX, get_topics, }; use libp2p::gossipsub::TopicHash; diff --git a/lean_client/networking/src/gossipsub/topic.rs b/lean_client/networking/src/gossipsub/topic.rs index 9a4e7b5..09fcd33 100644 --- a/lean_client/networking/src/gossipsub/topic.rs +++ b/lean_client/networking/src/gossipsub/topic.rs @@ -45,9 +45,7 @@ impl GossipsubTopic { let parts: Vec<&str> = topic.as_str().trim_start_matches('/').split('/').collect(); if parts.len() != 4 { - return Err(format!( - "Invalid topic part count: {topic:?}" - )); + return Err(format!("Invalid topic part count: {topic:?}")); } Ok(parts) @@ -78,10 +76,7 @@ impl std::fmt::Display for GossipsubTopic { write!( f, "/{}/{}/{}/{}", - TOPIC_PREFIX, - self.fork, - self.kind, - SSZ_SNAPPY_ENCODING_POSTFIX + TOPIC_PREFIX, self.fork, self.kind, SSZ_SNAPPY_ENCODING_POSTFIX ) } } @@ -106,10 +101,7 @@ impl From for TopicHash { }; TopicHash::from_raw(format!( "/{}/{}/{}/{}", - TOPIC_PREFIX, - val.fork, - kind_str, - SSZ_SNAPPY_ENCODING_POSTFIX + TOPIC_PREFIX, val.fork, kind_str, SSZ_SNAPPY_ENCODING_POSTFIX )) } } diff --git a/lean_client/networking/src/network/mod.rs b/lean_client/networking/src/network/mod.rs index 7609d9a..1900ed8 100644 --- a/lean_client/networking/src/network/mod.rs +++ b/lean_client/networking/src/network/mod.rs @@ -2,4 +2,4 @@ mod behaviour; mod service; pub use behaviour::{LeanNetworkBehaviour, LeanNetworkBehaviourEvent}; -pub use service::{NetworkServiceConfig, NetworkEvent, NetworkService}; +pub use service::{NetworkEvent, NetworkService, NetworkServiceConfig}; diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index 9c0993f..63b6f9c 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -338,8 +338,8 @@ where } fn handle_request_response_event(&mut self, event: ReqRespMessage) -> Option { - use libp2p::request_response::{Event, Message}; use crate::req_resp::LeanResponse; + use libp2p::request_response::{Event, Message}; match event { Event::Message { peer, message, .. } => match message { @@ -357,16 +357,24 @@ where tokio::spawn(async move { for block in blocks { let slot = block.message.block.slot.0; - if let Err(e) = chain_sink.send( - ChainMessage::ProcessBlock { + if let Err(e) = chain_sink + .send(ChainMessage::ProcessBlock { signed_block_with_attestation: block, is_trusted: false, should_gossip: false, // Don't re-gossip requested blocks - } - ).await { - warn!(slot = slot, ?e, "Failed to send requested block to chain"); + }) + .await + { + warn!( + slot = slot, + ?e, + "Failed to send requested block to chain" + ); } else { - debug!(slot = slot, "Queued requested block for processing"); + debug!( + slot = slot, + "Queued requested block for processing" + ); } } }); @@ -379,7 +387,9 @@ where } } } - Message::Request { request, channel, .. } => { + Message::Request { + request, channel, .. + } => { use crate::req_resp::{LeanRequest, LeanResponse}; let response = match request { @@ -395,10 +405,12 @@ where } }; - if let Err(e) = self.swarm + if let Err(e) = self + .swarm .behaviour_mut() .req_resp - .send_response(channel, response) { + .send_response(channel, response) + { warn!(peer = %peer, ?e, "Failed to send response"); } } diff --git a/lean_client/networking/src/req_resp.rs b/lean_client/networking/src/req_resp.rs index 51d705e..bd6c414 100644 --- a/lean_client/networking/src/req_resp.rs +++ b/lean_client/networking/src/req_resp.rs @@ -2,8 +2,8 @@ use std::io; use std::io::{Read, Write}; use async_trait::async_trait; -use containers::{Bytes32, SignedBlockWithAttestation, Status}; use containers::ssz::{SszReadDefault, SszWrite}; +use containers::{Bytes32, SignedBlockWithAttestation, Status}; use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use libp2p::request_response::{ Behaviour as RequestResponse, Codec, Config, Event, ProtocolSupport, @@ -46,8 +46,9 @@ impl LeanCodec { fn compress(data: &[u8]) -> io::Result> { let mut encoder = FrameEncoder::new(Vec::new()); encoder.write_all(data)?; - encoder.into_inner() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Snappy framing failed: {e}"))) + encoder.into_inner().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("Snappy framing failed: {e}")) + }) } /// Decompress data using Snappy framing format (required for req/resp protocol) @@ -60,8 +61,9 @@ impl LeanCodec { fn encode_request(request: &LeanRequest) -> io::Result> { let ssz_bytes = match request { - LeanRequest::Status(status) => status.to_ssz() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")))?, + LeanRequest::Status(status) => status.to_ssz().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) + })?, LeanRequest::BlocksByRoot(roots) => { let mut bytes = Vec::new(); for root in roots { @@ -77,12 +79,16 @@ impl LeanCodec { if data.is_empty() { return Ok(LeanRequest::Status(Status::default())); } - + let ssz_bytes = Self::decompress(data)?; - + if protocol.contains("status") { - let status = Status::from_ssz_default(&ssz_bytes) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ decode Status failed: {e:?}")))?; + let status = Status::from_ssz_default(&ssz_bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("SSZ decode Status failed: {e:?}"), + ) + })?; Ok(LeanRequest::Status(status)) } else if protocol.contains("blocks_by_root") { let mut roots = Vec::new(); @@ -96,35 +102,44 @@ impl LeanCodec { if roots.len() > MAX_REQUEST_BLOCKS { return Err(io::Error::new( io::ErrorKind::InvalidData, - format!("Too many block roots requested: {} > {}", roots.len(), MAX_REQUEST_BLOCKS), + format!( + "Too many block roots requested: {} > {}", + roots.len(), + MAX_REQUEST_BLOCKS + ), )); } Ok(LeanRequest::BlocksByRoot(roots)) } else { - Err(io::Error::new(io::ErrorKind::Other, format!("Unknown protocol: {protocol}"))) + Err(io::Error::new( + io::ErrorKind::Other, + format!("Unknown protocol: {protocol}"), + )) } } fn encode_response(response: &LeanResponse) -> io::Result> { let ssz_bytes = match response { - LeanResponse::Status(status) => status.to_ssz() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")))?, + LeanResponse::Status(status) => status.to_ssz().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) + })?, LeanResponse::BlocksByRoot(blocks) => { let mut bytes = Vec::new(); for block in blocks { - let block_bytes = block.to_ssz() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")))?; + let block_bytes = block.to_ssz().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("SSZ encode failed: {e}")) + })?; bytes.extend_from_slice(&block_bytes); } bytes } LeanResponse::Empty => Vec::new(), }; - + if ssz_bytes.is_empty() { return Ok(Vec::new()); } - + Self::compress(&ssz_bytes) } @@ -132,22 +147,33 @@ impl LeanCodec { if data.is_empty() { return Ok(LeanResponse::Empty); } - + let ssz_bytes = Self::decompress(data)?; - + if protocol.contains("status") { - let status = Status::from_ssz_default(&ssz_bytes) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ decode Status failed: {e:?}")))?; + let status = Status::from_ssz_default(&ssz_bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("SSZ decode Status failed: {e:?}"), + ) + })?; Ok(LeanResponse::Status(status)) } else if protocol.contains("blocks_by_root") { if ssz_bytes.is_empty() { return Ok(LeanResponse::BlocksByRoot(Vec::new())); } - let block = SignedBlockWithAttestation::from_ssz_default(&ssz_bytes) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("SSZ decode Block failed: {e:?}")))?; + let block = SignedBlockWithAttestation::from_ssz_default(&ssz_bytes).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("SSZ decode Block failed: {e:?}"), + ) + })?; Ok(LeanResponse::BlocksByRoot(vec![block])) } else { - Err(io::Error::new(io::ErrorKind::Other, format!("Unknown protocol: {protocol}"))) + Err(io::Error::new( + io::ErrorKind::Other, + format!("Unknown protocol: {protocol}"), + )) } } } diff --git a/lean_client/networking/src/types.rs b/lean_client/networking/src/types.rs index 37644c2..b15c737 100644 --- a/lean_client/networking/src/types.rs +++ b/lean_client/networking/src/types.rs @@ -70,7 +70,9 @@ pub enum ChainMessage { } impl ChainMessage { - pub fn block_with_attestation(signed_block_with_attestation: SignedBlockWithAttestation) -> Self { + pub fn block_with_attestation( + signed_block_with_attestation: SignedBlockWithAttestation, + ) -> Self { ChainMessage::ProcessBlock { signed_block_with_attestation, is_trusted: false, @@ -90,11 +92,24 @@ impl ChainMessage { impl Display for ChainMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ChainMessage::ProcessBlock { signed_block_with_attestation, .. } => { - write!(f, "ProcessBlockWithAttestation(slot={})", signed_block_with_attestation.message.block.slot.0) + ChainMessage::ProcessBlock { + signed_block_with_attestation, + .. + } => { + write!( + f, + "ProcessBlockWithAttestation(slot={})", + signed_block_with_attestation.message.block.slot.0 + ) } - ChainMessage::ProcessAttestation { signed_attestation, .. } => { - write!(f, "ProcessAttestation(slot={})", signed_attestation.message.data.slot.0) + ChainMessage::ProcessAttestation { + signed_attestation, .. + } => { + write!( + f, + "ProcessAttestation(slot={})", + signed_attestation.message.data.slot.0 + ) } } } diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index 396c8f7..92fb59a 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -95,7 +95,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}", @@ -234,7 +237,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 +252,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)) } } diff --git a/lean_client/validator/src/keys.rs b/lean_client/validator/src/keys.rs index 13c0deb..392fd95 100644 --- a/lean_client/validator/src/keys.rs +++ b/lean_client/validator/src/keys.rs @@ -1,8 +1,8 @@ +use containers::attestation::U3112; +use containers::ssz::ByteVector; +use containers::Signature; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use containers::Signature; -use containers::ssz::ByteVector; -use containers::attestation::U3112; use tracing::info; #[cfg(feature = "xmss-signing")] @@ -42,7 +42,9 @@ impl KeyManager { /// Load a secret key for a specific validator index pub fn load_key(&mut self, validator_index: u64) -> Result<(), Box> { - let sk_path = self.keys_dir.join(format!("validator_{}_sk.ssz", validator_index)); + let sk_path = self + .keys_dir + .join(format!("validator_{}_sk.ssz", validator_index)); if !sk_path.exists() { return Err(format!("Secret key file not found: {:?}", sk_path).into()); @@ -69,20 +71,20 @@ impl KeyManager { ) -> Result> { #[cfg(feature = "xmss-signing")] { - let key_bytes = self.keys + let key_bytes = self + .keys .get(&validator_index) .ok_or_else(|| format!("No key loaded for validator {}", validator_index))?; - type SecretKey = ::SecretKey; + type SecretKey = + ::SecretKey; let secret_key = SecretKey::from_bytes(key_bytes) .map_err(|e| format!("Failed to deserialize secret key: {:?}", e))?; - let leansig_signature = SIGTopLevelTargetSumLifetime32Dim64Base8::sign( - &secret_key, - epoch, - message, - ).map_err(|e| format!("Failed to sign message: {:?}", e))?; + let leansig_signature = + SIGTopLevelTargetSumLifetime32Dim64Base8::sign(&secret_key, epoch, message) + .map_err(|e| format!("Failed to sign message: {:?}", e))?; let sig_bytes = leansig_signature.to_bytes(); @@ -90,7 +92,8 @@ impl KeyManager { return Err(format!( "Invalid signature size: expected 3112, got {}", sig_bytes.len() - ).into()); + ) + .into()); } // Convert to ByteVector using unsafe pointer copy (same pattern as BlsPublicKey) @@ -105,7 +108,7 @@ impl KeyManager { #[cfg(not(feature = "xmss-signing"))] { - let _ = (epoch, message); // Suppress unused warnings + let _ = (epoch, message); // Suppress unused warnings warn!( validator = validator_index, "XMSS signing disabled - using zero signature" diff --git a/lean_client/validator/src/lib.rs b/lean_client/validator/src/lib.rs index e26bcf7..6c6a4a4 100644 --- a/lean_client/validator/src/lib.rs +++ b/lean_client/validator/src/lib.rs @@ -4,7 +4,7 @@ use std::path::Path; use containers::{ attestation::{Attestation, AttestationData, Signature, SignedAttestation}, - block::{BlockWithAttestation, SignedBlockWithAttestation, hash_tree_root}, + block::{hash_tree_root, BlockWithAttestation, SignedBlockWithAttestation}, checkpoint::Checkpoint, types::{Uint64, ValidatorIndex}, Slot, @@ -179,7 +179,7 @@ impl ValidatorService { 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(); @@ -197,13 +197,20 @@ 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)?; + let (block, _post_state, _collected_atts, sigs) = parent_state.build_block( + slot, + proposer_index, + parent_root, + Some(valid_attestations), + None, + None, + )?; // Collect signatures from the attestations we included let mut signatures = sigs; for signed_att in &valid_signed_attestations { - signatures.push(signed_att.signature.clone()) + signatures + .push(signed_att.signature.clone()) .map_err(|e| format!("Failed to add attestation signature: {:?}", e))?; } @@ -224,11 +231,10 @@ 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" - ); + signatures + .push(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)); diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..50b3f5d --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.92"