Skip to content
Draft
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
21 changes: 15 additions & 6 deletions crates/blockchain/src/block_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,8 +711,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 @@ -917,11 +921,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: 59 additions & 77 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 @@ -473,103 +473,85 @@ impl BlockChainServer {
return;
};

// Assemble SignedBlock: wrap the proposer's raw XMSS signature into a
// singleton Type-1 SNARK, then merge it with every attestation Type-1
// into the block's single Type-2 proof.
// 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),
};

// Process the block locally before publishing
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