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,370 changes: 735 additions & 635 deletions lean_client/Cargo.lock

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions lean_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,9 @@ features = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6
hex = "0.4.3"
http_api_utils = { git = "https://github.com/grandinetech/grandine", rev = "64afdee3c6be79fceffb66933dcb69a943f3f1ae" }
k256 = "0.13"
lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig", rev = "e4474138487eeb1ed7c2e1013674fe80ac9f3165" }
leansig = { git = "https://github.com/leanEthereum/leanSig", rev = "73bedc26ed961b110df7ac2e234dc11361a4bf25" }
rec_aggregation = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" }
leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" }
leansig_wrapper = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "fd8814045deb0ef8fcad4c9f4b1250ee33f7dd01" }
libp2p = { version = "0.56.0", default-features = false, features = [
'dns',
'gossipsub',
Expand All @@ -269,8 +270,9 @@ parking_lot = "0.12"
paste = "1.0.15"
pretty_assertions = "1.4"
prometheus = "0.14"
rand = "0.9"
rand_chacha = "0.9"
rand = "0.10"
rand_chacha = "0.10"
rayon = "1"
rstest = "0.18"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
1 change: 1 addition & 0 deletions lean_client/containers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ bls = { workspace = true }
env-config = { workspace = true }
hex = { workspace = true }
metrics = { workspace = true }
rayon = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
Expand Down
55 changes: 43 additions & 12 deletions lean_client/containers/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ pub type AttestationSignatures = PersistentList<AggregatedSignatureProof, Valida

/// Aggregated signature proof with participant tracking.
///
/// This type combines the participant bitfield with the proof bytes,
/// matches ream/zeam's `AggregatedSignatureProof` container structure.
/// Combines the participant bitfield with the proof bytes.
/// Used in `aggregated_payloads` to track which validators are covered by each proof.
#[derive(Clone, Debug, Ssz, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AggregatedSignatureProof {
/// Bitfield indicating which validators' signatures are included.
pub participants: AggregationBits,
/// The raw aggregated proof bytes from lean-multisig.
/// The raw aggregated proof bytes (lz4+postcard serialized AggregatedXMSS).
pub proof_data: AggregatedSignature,
}

Expand All @@ -35,11 +34,48 @@ impl AggregatedSignatureProof {
public_keys: impl IntoIterator<Item = PublicKey>,
signatures: impl IntoIterator<Item = Signature>,
message: H256,
epoch: u32,
slot: u32,
log_inv_rate: usize,
) -> Result<Self> {
Ok(Self {
participants,
proof_data: AggregatedSignature::aggregate(public_keys, signatures, message, epoch)?,
proof_data: AggregatedSignature::aggregate(
public_keys,
signatures,
message,
slot,
log_inv_rate,
)?,
})
}

/// Aggregate with optional recursive child proofs for proof compaction.
///
/// `children` is a list of `(public_keys_covered, child_proof)` pairs where
/// each child proof previously aggregated the listed keys.
pub fn aggregate_with_children(
participants: AggregationBits,
children: &[(&[PublicKey], &AggregatedSignatureProof)],
public_keys: impl IntoIterator<Item = PublicKey>,
signatures: impl IntoIterator<Item = Signature>,
message: H256,
slot: u32,
log_inv_rate: usize,
) -> Result<Self> {
let xmss_children: Vec<(&[PublicKey], &AggregatedSignature)> = children
.iter()
.map(|(pks, proof)| (*pks, &proof.proof_data))
.collect();
Ok(Self {
participants,
proof_data: AggregatedSignature::aggregate_with_children(
&xmss_children,
public_keys,
signatures,
message,
slot,
log_inv_rate,
)?,
})
}

Expand All @@ -52,9 +88,9 @@ impl AggregatedSignatureProof {
&self,
public_keys: impl IntoIterator<Item = PublicKey>,
message: H256,
epoch: u32,
slot: u32,
) -> Result<()> {
self.proof_data.verify(public_keys, message, epoch)
self.proof_data.verify(public_keys, message, slot)
}
}

