Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lean_client/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 60 additions & 31 deletions lean_client/containers/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
Attestation, Bytes32, MultisigAggregatedSignature, Signature, Slot, State, ValidatorIndex,
};
use serde::{Deserialize, Serialize};
use ssz::SszHash;
use ssz_derive::Ssz;

use crate::attestation::{AggregatedAttestations, AttestationSignatures};
Expand Down Expand Up @@ -80,6 +81,11 @@ pub fn hash_tree_root<T: ssz::SszHash>(value: &T) -> Bytes32 {
Bytes32(h)
}

/// Compute the canonical block root for a Block.
pub fn compute_block_root(block: &Block) -> Bytes32 {
Bytes32(block.hash_tree_root())
}

impl SignedBlockWithAttestation {
/// Verify all XMSS signatures in this signed block.
///
Expand Down Expand Up @@ -124,19 +130,23 @@ impl SignedBlockWithAttestation {
/// Verifies all attestation signatures using lean-multisig aggregated proofs.
/// Each attestation has a single `MultisigAggregatedSignature` proof that covers
/// all participating validators.
pub fn verify_signatures(&self, parent_state: State) -> bool {
///
/// Returns `Ok(())` if all signatures are valid, or an error describing the failure.
pub fn verify_signatures(&self, parent_state: State) -> Result<(), String> {
// Unpack the signed block components
let block = &self.message.block;
let signatures = &self.signature;
let aggregated_attestations = block.body.attestations.clone();
let attestation_signatures = signatures.attestation_signatures.clone();

// Verify signature count matches aggregated attestation count
assert_eq!(
aggregated_attestations.len_u64(),
attestation_signatures.len_u64(),
"Attestation signature groups must align with block body attestations"
);
if aggregated_attestations.len_u64() != attestation_signatures.len_u64() {
return Err(format!(
"Attestation signature count mismatch: {} attestations vs {} signatures",
aggregated_attestations.len_u64(),
attestation_signatures.len_u64()
));
}

let validators = &parent_state.validators;
let num_validators = validators.len_u64();
Expand All @@ -152,57 +162,76 @@ impl SignedBlockWithAttestation {

// Ensure all validators exist in the active set
for validator_id in &validator_ids {
assert!(
*validator_id < num_validators,
"Validator index out of range"
);
if *validator_id >= num_validators {
return Err(format!(
"Validator index {} out of range (max {})",
validator_id, num_validators
));
}
}

let attestation_data_root: [u8; 32] =
hash_tree_root(&aggregated_attestation.data).0.into();

// Collect validators, returning error if any not found
let mut collected_validators = Vec::with_capacity(validator_ids.len());
for vid in &validator_ids {
let validator = validators
.get(*vid)
.map_err(|_| format!("Validator {} not found in state", vid))?;
collected_validators.push(validator);
}

// Verify the lean-multisig aggregated proof for this attestation
//
// The proof verifies that all validators in aggregation_bits signed
// the same attestation_data_root at the given epoch (slot).
_aggregated_signature_proof
.proof_data
.verify_aggregated_payload(
&validator_ids
.iter()
.map(|vid| validators.get(*vid).expect("validator must exist"))
.collect::<Vec<_>>(),
&collected_validators,
&attestation_data_root,
aggregated_attestation.data.slot.0 as u32,
)
.expect("Attestation aggregated signature verification failed");
.map_err(|e| {
format!(
"Attestation aggregated signature verification failed: {:?}",
e
)
})?;
}

// Verify the proposer attestation signature (outside the attestation loop)
let proposer_attestation = &self.message.proposer_attestation;
let proposer_signature = &signatures.proposer_signature;

assert!(
proposer_attestation.validator_id.0 < num_validators,
"Proposer index out of range"
);
if proposer_attestation.validator_id.0 >= num_validators {
return Err(format!(
"Proposer index {} out of range (max {})",
proposer_attestation.validator_id.0, num_validators
));
}

let proposer = validators
.get(proposer_attestation.validator_id.0)
.expect("proposer must exist");
.map_err(|_| {
format!(
"Proposer {} not found in state",
proposer_attestation.validator_id.0
)
})?;

let proposer_root: [u8; 32] = hash_tree_root(&proposer_attestation.data).0.into();
assert!(
verify_xmss_signature(
proposer.pubkey,
proposer_attestation.data.slot,
&proposer_root,
proposer_signature,
),
"Proposer attestation signature verification failed"
);

true
if !verify_xmss_signature(
proposer.pubkey,
proposer_attestation.data.slot,
&proposer_root,
proposer_signature,
) {
return Err("Proposer attestation signature verification failed".to_string());
}

Ok(())
}
}

Expand Down
42 changes: 25 additions & 17 deletions lean_client/containers/src/public_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,20 @@ impl SszSize for PublicKey {

// 2. Define how to write (Serialize)
impl SszWrite for PublicKey {
fn write_fixed(&self, _bytes: &mut [u8]) {
panic!("SszWrite::write_fixed must be implemented for fixed-size types");
fn write_fixed(&self, bytes: &mut [u8]) {
// Write the 52 bytes of the public key
bytes[..PUBLIC_KEY_SIZE].copy_from_slice(&self.inner);
}

fn write_variable(&self, _bytes: &mut Vec<u8>) -> Result<(), WriteError> {
panic!("SszWrite::write_variable must be implemented for variable-size types");
// PublicKey is fixed-size, this should not be called
panic!("PublicKey is fixed-size, write_variable should not be called");
}

fn to_ssz(&self) -> Result<Vec<u8>, WriteError> {
match Self::SIZE {
Size::Fixed { size } => {
let mut bytes = vec![0; size];
self.write_fixed(bytes.as_mut_slice());
Ok(bytes)
}
Size::Variable { minimum_size } => {
let mut bytes = Vec::with_capacity(minimum_size);
self.write_variable(&mut bytes)?;
Ok(bytes)
}
}
let mut bytes = vec![0u8; PUBLIC_KEY_SIZE];
self.write_fixed(&mut bytes);
Ok(bytes)
}
}

Expand Down Expand Up @@ -100,11 +93,26 @@ impl SszHash for PublicKey {
type PackingFactor = typenum::U1;

fn hash_tree_root(&self) -> H256 {
// Simple implementation: hash the inner bytes directly
// SSZ hash_tree_root for fixed-size types > 32 bytes:
// 1. Split into 32-byte chunks
// 2. Pad last chunk with zeros if needed
// 3. Merkleize the chunks
use sha2::{Digest, Sha256};

// For 52 bytes: 2 chunks (32 + 20 bytes, second chunk padded to 32)
let mut chunk1 = [0u8; 32];
let mut chunk2 = [0u8; 32];

chunk1.copy_from_slice(&self.inner[0..32]);
chunk2[..20].copy_from_slice(&self.inner[32..52]);
// Remaining 12 bytes of chunk2 are already zeros (padding)

// Merkleize: hash(chunk1 || chunk2)
let mut hasher = Sha256::new();
hasher.update(&self.inner);
hasher.update(&chunk1);
hasher.update(&chunk2);
let result = hasher.finalize();

H256::from_slice(&result)
}
}
Expand Down
Loading