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
7 changes: 4 additions & 3 deletions crates/blockchain/src/block_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ mod tests {
use super::*;
use ethlambda_types::{
attestation::{AggregatedAttestation, AggregationBits, AttestationData},
block::{ByteList512KiB, SignedBlock, TypeOneMultiSignature},
block::{ByteList512KiB, MultiMessageAggregate, SignedBlock, TypeOneMultiSignature},
checkpoint::Checkpoint,
state::State,
};
Expand Down Expand Up @@ -891,8 +891,9 @@ mod tests {
// would attach. The actual SNARK can't be built without lean-multisig,
// but the size cap (`ByteList512KiB`) bounds the worst case.
let _ = signatures;
let proof =
ByteList512KiB::try_from(vec![0xAB; 512 * 1024]).expect("worst-case proof fits in cap");
let proof = MultiMessageAggregate::new(
ByteList512KiB::try_from(vec![0xAB; 512 * 1024]).expect("worst-case proof fits in cap"),
);
let signed_block = SignedBlock {
message: block,
proof,
Expand Down
16 changes: 7 additions & 9 deletions crates/blockchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ethlambda_types::{
ShortRoot,
aggregator::AggregatorController,
attestation::{SignedAggregatedAttestation, SignedAttestation},
block::{ByteList512KiB, SignedBlock},
block::{ByteList512KiB, MultiMessageAggregate, SignedBlock},
primitives::{H256, HashTreeRoot as _},
signature::{ValidatorPublicKey, ValidatorSignature},
};
Expand Down Expand Up @@ -510,11 +510,9 @@ impl BlockChainServer {
}
merge_inputs.push((vec![proposer_pubkey], proposer_proof_bytes));

// Merge yields raw lean-multisig Type-2 bytes; wrap them in the
// thin SSZ container the spec uses (`[4-byte offset][type2_wire]`)
// before stashing into the block envelope (leanSpec PR #717 wire
// format). Per-component participants are rederived at verify time
// from `block.body.attestations[i].aggregation_bits` plus
// 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,
Expand All @@ -524,10 +522,10 @@ impl BlockChainServer {
return;
}
};
let proof_bytes = match SignedBlock::wrap_merged_proof(merged_bytes.iter().as_slice()) {
let proof = match MultiMessageAggregate::from_bytes(merged_bytes.iter().as_slice()) {
Ok(p) => p,
Err(err) => {
error!(%slot, %validator_id, %err, "Failed to wrap merged proof envelope");
error!(%slot, %validator_id, %err, "Failed to build multi-message aggregate");
metrics::inc_block_building_failures();
return;
}
Expand All @@ -536,7 +534,7 @@ impl BlockChainServer {
drop(type_one_proofs);
let signed_block = SignedBlock {
message: block,
proof: proof_bytes,
proof,
};

// Process the block locally before publishing
Expand Down
10 changes: 3 additions & 7 deletions crates/blockchain/src/reaggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,9 @@ pub fn reaggregate_from_block(
Err(_) => continue,
};

// Step 1: SNARK-split this attestation's component out of the
// block's merged Type-2 proof. Strip the SSZ container header so
// lean-multisig sees raw bytes.
let Ok(merged_bytes) = signed_block.merged_proof_bytes() else {
debug!("Reaggregation skipped: block proof envelope unusable");
return Vec::new();
};
// 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();
let split_bytes = match ethlambda_crypto::split_type_2_by_message(
merged_bytes,
pubkeys_per_component.clone(),
Expand Down
18 changes: 5 additions & 13 deletions crates/blockchain/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -964,16 +964,7 @@ pub fn verify_block_signatures(
u32::try_from(block.slot).map_err(|_| StoreError::SlotOutOfRange(block.slot))?;
expected_bindings.push((block_root, block_slot_u32));

// Strip the thin SSZ container wrapper to recover the raw lean-multisig
// Type-2 bytes the verifier consumes. The spec carries
// `signed_block.proof = [4-byte offset = 4][type2_wire]` so other clients
// can decode through the spec's `TypeTwoMultiSignature` SSZ container
// (leanSpec PR #717).
let merged_bytes = signed_block.merged_proof_bytes().map_err(|_| {
StoreError::AggregateVerificationFailed(
ethlambda_crypto::VerificationError::DeserializationFailed,
)
})?;
let merged_bytes = signed_block.proof.proof_bytes();

let crypto_start = std::time::Instant::now();
ethlambda_crypto::verify_type_2_signature(
Expand Down Expand Up @@ -1049,7 +1040,8 @@ mod tests {
use ethlambda_types::{
attestation::{AggregatedAttestation, AggregationBits, AttestationData},
block::{
AggregatedAttestations, BlockBody, ByteList512KiB, SignedBlock, TypeOneMultiSignature,
AggregatedAttestations, BlockBody, MultiMessageAggregate, SignedBlock,
TypeOneMultiSignature,
},
checkpoint::Checkpoint,
state::State,
Expand All @@ -1064,8 +1056,8 @@ mod tests {
fn make_signed_block_proof(
_proposer_index: u64,
_attestation_proofs: Vec<TypeOneMultiSignature>,
) -> ByteList512KiB {
ByteList512KiB::default()
) -> MultiMessageAggregate {
MultiMessageAggregate::default()
}

fn make_bits(indices: &[usize]) -> AggregationBits {
Expand Down
9 changes: 4 additions & 5 deletions crates/common/test-fixtures/src/fork_choice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
deser_xmss_hex,
};
use ethlambda_types::attestation::XmssSignature;
use ethlambda_types::block::{ByteList512KiB, SignedBlock};
use ethlambda_types::block::{MultiMessageAggregate, SignedBlock};
use ethlambda_types::primitives::H256;
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
Expand Down Expand Up @@ -157,13 +157,12 @@ impl BlockStepData {
///
/// Used by callers that import the block via `on_block_without_verification`
/// (fork-choice spec-test runner and Hive test-driver), which skip the
/// crypto verifier entirely. Under the leanSpec PR #717 wire format the
/// merged proof bytes live opaquely on `SignedBlock.proof` and are only
/// inspected by `verify_block_signatures`, so an empty blob suffices.
/// crypto verifier entirely. The merged proof bytes are only inspected by
/// `verify_block_signatures`, so an empty aggregate suffices.
pub fn to_blank_signed_block(&self) -> SignedBlock {
SignedBlock {
message: self.to_block(),
proof: ByteList512KiB::default(),
proof: MultiMessageAggregate::default(),
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions crates/common/test-fixtures/src/verify_signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//! proof: { proof: { data: "0x<hex-encoded merged Type-2 bytes>" } }

use crate::{Block, TestInfo, TestState};
use ethlambda_types::block::SignedBlock;
use ethlambda_types::block::{MultiMessageAggregate, SignedBlock};
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt;
Expand Down Expand Up @@ -123,15 +123,15 @@ impl TestSignedBlock {
/// Materialize a `SignedBlock` preserving the fixture-supplied merged
/// Type-2 proof bytes verbatim.
///
/// The container carries the raw lean-multisig wire, so it gets wrapped
/// into the SSZ-container envelope that `SignedBlock.proof` stores.
/// The container carries the raw lean-multisig wire in the
/// `MultiMessageAggregate` stored by `SignedBlock.proof`.
pub fn try_into_signed_block_with_proofs(self) -> Result<SignedBlock, SignedBlockConvertError> {
let bytes = self
.proof
.decode()
.map_err(|err| SignedBlockConvertError::InvalidProofHex(err.to_string()))?;
let len = bytes.len();
let proof = SignedBlock::wrap_merged_proof(&bytes)
let proof = MultiMessageAggregate::from_bytes(&bytes)
.map_err(|_| SignedBlockConvertError::ProofTooLarge(len))?;
Ok(SignedBlock {
message: self.block.into(),
Expand Down
135 changes: 66 additions & 69 deletions crates/common/types/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ use primitives::HashTreeRoot as _;
/// Envelope carrying a block and the single merged proof binding every
/// signature it depends on.
///
/// `proof` holds the SSZ-encoded form of a [`TypeTwoMultiSignature`]
/// container whose only field is a `ByteList512KiB` holding the raw
/// `compress_without_pubkeys()` Type-2 merged proof bytes. On the wire the
/// container collapses to `[4-byte offset = 4][type2_wire]` — a thin
/// 4-byte prefix in front of the lean-multisig bytes (leanSpec PR #717).
///
/// <div class="warning">
///
/// `HashTreeRoot` is intentionally not derived: consumers never hash a
Expand All @@ -33,80 +27,71 @@ pub struct SignedBlock {
/// The block being signed.
pub message: Block,

/// SSZ-encoded `TypeTwoMultiSignature` envelope. Use
/// [`SignedBlock::merged_proof_bytes`] to extract the raw
/// lean-multisig Type-2 bytes inside, or
/// [`SignedBlock::wrap_merged_proof`] when building an envelope from
/// the prover output.
/// Single full-block proof covering attestations and the proposer signature.
pub proof: MultiMessageAggregate,
}

// Manual Debug impl because the merged proof bytes are large and opaque.
impl core::fmt::Debug for SignedBlock {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SignedBlock")
.field("message", &self.message)
.field("proof", &format_args!("<{} bytes>", self.proof.proof.len()))
.finish()
}
}

/// 512 KiB byte-list cap shared by every block-level / Type-1 proof field.
/// Matches leanSpec PR #717's `ByteList512KiB` SSZ container.
pub type ByteList512KiB = ByteList<524_288>;

/// A merged proof covering multiple messages with a single proof blob.
///
/// The proof bytes use lean-multisig's compact public-key-free
/// representation. SSZ encoding this container adds the offset required for
/// its variable-length field.
#[derive(Debug, Default, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)]
pub struct MultiMessageAggregate {
/// Serialized multi-message aggregate proof bytes.
pub proof: ByteList512KiB,
}

impl SignedBlock {
/// Strip the SSZ-container offset header to return the raw
/// lean-multisig Type-2 merged proof bytes the verifier consumes.
pub fn merged_proof_bytes(&self) -> Result<&[u8], ProofEnvelopeError> {
let bytes = self.proof.iter().as_slice();
if bytes.len() < 4 {
return Err(ProofEnvelopeError::TruncatedEnvelope);
}
let mut header = [0u8; 4];
header.copy_from_slice(&bytes[..4]);
let offset = u32::from_le_bytes(header) as usize;
if offset != 4 {
return Err(ProofEnvelopeError::UnexpectedOffset(offset));
}
Ok(&bytes[4..])
impl MultiMessageAggregate {
/// Build an aggregate from an already bounded proof byte list.
pub fn new(proof: ByteList512KiB) -> Self {
Self { proof }
}

/// Copy raw lean-multisig proof bytes into the bounded SSZ container.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, MultiMessageAggregateError> {
let len = bytes.len();
ByteList512KiB::try_from(bytes.to_vec())
.map(Self::new)
.map_err(|_| MultiMessageAggregateError::ProofTooLarge(len))
}

/// Wrap raw lean-multisig Type-2 bytes into a `SignedBlock.proof`
/// envelope: prepend the 4-byte SSZ offset header so the wire matches
/// the spec's `TypeTwoMultiSignature { proof: ByteList512KiB }`
/// container.
pub fn wrap_merged_proof(type2_wire: &[u8]) -> Result<ByteList512KiB, ProofEnvelopeError> {
let mut wrapped = Vec::with_capacity(4 + type2_wire.len());
wrapped.extend_from_slice(&4u32.to_le_bytes());
wrapped.extend_from_slice(type2_wire);
let len = wrapped.len();
ByteList512KiB::try_from(wrapped).map_err(|_| ProofEnvelopeError::ExceedsCap(len))
/// Return the raw lean-multisig proof bytes.
pub fn proof_bytes(&self) -> &[u8] {
self.proof.iter().as_slice()
}
}

/// Errors returned by the [`SignedBlock`] proof-envelope helpers.
/// Errors returned when constructing a [`MultiMessageAggregate`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProofEnvelopeError {
/// Envelope is shorter than the 4-byte SSZ offset header.
TruncatedEnvelope,
/// Offset header is not the expected single-field value `4`.
UnexpectedOffset(usize),
/// Wrapped proof would exceed `ByteList512KiB`'s cap.
ExceedsCap(usize),
pub enum MultiMessageAggregateError {
/// Proof bytes exceed `ByteList512KiB`'s cap.
ProofTooLarge(usize),
}

impl core::fmt::Display for ProofEnvelopeError {
impl core::fmt::Display for MultiMessageAggregateError {
Comment thread
MegaRedHand marked this conversation as resolved.
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::TruncatedEnvelope => f.write_str("block proof envelope truncated"),
Self::UnexpectedOffset(o) => write!(f, "block proof envelope offset {o}, expected 4"),
Self::ExceedsCap(n) => write!(f, "wrapped proof {n} bytes exceeds 512 KiB cap"),
Self::ProofTooLarge(n) => write!(f, "proof {n} bytes exceeds 512 KiB cap"),
}
}
}

impl std::error::Error for ProofEnvelopeError {}

// Manual Debug impl because the merged proof bytes are large and opaque.
impl core::fmt::Debug for SignedBlock {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SignedBlock")
.field("message", &self.message)
.field("proof", &format_args!("<{} bytes>", self.proof.len()))
.finish()
}
}

/// 512 KiB byte-list cap shared by every block-level / Type-1 proof field.
/// Matches leanSpec PR #717's `ByteList512KiB` SSZ container.
pub type ByteList512KiB = ByteList<524_288>;
impl std::error::Error for MultiMessageAggregateError {}

// ============================================================================
// Type-1 multi-signature
Expand All @@ -118,10 +103,10 @@ pub type ByteList512KiB = ByteList<524_288>;
// from the surrounding block body (attestation `data` + slot for body
// components, block root + slot for the proposer component).
//
// `TypeTwoMultiSignature` has no Rust-side struct: the block carries the
// raw lean-multisig Type-2 bytes directly on `SignedBlock.proof`. Component
// participant bitfields come from `block.body.attestations[i].aggregation_bits`
// (and `block.proposer_index` for the trailing proposer entry).
// `MultiMessageAggregate` carries the raw lean-multisig Type-2 bytes.
// Component participant bitfields come from
// `block.body.attestations[i].aggregation_bits` (and `block.proposer_index` for
// the trailing proposer entry).

/// Maximum number of distinct `AttestationData` entries permitted in a single
/// block. Canonical home for the cap shared across `ethlambda-blockchain`,
Expand Down Expand Up @@ -322,15 +307,27 @@ mod tests {
};
let signed = SignedBlock {
message: block,
proof: ByteList512KiB::default(),
proof: MultiMessageAggregate::default(),
};
let bytes = signed.to_ssz();
let decoded = SignedBlock::from_ssz_bytes(&bytes).expect("decode");
assert_eq!(decoded.proof.len(), 0);
assert_eq!(decoded.proof.proof.len(), 0);
assert_eq!(decoded.message.slot, signed.message.slot);
assert_eq!(
decoded.message.proposer_index,
signed.message.proposer_index
);
}

#[test]
fn multi_message_aggregate_ssz_wraps_proof_bytes() {
let proof_bytes: Vec<u8> = (0..64).collect();
let aggregate = MultiMessageAggregate::from_bytes(&proof_bytes).unwrap();

let encoded = aggregate.to_ssz();

assert_eq!(&encoded[..4], &4u32.to_le_bytes());
assert_eq!(&encoded[4..], proof_bytes);
assert_eq!(aggregate.proof_bytes(), proof_bytes);
}
}
2 changes: 1 addition & 1 deletion crates/common/types/tests/ssz_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ impl From<SignedAttestation> for DomainSignedAttestation {

// NOTE: After Phase 3 the legacy `BlockSignatures` / `AttestationSignatures` /
// `AggregatedSignatureProof` containers are removed from the domain, and
// `SignedBlock` now carries a single `proof: ByteList512KiB` field. The pinned
// `SignedBlock` now carries a single `proof: MultiMessageAggregate` field. The pinned
// leanSpec fixtures still use the old shape, so SSZ-byte and root assertions
// for `SignedBlock`, `BlockSignatures`, `AggregatedSignatureProof`, and
// `SignedAggregatedAttestation` are intentionally skipped in
Expand Down
4 changes: 2 additions & 2 deletions crates/net/p2p/src/req_resp/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ mod tests {
use super::*;
use ethlambda_storage::{ForkCheckpoints, backend::InMemoryBackend};
use ethlambda_types::{
block::{Block, BlockBody, ByteList512KiB},
block::{Block, BlockBody, MultiMessageAggregate},
state::State,
};
use std::sync::Arc;
Expand All @@ -414,7 +414,7 @@ mod tests {
state_root: H256::ZERO,
body: BlockBody::default(),
},
proof: ByteList512KiB::default(),
proof: MultiMessageAggregate::default(),
}
}

Expand Down
Loading
Loading