Expand Down Expand Up @@ -83,10 +119,6 @@ impl AggregationBits {

let mut bits = BitList::<U4096>::with_length((max_id + 1) as usize);

for i in 0..=max_id {
bits.set(i as usize, false);
}

for &i in indices {
bits.set(i as usize, true);
}
Expand Down Expand Up @@ -223,7 +255,6 @@ impl AggregatedAttestation {
}

/// Aggregated attestation bundled with aggregated signature proof.
/// Structure matches ream/zeam for devnet-3 interoperability.
#[derive(Clone, Debug, Ssz)]
pub struct SignedAggregatedAttestation {
/// The attestation data being attested to.
Expand Down
198 changes: 69 additions & 129 deletions lean_client/containers/src/block.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{Attestation, Slot, State};
use crate::{Slot, State};
use anyhow::{Context, Result, ensure};
use metrics::METRICS;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use ssz::{H256, Ssz, SszHash};
use xmss::Signature;
Expand Down Expand Up @@ -38,16 +39,6 @@ pub struct Block {
pub body: BlockBody,
}

/// Bundle containing a block and the proposer's attestation.
#[derive(Clone, Debug, Ssz, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlockWithAttestation {
/// The proposed block message.
pub block: Block,
/// The proposer's attestation corresponding to this block.
pub proposer_attestation: Attestation,
}

// todo(containers): default implementation doesn't make sense here
#[derive(Debug, Clone, Ssz, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
Expand All @@ -57,77 +48,30 @@ pub struct BlockSignatures {
pub proposer_signature: Signature,
}

/// Envelope carrying a block, an attestation from proposer, and aggregated signatures.
/// Signed block for devnet4: block body + aggregated signatures.
/// Proposer attestation is no longer embedded in the block message.
#[derive(Clone, Debug, Ssz, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignedBlockWithAttestation {
/// The block plus an attestation from proposer being signed.
pub message: BlockWithAttestation,
/// Aggregated signature payload for the block.
///
/// Signatures remain in attestation order followed by the proposer signature.
pub signature: BlockSignatures,
}

/// Legacy signed block structure (kept for backwards compatibility).
#[derive(Clone, Debug, Ssz)]
pub struct SignedBlock {
pub message: Block,
pub signature: Signature,
/// The proposed block.
pub block: Block,
/// Aggregated signature payload (attestation proofs + proposer signature).
#[serde(alias = "signatures")]
pub signature: BlockSignatures,
}

