Skip to content
21 changes: 15 additions & 6 deletions crates/blockchain/src/block_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,8 +797,12 @@ fn trace_skipped_attestation(reason: &'static str, att: &AttestationData, data_r
mod tests {
use super::*;
use ethlambda_types::{
attestation::{AggregatedAttestation, AggregationBits, AttestationData},
block::{ByteList512KiB, MultiMessageAggregate, SignedBlock, TypeOneMultiSignature},
attestation::{
AggregatedAttestation, AggregationBits, AttestationData, blank_xmss_signature,
},
block::{
BlockProof, ByteList512KiB, MultiMessageAggregate, SignedBlock, TypeOneMultiSignature,
},
checkpoint::Checkpoint,
state::State,
};
Expand Down Expand Up @@ -1004,11 +1008,16 @@ mod tests {
);

// Substitute a worst-case-size proof to model what `propose_block`
// would attach. The actual SNARK can't be built without lean-multisig,
// but the size cap (`ByteList512KiB`) bounds the worst case.
// would attach: a 512 KiB attestation aggregate plus the fixed-size
// proposer signature. The actual SNARK can't be built without
// lean-multisig, but the size cap bounds the worst case.
let _ = signatures;
let proof = MultiMessageAggregate::new(
ByteList512KiB::try_from(vec![0xAB; 512 * 1024]).expect("worst-case proof fits in cap"),
let proof = BlockProof::new(
blank_xmss_signature(),
MultiMessageAggregate::new(
ByteList512KiB::try_from(vec![0xAB; 512 * 1024])
.expect("worst-case proof fits in cap"),
),
);
let signed_block = SignedBlock {
message: block,
Expand Down
136 changes: 60 additions & 76 deletions crates/blockchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use ethlambda_types::{
ShortRoot,
aggregator::AggregatorController,
attestation::{SignedAggregatedAttestation, SignedAttestation},
block::{ByteList512KiB, MultiMessageAggregate, SignedBlock},
block::{BlockProof, ByteList512KiB, MultiMessageAggregate, SignedBlock},
primitives::{H256, HashTreeRoot as _},
signature::{ValidatorPublicKey, ValidatorSignature},
signature::ValidatorPublicKey,
};

use crate::aggregation::{
Expand Down Expand Up @@ -532,100 +532,84 @@ impl BlockChainServer {
return;
};

// Wrap the proposer's raw XMSS signature into a singleton Type-1 SNARK,
// then merge it with every attestation Type-1 into the single Type-2.
// Assemble SignedBlock: carry the proposer's raw XMSS signature as a
// standalone field, and aggregate the attestation Type-1s (only) into
// the block's attestation Type-2. The proposer no longer enters the
// aggregate, so a block with no attestations needs no prover work and
// the attestation Type-2 can be built independently of the block root.
let head_state = self.store.head_state();
let validators = &head_state.validators;
let Some(proposer_validator) = validators.get(validator_id as usize) else {
if validators.get(validator_id as usize).is_none() {
error!(%slot, %validator_id, "Proposer index out of range when assembling block");
metrics::inc_block_building_failures();
return;
};
}

// Decode the proposer's proposal pubkey once and reuse it both for the
// singleton Type-1 wrap and for the Type-2 merge inputs.
let Ok(proposer_pubkey) = proposer_validator.get_proposal_pubkey().inspect_err(
|err| error!(%slot, %validator_id, %err, "Failed to decode proposer proposal pubkey"),
) else {
metrics::inc_block_building_failures();
return;
};
// `sign_block_root` already returns an `XmssSignature`, so the proposer
// signature is carried verbatim — no packing or prover work needed.

let Ok(proposer_validator_signature) =
ValidatorSignature::from_bytes(&proposer_signature).inspect_err(|err| {
error!(%slot, %validator_id, %err, "Failed to decode proposer signature bytes")
})
else {
metrics::inc_block_building_failures();
return;
};
let Ok(proposer_proof_bytes) = ethlambda_crypto::aggregate_signatures(
vec![proposer_pubkey.clone()],
vec![proposer_validator_signature],
&block_root,
slot as u32,
)
.inspect_err(
|err| error!(%slot, %validator_id, %err, "Failed to wrap proposer signature as Type-1"),
) else {
metrics::inc_block_building_failures();
return;
};

let mut merge_inputs: Vec<(Vec<ValidatorPublicKey>, ByteList512KiB)> =
Vec::with_capacity(type_one_proofs.len() + 1);
let mut resolve_failed = false;
for t1 in &type_one_proofs {
let mut pubkeys = Vec::new();
for vid in t1.participant_indices() {
let Some(validator) = validators.get(vid as usize) else {
error!(%slot, %validator_id, vid, "Participant out of range while resolving pubkeys");
resolve_failed = true;
break;
};
match validator.get_attestation_pubkey() {
Ok(pk) => pubkeys.push(pk),
Err(err) => {
error!(%slot, %validator_id, vid, %err, "Failed to decode attestation pubkey");
// Aggregate the attestation Type-1s into a single Type-2. With no
// attestations the aggregate is empty: the proposer signature stands
// alone, mirroring `(prop-sig, empty-proof)`.
let attestation_proof = if type_one_proofs.is_empty() {
MultiMessageAggregate::default()
} else {
let mut merge_inputs: Vec<(Vec<ValidatorPublicKey>, ByteList512KiB)> =
Vec::with_capacity(type_one_proofs.len());
let mut resolve_failed = false;
for t1 in &type_one_proofs {
let mut pubkeys = Vec::new();
for vid in t1.participant_indices() {
let Some(validator) = validators.get(vid as usize) else {
error!(%slot, %validator_id, vid, "Participant out of range while resolving pubkeys");
resolve_failed = true;
break;
};
match validator.get_attestation_pubkey() {
Ok(pk) => pubkeys.push(pk),
Err(err) => {
error!(%slot, %validator_id, vid, %err, "Failed to decode attestation pubkey");
resolve_failed = true;
break;
}
}
}
if resolve_failed {
break;
}
merge_inputs.push((pubkeys, t1.proof.clone()));
}
if resolve_failed {
break;
}
merge_inputs.push((pubkeys, t1.proof.clone()));
}
if resolve_failed {
metrics::inc_block_building_failures();
return;
}
merge_inputs.push((vec![proposer_pubkey], proposer_proof_bytes));

// Merge yields raw lean-multisig Type-2 bytes. Per-component
// participants are rederived at verify time from
// `block.body.attestations[i].aggregation_bits` plus
// `block.proposer_index`, so nothing else needs persisting.
let merged_bytes = match ethlambda_crypto::merge_type_1s_into_type_2(merge_inputs) {
Ok(bytes) => bytes,
Err(err) => {
error!(%slot, %validator_id, %err, "Failed to merge Type-1s into Type-2");
metrics::inc_block_building_failures();
return;
}
};
let proof = match MultiMessageAggregate::from_bytes(merged_bytes.iter().as_slice()) {
Ok(p) => p,
Err(err) => {
error!(%slot, %validator_id, %err, "Failed to build multi-message aggregate");
metrics::inc_block_building_failures();
return;

// Merge yields raw lean-multisig Type-2 bytes. Per-component
// participants are rederived at verify time from
// `block.body.attestations[i].aggregation_bits`, so nothing else
// needs persisting.
let merged_bytes = match ethlambda_crypto::merge_type_1s_into_type_2(merge_inputs) {
Ok(bytes) => bytes,
Err(err) => {
error!(%slot, %validator_id, %err, "Failed to merge Type-1s into Type-2");
metrics::inc_block_building_failures();
return;
}
};
match MultiMessageAggregate::from_bytes(merged_bytes.iter().as_slice()) {
Ok(p) => p,
Err(err) => {
error!(%slot, %validator_id, %err, "Failed to build multi-message aggregate");
metrics::inc_block_building_failures();
return;
}
}
};
// `type_one_proofs` is no longer needed past this point.
drop(type_one_proofs);
let signed_block = SignedBlock {
message: block,
proof,
proof: BlockProof::new(proposer_signature, attestation_proof),
};

// Stop timing here: the build is done, and the alignment wait below must
Expand Down
21 changes: 7 additions & 14 deletions crates/blockchain/src/reaggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@ pub fn reaggregate_from_block(
let validators = &parent_state.validators;
let num_validators = validators.len() as u64;

// Per-component pubkeys: one entry per body attestation in order, then
// the proposer entry. Layout is invariant per block, so it's resolved
// once and reused for every split call below.
// Per-component pubkeys: one entry per body attestation in order. The
// attestation aggregate no longer carries a proposer component (the
// proposer signature lives outside it), so the layout is attestations
// only. Resolved once and reused for every split call below.
let mut pubkeys_per_component: Vec<Vec<ValidatorPublicKey>> =
Vec::with_capacity(attestations.len() + 1);
Vec::with_capacity(attestations.len());
for att in &attestations {
let mut pubkeys = Vec::new();
for vid in validator_indices(&att.aggregation_bits) {
Expand All @@ -90,14 +91,6 @@ pub fn reaggregate_from_block(
}
pubkeys_per_component.push(pubkeys);
}
if block.proposer_index >= num_validators {
return Vec::new();
}
let Ok(proposer_pubkey) = validators[block.proposer_index as usize].get_proposal_pubkey()
else {
return Vec::new();
};
pubkeys_per_component.push(vec![proposer_pubkey]);

let candidates = select_candidates(store, &attestations);
if candidates.is_empty() {
Expand All @@ -119,8 +112,8 @@ pub fn reaggregate_from_block(
};

// Step 1: SNARK-split this attestation's component out of the block's
// merged Type-2 proof.
let merged_bytes = signed_block.proof.proof_bytes();
// attestation Type-2 aggregate.
let merged_bytes = signed_block.proof.attestation_proof.proof_bytes();
let split_bytes = match ethlambda_crypto::split_type_2_by_message(
merged_bytes,
pubkeys_per_component.clone(),
Expand Down
Loading
Loading