Skip to content

Commit 810fa4d

Browse files
committed
feat(types): add MultiMessageAggregate
1 parent 9885193 commit 810fa4d

11 files changed

Lines changed: 106 additions & 121 deletions

File tree

crates/blockchain/src/block_builder.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,7 @@ mod tests {
683683
use super::*;
684684
use ethlambda_types::{
685685
attestation::{AggregatedAttestation, AggregationBits, AttestationData},
686-
block::{ByteList512KiB, SignedBlock, TypeOneMultiSignature},
686+
block::{ByteList512KiB, MultiMessageAggregate, SignedBlock, TypeOneMultiSignature},
687687
checkpoint::Checkpoint,
688688
state::State,
689689
};
@@ -891,8 +891,9 @@ mod tests {
891891
// would attach. The actual SNARK can't be built without lean-multisig,
892892
// but the size cap (`ByteList512KiB`) bounds the worst case.
893893
let _ = signatures;
894-
let proof =
895-
ByteList512KiB::try_from(vec![0xAB; 512 * 1024]).expect("worst-case proof fits in cap");
894+
let proof = MultiMessageAggregate::new(
895+
ByteList512KiB::try_from(vec![0xAB; 512 * 1024]).expect("worst-case proof fits in cap"),
896+
);
896897
let signed_block = SignedBlock {
897898
message: block,
898899
proof,

crates/blockchain/src/lib.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use ethlambda_types::{
88
ShortRoot,
99
aggregator::AggregatorController,
1010
attestation::{SignedAggregatedAttestation, SignedAttestation},
11-
block::{ByteList512KiB, SignedBlock},
11+
block::{ByteList512KiB, MultiMessageAggregate, SignedBlock},
1212
primitives::{H256, HashTreeRoot as _},
1313
signature::{ValidatorPublicKey, ValidatorSignature},
1414
};
@@ -489,11 +489,9 @@ impl BlockChainServer {
489489
}
490490
merge_inputs.push((vec![proposer_pubkey], proposer_proof_bytes));
491491

492-
// Merge yields raw lean-multisig Type-2 bytes; wrap them in the
493-
// thin SSZ container the spec uses (`[4-byte offset][type2_wire]`)
494-
// before stashing into the block envelope (leanSpec PR #717 wire
495-
// format). Per-component participants are rederived at verify time
496-
// from `block.body.attestations[i].aggregation_bits` plus
492+
// Merge yields raw lean-multisig Type-2 bytes. Per-component
493+
// participants are rederived at verify time from
494+
// `block.body.attestations[i].aggregation_bits` plus
497495
// `block.proposer_index`, so nothing else needs persisting.
498496
let merged_bytes = match ethlambda_crypto::merge_type_1s_into_type_2(merge_inputs) {
499497
Ok(bytes) => bytes,
@@ -503,10 +501,10 @@ impl BlockChainServer {
503501
return;
504502
}
505503
};
506-
let proof_bytes = match SignedBlock::wrap_merged_proof(merged_bytes.iter().as_slice()) {
504+
let proof = match MultiMessageAggregate::from_bytes(merged_bytes.iter().as_slice()) {
507505
Ok(p) => p,
508506
Err(err) => {
509-
error!(%slot, %validator_id, %err, "Failed to wrap merged proof envelope");
507+
error!(%slot, %validator_id, %err, "Failed to build multi-message aggregate");
510508
metrics::inc_block_building_failures();
511509
return;
512510
}
@@ -515,7 +513,7 @@ impl BlockChainServer {
515513
drop(type_one_proofs);
516514
let signed_block = SignedBlock {
517515
message: block,
518-
proof: proof_bytes,
516+
proof,
519517
};
520518

521519
// Process the block locally before publishing

crates/blockchain/src/reaggregate.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,9 @@ pub fn reaggregate_from_block(
118118
Err(_) => continue,
119119
};
120120

121-
// Step 1: SNARK-split this attestation's component out of the
122-
// block's merged Type-2 proof. Strip the SSZ container header so
123-
// lean-multisig sees raw bytes.
124-
let Ok(merged_bytes) = signed_block.merged_proof_bytes() else {
125-
debug!("Reaggregation skipped: block proof envelope unusable");
126-
return Vec::new();
127-
};
121+
// Step 1: SNARK-split this attestation's component out of the block's
122+
// merged Type-2 proof.
123+
let merged_bytes = signed_block.proof.proof_bytes();
128124
let split_bytes = match ethlambda_crypto::split_type_2_by_message(
129125
merged_bytes,
130126
pubkeys_per_component.clone(),

crates/blockchain/src/store.rs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -895,16 +895,7 @@ pub fn verify_block_signatures(
895895
u32::try_from(block.slot).map_err(|_| StoreError::SlotOutOfRange(block.slot))?;
896896
expected_bindings.push((block_root, block_slot_u32));
897897

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

909900
let crypto_start = std::time::Instant::now();
910901
ethlambda_crypto::verify_type_2_signature(
@@ -980,7 +971,8 @@ mod tests {
980971
use ethlambda_types::{
981972
attestation::{AggregatedAttestation, AggregationBits, AttestationData},
982973
block::{
983-
AggregatedAttestations, BlockBody, ByteList512KiB, SignedBlock, TypeOneMultiSignature,
974+
AggregatedAttestations, BlockBody, MultiMessageAggregate, SignedBlock,
975+
TypeOneMultiSignature,
984976
},
985977
checkpoint::Checkpoint,
986978
state::State,
@@ -995,8 +987,8 @@ mod tests {
995987
fn make_signed_block_proof(
996988
_proposer_index: u64,
997989
_attestation_proofs: Vec<TypeOneMultiSignature>,
998-
) -> ByteList512KiB {
999-
ByteList512KiB::default()
990+
) -> MultiMessageAggregate {
991+
MultiMessageAggregate::default()
1000992
}
1001993

1002994
fn make_bits(indices: &[usize]) -> AggregationBits {

crates/common/test-fixtures/src/fork_choice.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::{
88
deser_xmss_hex,
99
};
1010
use ethlambda_types::attestation::XmssSignature;
11-
use ethlambda_types::block::{ByteList512KiB, SignedBlock};
11+
use ethlambda_types::block::{MultiMessageAggregate, SignedBlock};
1212
use ethlambda_types::primitives::H256;
1313
use serde::{Deserialize, Deserializer};
1414
use std::collections::HashMap;
@@ -157,13 +157,12 @@ impl BlockStepData {
157157
///
158158
/// Used by callers that import the block via `on_block_without_verification`
159159
/// (fork-choice spec-test runner and Hive test-driver), which skip the
160-
/// crypto verifier entirely. Under the leanSpec PR #717 wire format the
161-
/// merged proof bytes live opaquely on `SignedBlock.proof` and are only
162-
/// inspected by `verify_block_signatures`, so an empty blob suffices.
160+
/// crypto verifier entirely. The merged proof bytes are only inspected by
161+
/// `verify_block_signatures`, so an empty aggregate suffices.
163162
pub fn to_blank_signed_block(&self) -> SignedBlock {
164163
SignedBlock {
165164
message: self.to_block(),
166-
proof: ByteList512KiB::default(),
165+
proof: MultiMessageAggregate::default(),
167166
}
168167
}
169168
}

crates/common/test-fixtures/src/verify_signatures.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//! proof: { proof: { data: "0x<hex-encoded merged Type-2 bytes>" } }
1212
1313
use crate::{Block, TestInfo, TestState};
14-
use ethlambda_types::block::SignedBlock;
14+
use ethlambda_types::block::{MultiMessageAggregate, SignedBlock};
1515
use serde::Deserialize;
1616
use std::collections::HashMap;
1717
use std::fmt;
@@ -123,15 +123,15 @@ impl TestSignedBlock {
123123
/// Materialize a `SignedBlock` preserving the fixture-supplied merged
124124
/// Type-2 proof bytes verbatim.
125125
///
126-
/// The container carries the raw lean-multisig wire, so it gets wrapped
127-
/// into the SSZ-container envelope that `SignedBlock.proof` stores.
126+
/// The container carries the raw lean-multisig wire in the
127+
/// `MultiMessageAggregate` stored by `SignedBlock.proof`.
128128
pub fn try_into_signed_block_with_proofs(self) -> Result<SignedBlock, SignedBlockConvertError> {
129129
let bytes = self
130130
.proof
131131
.decode()
132132
.map_err(|err| SignedBlockConvertError::InvalidProofHex(err.to_string()))?;
133133
let len = bytes.len();
134-
let proof = SignedBlock::wrap_merged_proof(&bytes)
134+
let proof = MultiMessageAggregate::from_bytes(&bytes)
135135
.map_err(|_| SignedBlockConvertError::ProofTooLarge(len))?;
136136
Ok(SignedBlock {
137137
message: self.block.into(),

crates/common/types/src/block.rs

Lines changed: 66 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@ use primitives::HashTreeRoot as _;
1414
/// Envelope carrying a block and the single merged proof binding every
1515
/// signature it depends on.
1616
///
17-
/// `proof` holds the SSZ-encoded form of a [`TypeTwoMultiSignature`]
18-
/// container whose only field is a `ByteList512KiB` holding the raw
19-
/// `compress_without_pubkeys()` Type-2 merged proof bytes. On the wire the
20-
/// container collapses to `[4-byte offset = 4][type2_wire]` — a thin
21-
/// 4-byte prefix in front of the lean-multisig bytes (leanSpec PR #717).
22-
///
2317
/// <div class="warning">
2418
///
2519
/// `HashTreeRoot` is intentionally not derived: consumers never hash a
@@ -33,80 +27,71 @@ pub struct SignedBlock {
3327
/// The block being signed.
3428
pub message: Block,
3529

36-
/// SSZ-encoded `TypeTwoMultiSignature` envelope. Use
37-
/// [`SignedBlock::merged_proof_bytes`] to extract the raw
38-
/// lean-multisig Type-2 bytes inside, or
39-
/// [`SignedBlock::wrap_merged_proof`] when building an envelope from
40-
/// the prover output.
30+
/// Single full-block proof covering attestations and the proposer signature.
31+
pub proof: MultiMessageAggregate,
32+
}
33+
34+
// Manual Debug impl because the merged proof bytes are large and opaque.
35+
impl core::fmt::Debug for SignedBlock {
36+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37+
f.debug_struct("SignedBlock")
38+
.field("message", &self.message)
39+
.field("proof", &format_args!("<{} bytes>", self.proof.proof.len()))
40+
.finish()
41+
}
42+
}
43+
44+
/// 512 KiB byte-list cap shared by every block-level / Type-1 proof field.
45+
/// Matches leanSpec PR #717's `ByteList512KiB` SSZ container.
46+
pub type ByteList512KiB = ByteList<524_288>;
47+
48+
/// A merged proof covering multiple messages with a single proof blob.
49+
///
50+
/// The proof bytes use lean-multisig's compact public-key-free
51+
/// representation. SSZ encoding this container adds the offset required for
52+
/// its variable-length field.
53+
#[derive(Debug, Default, Clone, PartialEq, Eq, SszEncode, SszDecode, HashTreeRoot)]
54+
pub struct MultiMessageAggregate {
55+
/// Serialized multi-message aggregate proof bytes.
4156
pub proof: ByteList512KiB,
4257
}
4358

44-
impl SignedBlock {
45-
/// Strip the SSZ-container offset header to return the raw
46-
/// lean-multisig Type-2 merged proof bytes the verifier consumes.
47-
pub fn merged_proof_bytes(&self) -> Result<&[u8], ProofEnvelopeError> {
48-
let bytes = self.proof.iter().as_slice();
49-
if bytes.len() < 4 {
50-
return Err(ProofEnvelopeError::TruncatedEnvelope);
51-
}
52-
let mut header = [0u8; 4];
53-
header.copy_from_slice(&bytes[..4]);
54-
let offset = u32::from_le_bytes(header) as usize;
55-
if offset != 4 {
56-
return Err(ProofEnvelopeError::UnexpectedOffset(offset));
57-
}
58-
Ok(&bytes[4..])
59+
impl MultiMessageAggregate {
60+
/// Build an aggregate from an already bounded proof byte list.
61+
pub fn new(proof: ByteList512KiB) -> Self {
62+
Self { proof }
63+
}
64+
65+
/// Copy raw lean-multisig proof bytes into the bounded SSZ container.
66+
pub fn from_bytes(bytes: &[u8]) -> Result<Self, MultiMessageAggregateError> {
67+
let len = bytes.len();
68+
ByteList512KiB::try_from(bytes.to_vec())
69+
.map(Self::new)
70+
.map_err(|_| MultiMessageAggregateError::ProofTooLarge(len))
5971
}
6072

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

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

85-
impl core::fmt::Display for ProofEnvelopeError {
86+
impl core::fmt::Display for MultiMessageAggregateError {
8687
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
8788
match self {
88-
Self::TruncatedEnvelope => f.write_str("block proof envelope truncated"),
89-
Self::UnexpectedOffset(o) => write!(f, "block proof envelope offset {o}, expected 4"),
90-
Self::ExceedsCap(n) => write!(f, "wrapped proof {n} bytes exceeds 512 KiB cap"),
89+
Self::ProofTooLarge(n) => write!(f, "proof {n} bytes exceeds 512 KiB cap"),
9190
}
9291
}
9392
}
9493

95-
impl std::error::Error for ProofEnvelopeError {}
96-
97-
// Manual Debug impl because the merged proof bytes are large and opaque.
98-
impl core::fmt::Debug for SignedBlock {
99-
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
100-
f.debug_struct("SignedBlock")
101-
.field("message", &self.message)
102-
.field("proof", &format_args!("<{} bytes>", self.proof.len()))
103-
.finish()
104-
}
105-
}
106-
107-
/// 512 KiB byte-list cap shared by every block-level / Type-1 proof field.
108-
/// Matches leanSpec PR #717's `ByteList512KiB` SSZ container.
109-
pub type ByteList512KiB = ByteList<524_288>;
94+
impl std::error::Error for MultiMessageAggregateError {}
11095

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

126111
/// Maximum number of distinct `AttestationData` entries permitted in a single
127112
/// block. Canonical home for the cap shared across `ethlambda-blockchain`,
@@ -322,15 +307,27 @@ mod tests {
322307
};
323308
let signed = SignedBlock {
324309
message: block,
325-
proof: ByteList512KiB::default(),
310+
proof: MultiMessageAggregate::default(),
326311
};
327312
let bytes = signed.to_ssz();
328313
let decoded = SignedBlock::from_ssz_bytes(&bytes).expect("decode");
329-
assert_eq!(decoded.proof.len(), 0);
314+
assert_eq!(decoded.proof.proof.len(), 0);
330315
assert_eq!(decoded.message.slot, signed.message.slot);
331316
assert_eq!(
332317
decoded.message.proposer_index,
333318
signed.message.proposer_index
334319
);
335320
}
321+
322+
#[test]
323+
fn multi_message_aggregate_ssz_wraps_proof_bytes() {
324+
let proof_bytes: Vec<u8> = (0..64).collect();
325+
let aggregate = MultiMessageAggregate::from_bytes(&proof_bytes).unwrap();
326+
327+
let encoded = aggregate.to_ssz();
328+
329+
assert_eq!(&encoded[..4], &4u32.to_le_bytes());
330+
assert_eq!(&encoded[4..], proof_bytes);
331+
assert_eq!(aggregate.proof_bytes(), proof_bytes);
332+
}
336333
}

crates/common/types/tests/ssz_types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ impl From<SignedAttestation> for DomainSignedAttestation {
126126

127127
// NOTE: After Phase 3 the legacy `BlockSignatures` / `AttestationSignatures` /
128128
// `AggregatedSignatureProof` containers are removed from the domain, and
129-
// `SignedBlock` now carries a single `proof: ByteList512KiB` field. The pinned
129+
// `SignedBlock` now carries a single `proof: MultiMessageAggregate` field. The pinned
130130
// leanSpec fixtures still use the old shape, so SSZ-byte and root assertions
131131
// for `SignedBlock`, `BlockSignatures`, `AggregatedSignatureProof`, and
132132
// `SignedAggregatedAttestation` are intentionally skipped in

crates/net/p2p/src/req_resp/handlers.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ mod tests {
400400
use super::*;
401401
use ethlambda_storage::{ForkCheckpoints, backend::InMemoryBackend};
402402
use ethlambda_types::{
403-
block::{Block, BlockBody, ByteList512KiB},
403+
block::{Block, BlockBody, MultiMessageAggregate},
404404
state::State,
405405
};
406406
use std::sync::Arc;
@@ -414,7 +414,7 @@ mod tests {
414414
state_root: H256::ZERO,
415415
body: BlockBody::default(),
416416
},
417-
proof: ByteList512KiB::default(),
417+
proof: MultiMessageAggregate::default(),
418418
}
419419
}
420420

0 commit comments

Comments
 (0)