impl SignedBlockWithAttestation {
impl SignedBlock {
/// Verify all XMSS signatures in this signed block.
///
/// This function ensures that every attestation included in the block
/// (both on-chain attestations from the block body and the proposer's
/// own attestation) is properly signed by the claimed validator using
/// their registered XMSS public key.
///
/// # XMSS Verification
///
/// ## Without feature flag (default):
/// The function performs structural validation only:
/// - Verifies signature count matches attestation count
/// - Validates validator indices are within bounds
/// - Prepares all data for verification
///
/// ## With `xmss-verify` feature flag:
/// Enables cryptographic XMSS signature verification using the leanSig library.
///
/// To enable: `cargo build --features xmss-verify`
///
/// # Arguments
///
/// * `parent_state` - The state at the parent block, used to retrieve
/// validator public keys and verify signatures.
///
/// # Returns
///
/// `true` if all signatures are cryptographically valid (or verification is disabled).
///
/// # Panics
///
/// Panics if validation fails:
/// - Signature count mismatch
/// - Validator index out of range
/// - XMSS signature verification failure (when feature enabled)
///
/// # References
///
/// - Spec: <https://github.com/leanEthereum/leanSpec/blob/main/src/lean_spec/subspecs/containers/block/block.py#L35>
/// - XMSS Library: <https://github.com/leanEthereum/leanSig>
/// Verifies all attestation signatures using lean-multisig aggregated proofs.
/// Each attestation has a single `MultisigAggregatedSignature` proof that covers
/// all participating validators.
/// Verifies each aggregated attestation proof against the participant
/// validator public keys from parent state.
///
/// Returns `Ok(())` if all signatures are valid, or an error describing the failure.
pub fn verify_signatures(&self, parent_state: State) -> Result<()> {
// Unpack the signed block components
let block = &self.message.block;
let signatures = &self.signature;
let block = &self.block;
let signature = &self.signature;
let aggregated_attestations = &block.body.attestations;
let attestation_signatures = &signatures.attestation_signatures;
let attestation_signatures = &signature.attestation_signatures;

// Verify signature count matches aggregated attestation count
ensure!(
Expand All @@ -140,79 +84,75 @@ impl SignedBlockWithAttestation {
let validators = &parent_state.validators;
let num_validators = validators.len_u64();

// Verify each aggregated attestation's zkVM proof
for (aggregated_attestation, aggregated_signature) in aggregated_attestations
// Phase 1: collect all verification inputs (serial - reads from State)
let verification_tasks = aggregated_attestations
.into_iter()
.zip(attestation_signatures.into_iter())
{
let validator_ids = aggregated_attestation
.aggregation_bits
.to_validator_indices();

// Ensure all validators exist in the active set
for validator_id in &validator_ids {
ensure!(
*validator_id < num_validators,
"validator index {validator_id} out of range (max {num_validators})"
);
}

let attestation_data_root = aggregated_attestation.data.hash_tree_root();

// Collect validators, returning error if any not found
let public_keys = validator_ids
.into_iter()
.map(|id| {
validators
.get(id)
.map(|validator| validator.pubkey.clone())
.map_err(Into::into)
})
.collect::<Result<Vec<_>>>()?;

// Verify the lean-multisig aggregated proof for this attestation
//
// The proof verifies that all validators in aggregation_bits signed
// the same attestation_data_root at the given epoch (slot).
aggregated_signature
.verify(
public_keys,
attestation_data_root,
aggregated_attestation.data.slot.0 as u32,
)
.context("attestation aggregated signature verification failed")?;
}

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

.map(|(aggregated_attestation, aggregated_signature)| {
let validator_ids = aggregated_attestation
.aggregation_bits
.to_validator_indices();

// Ensure all validators exist in the active set
for validator_id in &validator_ids {
ensure!(
*validator_id < num_validators,
"validator index {validator_id} out of range (max {num_validators})"
);
}

let attestation_data_root = aggregated_attestation.data.hash_tree_root();
let slot = aggregated_attestation.data.slot.0 as u32;

// Collect validators, returning error if any not found
let public_keys = validator_ids
.into_iter()
.map(|id| {
validators
.get(id)
.map(|validator| validator.attestation_pubkey.clone())
.map_err(Into::into)
})
.collect::<Result<Vec<_>>>()?;

Ok((public_keys, attestation_data_root, slot, aggregated_signature))
})
.collect::<Result<Vec<_>>>()?;

// Phase 2: verify all proofs in parallel (CPU-intensive XMSS verification)
verification_tasks
.into_par_iter()
.try_for_each(|(public_keys, attestation_data_root, slot, aggregated_signature)| {
aggregated_signature
.verify(public_keys, attestation_data_root, slot)
.context("attestation aggregated signature verification failed")
})?;

// Verify the proposer's XMSS signature over the block root
let proposer_index = block.proposer_index;
ensure!(
proposer_attestation.validator_id < num_validators,
"proposer index {} out of range (max {num_validators})",
proposer_attestation.validator_id
proposer_index < num_validators,
"proposer index {proposer_index} out of range (max {num_validators})"
);

let proposer = validators
.get(proposer_attestation.validator_id)
.context(format!(
"proposer {} not found in state",
proposer_attestation.validator_id
))?;
.get(proposer_index)
.context(format!("proposer {proposer_index} not found in state"))?;

let _timer = METRICS.get().map(|metrics| {
metrics
.lean_pq_sig_attestation_verification_time_seconds
.start_timer()
});

proposer_signature
signature
.proposer_signature
.verify(
&proposer.pubkey,
proposer_attestation.data.slot.0 as u32,
proposer_attestation.data.hash_tree_root(),
&proposer.proposal_pubkey,
block.slot.0 as u32,
block.hash_tree_root(),
)
.context("Proposer signature verification failed")?;
.context("proposer signature verification failed")?;

Ok(())
}
Expand Down
Loading
Loading