diff --git a/Cargo.lock b/Cargo.lock index c99f8aa1..fbcf55bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,7 +651,7 @@ dependencies = [ [[package]] name = "backend" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "mt-air", "mt-fiat-shamir", @@ -3468,6 +3468,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indenter" version = "0.3.4" @@ -3731,7 +3750,7 @@ dependencies = [ [[package]] name = "lean-multisig" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "backend", "clap", @@ -3739,16 +3758,20 @@ dependencies = [ "leansig_wrapper", "rand 0.10.0", "rec_aggregation", + "serde_json", "sub_protocols", + "system-info", "utils", + "zk-alloc", ] [[package]] name = "lean_compiler" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "backend", + "include_dir", "lean_vm", "pest", "pest_derive", @@ -3761,7 +3784,7 @@ dependencies = [ [[package]] name = "lean_prover" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "backend", "itertools 0.14.0", @@ -3770,6 +3793,7 @@ dependencies = [ "pest", "pest_derive", "rand 0.10.0", + "serde", "sub_protocols", "tracing", "utils", @@ -3778,7 +3802,7 @@ dependencies = [ [[package]] name = "lean_vm" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "backend", "itertools 0.14.0", @@ -3834,7 +3858,7 @@ dependencies = [ [[package]] name = "leansig_wrapper" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "backend", "ethereum_ssz", @@ -4875,7 +4899,7 @@ dependencies = [ [[package]] name = "mt-air" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "mt-field", "mt-poly", @@ -4884,7 +4908,7 @@ dependencies = [ [[package]] name = "mt-fiat-shamir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "mt-field", "mt-koala-bear", @@ -4892,12 +4916,13 @@ dependencies = [ "mt-utils", "rayon", "serde", + "tracing", ] [[package]] name = "mt-field" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "itertools 0.14.0", "mt-utils", @@ -4912,7 +4937,7 @@ dependencies = [ [[package]] name = "mt-koala-bear" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "itertools 0.14.0", "mt-field", @@ -4928,7 +4953,7 @@ dependencies = [ [[package]] name = "mt-poly" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "itertools 0.14.0", "mt-field", @@ -4936,12 +4961,13 @@ dependencies = [ "rand 0.10.0", "rayon", "serde", + "system-info", ] [[package]] name = "mt-sumcheck" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "mt-air", "mt-fiat-shamir", @@ -4954,7 +4980,7 @@ dependencies = [ [[package]] name = "mt-symetric" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "mt-field", "mt-koala-bear", @@ -4964,7 +4990,7 @@ dependencies = [ [[package]] name = "mt-utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "serde", ] @@ -4972,7 +4998,7 @@ dependencies = [ [[package]] name = "mt-whir" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "itertools 0.14.0", "mt-fiat-shamir", @@ -4984,6 +5010,7 @@ dependencies = [ "mt-utils", "rand 0.10.0", "rayon", + "system-info", "tracing", ] @@ -5303,6 +5330,16 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "object" version = "0.37.3" @@ -6471,14 +6508,17 @@ dependencies = [ [[package]] name = "rec_aggregation" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "backend", + "include_dir", "lean_compiler", "lean_prover", "lean_vm", "leansig_wrapper", "lz4_flex", + "objc2", + "objc2-foundation", "postcard", "rand 0.10.0", "serde", @@ -6486,6 +6526,7 @@ dependencies = [ "sub_protocols", "tracing", "utils", + "zk-alloc", ] [[package]] @@ -7503,7 +7544,7 @@ dependencies = [ [[package]] name = "sub_protocols" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "backend", "lean_vm", @@ -7594,6 +7635,15 @@ dependencies = [ "libc", ] +[[package]] +name = "system-info" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" +dependencies = [ + "libc", + "rayon", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -8138,7 +8188,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=2eb4b9d#2eb4b9d983171139af36749f127dd9890c9109e6" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" dependencies = [ "backend", "tracing", @@ -9095,6 +9145,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zk-alloc" +version = "0.1.0" +source = "git+https://github.com/leanEthereum/leanMultisig.git?rev=0242c909#0242c909260c9e16893baae3004667230429808d" +dependencies = [ + "libc", + "system-info", +] + [[package]] name = "zkhash" version = "0.2.0" diff --git a/crates/blockchain/src/aggregation.rs b/crates/blockchain/src/aggregation.rs index b48390fb..78af91ec 100644 --- a/crates/blockchain/src/aggregation.rs +++ b/crates/blockchain/src/aggregation.rs @@ -13,7 +13,7 @@ use ethlambda_crypto::aggregate_mixed; use ethlambda_storage::Store; use ethlambda_types::{ attestation::{AggregationBits, HashedAttestationData}, - block::{ByteListMiB, BytecodeClaim, TypeOneInfo, TypeOneMultiSignature}, + block::{ByteListMiB, TypeOneMultiSignature}, primitives::H256, signature::{ValidatorPublicKey, ValidatorSignature}, state::Validator, @@ -290,15 +290,7 @@ pub fn aggregate_job(job: AggregationJob) -> Option { participants.dedup(); let aggregation_bits = aggregation_bits_from_validator_indices(&participants); - let proof = TypeOneMultiSignature { - info: TypeOneInfo { - message: data_root, - slot: job.slot, - participants: aggregation_bits, - bytecode_claim: BytecodeClaim::ZERO, - }, - proof: proof_data, - }; + let proof = TypeOneMultiSignature::new(aggregation_bits, proof_data); metrics::observe_aggregated_proof_size(proof.proof.len()); Some(AggregatedGroupOutput { diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 707af364..baff62db 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -8,8 +8,12 @@ use ethlambda_types::{ ShortRoot, aggregator::AggregatorController, attestation::{SignedAggregatedAttestation, SignedAttestation}, - block::{ByteListMiB, SignedBlock, TypeOneMultiSignature, TypeTwoMultiSignature}, + block::{ + ByteListMiB, SignedBlock, TypeOneInfo, TypeOneInfos, TypeOneMultiSignature, + TypeTwoMultiSignature, + }, primitives::{H256, HashTreeRoot as _}, + signature::{ValidatorPublicKey, ValidatorSignature}, }; use libssz::SszEncode as _; @@ -335,22 +339,118 @@ impl BlockChainServer { return; }; - // Assemble SignedBlock: wrap the proposer's XMSS signature as a - // singleton Type-1 and fold every attestation Type-1 plus the - // proposer Type-1 into the block's single merged Type-2 proof. - let proposer_proof_bytes = ByteListMiB::try_from(proposer_signature.to_vec()) - .expect("XMSS signature fits in ByteListMiB"); - let proposer_t1 = TypeOneMultiSignature::for_proposer( - validator_id, - proposer_proof_bytes, - block_root, - slot, - ); + // 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. + // + // Both proofs run synchronously on the actor thread, so propose_block + // currently dominates the slot budget; see PR #370 for the off-thread + // refactor follow-up. + let head_state = self.store.head_state(); + let validators = &head_state.validators; + let Some(proposer_validator) = validators.get(validator_id as usize) else { + 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; + }; + + 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 proposer_t1 = + TypeOneMultiSignature::for_proposer(validator_id, proposer_proof_bytes.clone()); + + let mut merge_inputs: Vec<(Vec, ByteListMiB)> = + 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"); + resolve_failed = true; + break; + } + } + } + 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)); + + let merged_proof_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 mut all_proofs = type_one_proofs; all_proofs.push(proposer_t1); - let merged = TypeTwoMultiSignature::from_type_1s(all_proofs); - let proof_bytes = ByteListMiB::try_from(merged.to_ssz()) - .expect("merged Type-2 proof fits in ByteListMiB"); + let infos: Vec = all_proofs + .into_iter() + .map(|t1| TypeOneInfo { + participants: t1.info.participants, + proof: ByteListMiB::default(), + }) + .collect(); + let Ok(merged_infos) = TypeOneInfos::try_from(infos) else { + error!(%slot, %validator_id, "Too many Type-1 infos for Type-2 envelope"); + metrics::inc_block_building_failures(); + return; + }; + let merged_envelope = TypeTwoMultiSignature { + info: merged_infos, + proof: merged_proof_bytes, + }; + let Ok(proof_bytes) = ByteListMiB::try_from(merged_envelope.to_ssz()).inspect_err( + |err| error!(%slot, %validator_id, ?err, "Merged Type-2 envelope exceeds ByteListMiB"), + ) else { + metrics::inc_block_building_failures(); + return; + }; let signed_block = SignedBlock { message: block, proof: proof_bytes, diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index cfd422bd..6b1a16df 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -13,12 +13,12 @@ use ethlambda_types::{ HashedAttestationData, SignedAggregatedAttestation, SignedAttestation, validator_indices, }, block::{ - AggregatedAttestations, Block, BlockBody, ByteListMiB, BytecodeClaim, SignedBlock, - TypeOneInfo, TypeOneMultiSignature, TypeTwoMultiSignature, + AggregatedAttestations, Block, BlockBody, ByteListMiB, SignedBlock, TypeOneMultiSignature, + TypeTwoMultiSignature, }, checkpoint::Checkpoint, primitives::{H256, HashTreeRoot as _}, - signature::ValidatorSignature, + signature::{ValidatorPublicKey, ValidatorSignature}, state::State, }; use libssz::SszDecode as _; @@ -515,14 +515,12 @@ fn on_block_core( // Process block body attestations and feed them into the payload buffer // so fork choice's LMD GHOST overlay can see block-only votes. // - // Since the block carries a single merged Type-2 proof, we cannot recover - // per-attestation proof bytes here. The entries we insert are info-only - // (`TypeOneInfo` from the merged proof's `info` list, with empty `proof` - // bytes). Real per-attestation proof bytes still arrive via gossip - // (`SignedAggregatedAttestation`) and verify there; this insertion is - // purely for fork-choice vote bookkeeping. Compact aggregation paths - // (`compact_attestations` → `aggregate_proofs`) only run when there are - // multiple proofs per attestation data, so info-only entries are safe. + // The merged Type-2 envelope carries info-only (participants) entries + // for each component to keep the on-wire envelope under the 1 MiB cap. + // Standalone Type-1 proof bytes are not recoverable from a block; + // downstream re-aggregation has to come from the gossip channel or be + // SNARK-split with `split_type_2_by_message`. Entries we insert here + // are info-only, used only for fork-choice vote bookkeeping. let aggregated_attestations = &block.body.attestations; let merged = TypeTwoMultiSignature::from_ssz_bytes(signed_block.proof.iter().as_slice()) .map_err(|_| StoreError::ProposerSignatureDecodingFailed)?; @@ -796,6 +794,9 @@ pub enum StoreError { #[error("Proposer signature verification failed")] ProposerSignatureVerificationFailed, + #[error("Block slot {0} exceeds u32 range")] + SlotOutOfRange(u64), + #[error("State transition failed: {0}")] StateTransitionFailed(#[from] ethlambda_state_transition::Error), @@ -981,15 +982,7 @@ fn compact_attestations( let merged_proof_data = aggregate_proofs(children, &data_root, slot) .map_err(StoreError::SignatureAggregationFailed)?; - let merged_proof = TypeOneMultiSignature { - info: TypeOneInfo { - message: data_root, - slot: data.slot, - participants: merged_bits.clone(), - bytecode_claim: BytecodeClaim::ZERO, - }, - proof: merged_proof_data, - }; + let merged_proof = TypeOneMultiSignature::new(merged_bits.clone(), merged_proof_data); let merged_att = AggregatedAttestation { aggregation_bits: merged_bits, data, @@ -1007,9 +1000,9 @@ fn compact_attestations( /// one previously-uncovered validator; partially-overlapping participants /// between selected proofs are allowed. `compact_attestations` later feeds /// these proofs as children to `aggregate_proofs`, which delegates to -/// `xmss_aggregate` — that function tracks duplicate pubkeys across -/// children via its `dup_pub_keys` machinery, so overlap is supported by -/// the underlying aggregation scheme. +/// lean-multisig devnet5 `aggregate_type_1` — that function tracks duplicate +/// pubkeys across children via its `dup_pub_keys` machinery, so overlap is +/// supported by the underlying aggregation scheme. /// /// Each selected proof is appended to `selected` paired with its /// corresponding AggregatedAttestation. @@ -1220,16 +1213,13 @@ fn build_block( Ok((final_block, aggregated_signatures, post_checkpoints)) } -/// Structural verification of a signed block's merged Type-2 proof. +/// Full verification of a signed block's merged Type-2 proof. /// -/// Phase 3 of the Type-1 / Type-2 aggregation migration replaces the per- -/// attestation `verify_aggregated_signature` plus standalone proposer-signature -/// check with a structural alignment check on the merged Type-2 blob: the -/// `info` list must hold one entry per block-body attestation plus one -/// trailing entry for the proposer. Cryptographic verification of each Type-1 -/// still happens at gossip ingestion (`on_gossip_aggregated_attestation`); the -/// block-level crypto path returns once `lean_multisig` exposes a real -/// merged-proof verification primitive. +/// Structural pre-checks (fast fail) ensure the merged proof's `info` list lines +/// up with the block body (one entry per attestation plus a trailing proposer +/// entry; messages, slots, and participants match what the body declares). +/// On success, the lean-multisig devnet5 `verify_type_2` primitive runs the +/// SNARK verifier over the merged proof bytes against the resolved pubkey set. /// /// Exposed publicly so RPC handlers (notably the Hive test-driver /// `verify_signatures/run` endpoint) can run the exact same verification path @@ -1258,21 +1248,14 @@ pub fn verify_block_signatures( let validators = &state.validators; let num_validators = validators.len() as u64; - // Per-attestation entries: messages, slots, and participants must mirror - // the block body. The crypto binding for each is already checked at gossip. + // Per-attestation entries: participant bitfields must mirror the block + // body. The signed message and slot live on the body, not on the proof + // envelope (leanSpec PR #717), so they're rederived below for the crypto + // binding check rather than cross-checked here. for (attestation, info) in attestations.iter().zip(merged.info.iter()) { if attestation.aggregation_bits != info.participants { return Err(StoreError::ParticipantsMismatch); } - if info.slot != attestation.data.slot { - return Err(StoreError::AttestationSignatureMismatch { - signatures: merged.info.len(), - attestations: attestations.len(), - }); - } - if info.message != attestation.data.hash_tree_root() { - return Err(StoreError::ParticipantsMismatch); - } for vid in validator_indices(&attestation.aggregation_bits) { if vid >= num_validators { return Err(StoreError::InvalidValidatorIndex); @@ -1280,13 +1263,9 @@ pub fn verify_block_signatures( } } - // Trailing proposer entry: single bit for `block.proposer_index`, - // message equals the block root, slot matches the block slot. + // Trailing proposer entry: single bit for `block.proposer_index`. let proposer_info = &merged.info[attestations.len()]; let block_root = block.hash_tree_root(); - if proposer_info.message != block_root || proposer_info.slot != block.slot { - return Err(StoreError::ProposerSignatureVerificationFailed); - } let proposer_bits: Vec = validator_indices(&proposer_info.participants).collect(); if proposer_bits != [block.proposer_index] { return Err(StoreError::ProposerSignatureVerificationFailed); @@ -1295,12 +1274,61 @@ pub fn verify_block_signatures( return Err(StoreError::InvalidValidatorIndex); } + let structural_elapsed = total_start.elapsed(); + + // Resolve pubkeys per Type-2 component for verify_type_2 and rederive the + // expected (message, slot) bindings from the block body. Attestation + // components use each participant's attestation_pubkey; the trailing + // proposer component uses the proposal_pubkey of `block.proposer_index`. + let mut pubkeys_per_component: Vec> = + Vec::with_capacity(merged.info.len()); + let mut expected_bindings: Vec<(H256, u32)> = Vec::with_capacity(merged.info.len()); + + for attestation in attestations.iter() { + let mut pubkeys = Vec::new(); + for vid in validator_indices(&attestation.aggregation_bits) { + let validator = validators + .get(vid as usize) + .ok_or(StoreError::InvalidValidatorIndex)?; + let pk = validator + .get_attestation_pubkey() + .map_err(|_| StoreError::PubkeyDecodingFailed(vid))?; + pubkeys.push(pk); + } + pubkeys_per_component.push(pubkeys); + let slot_u32 = u32::try_from(attestation.data.slot) + .map_err(|_| StoreError::SlotOutOfRange(attestation.data.slot))?; + expected_bindings.push((attestation.data.hash_tree_root(), slot_u32)); + } + + let proposer_validator = validators + .get(block.proposer_index as usize) + .ok_or(StoreError::InvalidValidatorIndex)?; + let proposer_pubkey = proposer_validator + .get_proposal_pubkey() + .map_err(|_| StoreError::PubkeyDecodingFailed(block.proposer_index))?; + pubkeys_per_component.push(vec![proposer_pubkey]); + let block_slot_u32 = + u32::try_from(block.slot).map_err(|_| StoreError::SlotOutOfRange(block.slot))?; + expected_bindings.push((block_root, block_slot_u32)); + + let crypto_start = std::time::Instant::now(); + ethlambda_crypto::verify_type_2_signature( + &merged.proof, + pubkeys_per_component, + &expected_bindings, + ) + .map_err(StoreError::AggregateVerificationFailed)?; + let crypto_elapsed = crypto_start.elapsed(); + let total_elapsed = total_start.elapsed(); info!( slot = block.slot, attestation_count = attestations.len(), + ?structural_elapsed, + ?crypto_elapsed, ?total_elapsed, - "Block proof structural check" + "Block Type-2 proof verified" ); Ok(()) @@ -1366,20 +1394,15 @@ mod tests { use libssz::SszEncode as _; /// Test helper: wrap a list of Type-1 attestation proofs plus a stub - /// proposer Type-1 into the SSZ-encoded merged Type-2 blob that the - /// post-Phase-3 `SignedBlock.proof` carries. + /// proposer Type-1 into the SSZ-encoded merged Type-2 blob. fn make_signed_block_proof( proposer_index: u64, - block_root: H256, - slot: u64, attestation_proofs: Vec, ) -> ByteListMiB { let mut all = attestation_proofs; all.push(TypeOneMultiSignature::for_proposer( proposer_index, ByteListMiB::default(), - block_root, - slot, )); let merged = TypeTwoMultiSignature::from_type_1s(all); ByteListMiB::try_from(merged.to_ssz()).expect("merged proof fits in ByteListMiB") @@ -1430,12 +1453,9 @@ mod tests { }; let block_root = block.hash_tree_root(); - let mismatching_t1 = TypeOneMultiSignature::empty( - proof_bits, - attestation_data.hash_tree_root(), - attestation_data.slot, - ); - let proof = make_signed_block_proof(0, block_root, 0, vec![mismatching_t1]); + let mismatching_t1 = TypeOneMultiSignature::empty(proof_bits); + let _ = block_root; // proof envelope no longer carries the block root + let proof = make_signed_block_proof(0, vec![mismatching_t1]); let signed_block = SignedBlock { message: block, @@ -1466,7 +1486,12 @@ mod tests { use libssz_types::SszList; const MAX_PAYLOAD_SIZE: usize = 10 * 1024 * 1024; // 10 MiB (spec limit) - const PROOF_SIZE: usize = 253 * 1024; // ~253 KB realistic XMSS proof + // The Type-2 envelope now embeds each per-component Type-1 proof in + // info[i].proof (leanSpec PR #717), so the per-component size has to + // budget for `MAX_ATTESTATIONS_DATA + 1` copies fitting in the 1 MiB + // ByteListMiB cap. 50 KiB per component is roughly what a real + // lean-multisig devnet5 Type-1 SNARK weighs in at. + const PROOF_SIZE: usize = 50 * 1024; const NUM_VALIDATORS: usize = 50; const NUM_PAYLOAD_ENTRIES: usize = 50; @@ -1560,7 +1585,7 @@ mod tests { let proof_bytes: Vec = vec![0xAB; PROOF_SIZE]; let proof_data = SszList::try_from(proof_bytes).expect("proof fits in ByteListMiB"); - let proof = TypeOneMultiSignature::new(bits, data_root, att_data.slot, proof_data); + let proof = TypeOneMultiSignature::new(bits, proof_data); aggregated_payloads.insert(data_root, (att_data, vec![proof])); } @@ -1585,8 +1610,7 @@ mod tests { ); // Build the merged Type-2 proof exactly as `propose_block` would. - let block_root = block.hash_tree_root(); - let proof = make_signed_block_proof(proposer_index, block_root, block.slot, signatures); + let proof = make_signed_block_proof(proposer_index, signatures); let signed_block = SignedBlock { message: block, proof, @@ -1697,7 +1721,7 @@ mod tests { bits.set(i, true).unwrap(); } let proof_data = SszList::try_from(vec![0xAB; 64]).unwrap(); - let proof = TypeOneMultiSignature::new(bits, data_root, att_data.slot, proof_data); + let proof = TypeOneMultiSignature::new(bits, proof_data); let mut aggregated_payloads = HashMap::new(); aggregated_payloads.insert(data_root, (att_data.clone(), vec![proof])); @@ -1753,10 +1777,10 @@ mod tests { } /// Test helper: empty Type-1 proof carrying the given participants and slot - /// metadata. The message and bytecode_claim are zeroed — only the participant - /// bitfield matters for the pipeline tests below. - fn make_type_one_proof(bits: AggregationBits, slot: u64) -> TypeOneMultiSignature { - TypeOneMultiSignature::empty(bits, H256::ZERO, slot) + /// metadata. Only the participant bitfield matters for the pipeline tests + /// below; the proof envelope no longer carries a slot or message. + fn make_type_one_proof(bits: AggregationBits, _slot: u64) -> TypeOneMultiSignature { + TypeOneMultiSignature::empty(bits) } #[test] @@ -1886,13 +1910,12 @@ mod tests { }; let block_root = block.hash_tree_root(); let att_root = att_data.hash_tree_root(); + let _ = (block_root, att_root); // unused under the slim wire format let proof = make_signed_block_proof( 0, - block_root, - block.slot, vec![ - TypeOneMultiSignature::empty(bits_a, att_root, att_data.slot), - TypeOneMultiSignature::empty(bits_b, att_root, att_data.slot), + TypeOneMultiSignature::empty(bits_a), + TypeOneMultiSignature::empty(bits_b), ], ); let signed_block = SignedBlock { @@ -1917,7 +1940,7 @@ mod tests { /// least one previously-uncovered validator. The greedy prefers the /// largest proof first, then picks additional proofs whose coverage /// extends `covered`. The resulting overlap is handled downstream by - /// `aggregate_proofs` → `xmss_aggregate` (which tracks duplicate pubkeys + /// `aggregate_proofs` → `aggregate_type_1` (which tracks duplicate pubkeys /// across children via its `dup_pub_keys` machinery). #[test] fn extend_proofs_greedily_allows_overlap_when_it_adds_coverage() { diff --git a/crates/blockchain/tests/forkchoice_spectests.rs b/crates/blockchain/tests/forkchoice_spectests.rs index 4e63a3dc..fc38b532 100644 --- a/crates/blockchain/tests/forkchoice_spectests.rs +++ b/crates/blockchain/tests/forkchoice_spectests.rs @@ -146,12 +146,8 @@ fn run(path: &Path) -> datatest_stable::Result<()> { let proof_data = ByteList::try_from(proof_bytes) .expect("aggregated proof data fits in ByteListMiB"); let data: AttestationData = att_data.data.into(); - let proof = TypeOneMultiSignature::new( - proof_fixture.participants.into(), - data.hash_tree_root(), - data.slot, - proof_data, - ); + let proof = + TypeOneMultiSignature::new(proof_fixture.participants.into(), proof_data); let aggregated = SignedAggregatedAttestation { data, proof }; let result = store::on_gossip_aggregated_attestation(&mut store, aggregated); diff --git a/crates/blockchain/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs index ac4867c4..a11a58cc 100644 --- a/crates/blockchain/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -15,16 +15,9 @@ const SUPPORTED_FIXTURE_FORMAT: &str = "verify_signatures_test"; /// Tests that require cryptographic signature verification at block level. /// -/// Phase 3 of the Type-1 / Type-2 aggregation migration replaces the per- -/// attestation `verify_aggregated_signature` plus standalone proposer-signature -/// verification with a structural check on the merged Type-2 proof; the real -/// safety net is gossip-time per-attestation verification. Tests that only -/// fail on the *crypto* leg accordingly pass when run against the structural -/// stub, so they are skipped pending the `lean_multisig`-backed real -/// `verify_type_2` primitive. -/// -/// TODO(type1-type2): re-enable once block-level crypto verification returns. -const SKIP_TESTS: &[&str] = &["test_invalid_proposer_signature"]; +/// Block-level crypto verification is now wired through lean-multisig devnet5's +/// `verify_type_2`, so every fixture is exercised against the real primitive. +const SKIP_TESTS: &[&str] = &[]; fn run(path: &Path) -> datatest_stable::Result<()> { let tests = VerifySignaturesTestVector::from_file(path)?; diff --git a/crates/common/crypto/Cargo.toml b/crates/common/crypto/Cargo.toml index 4002997d..dc5ba718 100644 --- a/crates/common/crypto/Cargo.toml +++ b/crates/common/crypto/Cargo.toml @@ -12,9 +12,9 @@ version.workspace = true [dependencies] ethlambda-types.workspace = true -lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "2eb4b9d" } +lean-multisig = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "0242c909" } # leansig_wrapper provides XmssPublicKey/XmssSignature types used by lean-multisig's public API -leansig_wrapper = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "2eb4b9d" } +leansig_wrapper = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "0242c909" } leansig.workspace = true thiserror.workspace = true diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 006ef2b0..260a20c0 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -6,12 +6,18 @@ use ethlambda_types::{ signature::{ValidatorPublicKey, ValidatorSignature}, }; use lean_multisig::{ - AggregatedXMSS, ProofError, setup_prover, setup_verifier, xmss_aggregate, - xmss_verify_aggregation, + ProofError, TypeOneMultiSignature as LMType1, TypeTwoMultiSignature as LMType2, + aggregate_type_1, merge_many_type_1, setup_prover, setup_verifier, split_type_2, verify_type_1, + verify_type_2, }; use leansig_wrapper::{XmssPublicKey as LeanSigPubKey, XmssSignature as LeanSigSignature}; use thiserror::Error; +/// log(1/rate) for the WHIR commitment scheme used inside lean-multisig. +/// 2 matches the devnet-4 cross-client convention (zeam, ream, grandine, lantern +/// all use 2); the leanMultisig devnet5 examples also use 2 for recursion. +const LOG_INV_RATE: usize = 2; + // Lazy initialization for prover and verifier setup static PROVER_INIT: Once = Once::new(); static VERIFIER_INIT: Once = Once::new(); @@ -41,8 +47,26 @@ pub enum AggregationError { #[error("child proof deserialization failed at index {0}")] ChildDeserializationFailed(usize), + #[error("outer proof deserialization failed")] + DeserializationFailed, + #[error("need at least 2 children for recursive aggregation, got {0}")] InsufficientChildren(usize), + + #[error("component count ({components}) does not match pubkey-set count ({pubkey_sets})")] + ComponentPubkeyMismatch { + components: usize, + pubkey_sets: usize, + }, + + #[error("split-by-message target not found in type-2 components")] + UnknownMessage, + + #[error("split-by-message target matched multiple components")] + MultipleMessages, + + #[error("prover failure: {0}")] + ProverFailure(String), } /// Error type for signature verification operations. @@ -56,25 +80,74 @@ pub enum VerificationError { #[error("verification failed: {0}")] ProofError(#[from] ProofError), + + #[error( + "(message, slot) mismatch: proof binds {got_slot}/{got_msg:?}, expected {expected_slot}/{expected_msg:?}" + )] + BindingMismatch { + expected_msg: H256, + expected_slot: u32, + got_msg: H256, + got_slot: u32, + }, + + #[error("component count ({components}) does not match pubkey-set count ({pubkey_sets})")] + ComponentPubkeyMismatch { + components: usize, + pubkey_sets: usize, + }, + + #[error("type-2 binds {got} components but {expected} were expected")] + Type2ComponentCountMismatch { expected: usize, got: usize }, } -/// Aggregate multiple XMSS signatures into a single proof. -/// -/// This function takes a set of public keys and their corresponding signatures, -/// all signing the same message at the same slot, and produces a single -/// aggregated proof that can be verified more efficiently than checking -/// each signature individually. -/// -/// # Arguments +// ===================================================================== +// Helpers +// ===================================================================== + +fn into_lean_pubkeys(pubkeys: Vec) -> Vec { + pubkeys + .into_iter() + .map(ValidatorPublicKey::into_inner) + .collect() +} + +/// Decompress a stored Type-1 proof (without-pubkeys form) into a native +/// `TypeOneMultiSignature` by attaching the resolved validator pubkeys. +fn decompress_type1( + pubkeys: Vec, + proof_bytes: &ByteListMiB, + index: usize, +) -> Result { + let lean_pks = into_lean_pubkeys(pubkeys); + LMType1::decompress_without_pubkeys(proof_bytes.iter().as_slice(), lean_pks) + .ok_or(AggregationError::ChildDeserializationFailed(index)) +} + +fn compress_type1_to_byte_list(sig: &LMType1) -> Result { + let serialized = sig.compress_without_pubkeys(); + let len = serialized.len(); + ByteListMiB::try_from(serialized).map_err(|_| AggregationError::ProofTooBig(len)) +} + +fn compress_type2_to_byte_list(sig: &LMType2) -> Result { + let serialized = sig.compress_without_pubkeys(); + let len = serialized.len(); + ByteListMiB::try_from(serialized).map_err(|_| AggregationError::ProofTooBig(len)) +} + +// ===================================================================== +// Type-1 aggregation (single message, single slot) +// ===================================================================== + +/// Aggregate multiple XMSS signatures into a single Type-1 proof. /// -/// * `public_keys` - The public keys of the validators who signed -/// * `signatures` - The signatures from each validator (must match public_keys order) -/// * `message` - The 32-byte message that was signed -/// * `slot` - The slot in which the signatures were created +/// Equivalent to `aggregate_type_1([], raw_xmss, ...)` in lean-multisig. /// -/// # Returns +/// All signatures must bind to the same `(message, slot)` pair. /// -/// The serialized aggregated proof as `ByteListMiB`, or an error if aggregation fails. +/// Returns the lean-multisig `TypeOneMultiSignature::compress_without_pubkeys()` +/// bytes, packed as `ByteListMiB` for the on-wire SSZ proof field. pub fn aggregate_signatures( public_keys: Vec, signatures: Vec, @@ -87,8 +160,6 @@ pub fn aggregate_signatures( signatures.len(), )); } - - // Handle empty input if public_keys.is_empty() { return Err(AggregationError::EmptyInput); } @@ -101,28 +172,19 @@ pub fn aggregate_signatures( .map(|(pk, sig)| (pk.into_inner(), sig.into_inner())) .collect(); - // log_inv_rate=2 matches the devnet-4 cross-client convention (zeam, ream, - // grandine, lantern's c-leanvm-xmss all use 2). Ethlambda previously - // hardcoded 1, which produced proofs incompatible with every other client. - let (_sorted_pubkeys, aggregate) = xmss_aggregate(&[], raw_xmss, &message.0, slot, 2); + let proof = aggregate_type_1(&[], raw_xmss, message.0, slot, LOG_INV_RATE) + .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; - serialize_aggregate(aggregate) + compress_type1_to_byte_list(&proof) } -/// Aggregate both existing proofs (children) and raw XMSS signatures in a single call. +/// Aggregate both existing Type-1 proofs (children) and raw XMSS signatures. /// -/// This is the spec's gossip-time mixed aggregation: existing proofs from previous -/// rounds are fed as children, and only genuinely new signatures go as `raw_xmss`. -/// This avoids re-aggregating from scratch each round and keeps proof trees shallow. +/// Existing Type-1s are reused as recursive children; raw XMSS are mixed in. +/// All inputs must bind to the same `(message, slot)`. /// -/// Requires at least one raw signature OR at least 2 children. A lone child proof -/// is already valid and needs no further aggregation. -/// -/// # Panics -/// -/// Panics if any deserialized child proof is cryptographically invalid (e.g., was -/// produced for a different message or slot). This is an upstream constraint of -/// `xmss_aggregate`. +/// Requires at least one raw signature OR at least 2 children. A lone child is +/// already a valid Type-1; further aggregation is wasted work. pub fn aggregate_mixed( children: Vec<(Vec, ByteListMiB)>, raw_public_keys: Vec, @@ -136,22 +198,17 @@ pub fn aggregate_mixed( raw_signatures.len(), )); } - - // Need at least one raw signature OR at least 2 children to merge. if raw_public_keys.is_empty() && children.len() < 2 { return Err(AggregationError::InsufficientChildren(children.len())); } ensure_prover_ready(); - // Split deserialized children into parallel Vecs so we can borrow pubkey - // slices (required by xmss_aggregate's tuple type) while moving the large - // AggregatedXMSS values into the children list without cloning. `pks_list` - // must outlive `children_refs`. - let (pks_list, aggs): (Vec>, Vec) = - deserialize_children(children)?.into_iter().unzip(); - let children_refs: Vec<(&[LeanSigPubKey], AggregatedXMSS)> = - pks_list.iter().map(Vec::as_slice).zip(aggs).collect(); + let children_native: Vec = children + .into_iter() + .enumerate() + .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) + .collect::>()?; let raw_xmss: Vec<(LeanSigPubKey, LeanSigSignature)> = raw_public_keys .into_iter() @@ -159,20 +216,16 @@ pub fn aggregate_mixed( .map(|(pk, sig)| (pk.into_inner(), sig.into_inner())) .collect(); - let (_sorted_pubkeys, aggregate) = - xmss_aggregate(&children_refs, raw_xmss, &message.0, slot, 2); + let proof = aggregate_type_1(&children_native, raw_xmss, message.0, slot, LOG_INV_RATE) + .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; - serialize_aggregate(aggregate) + compress_type1_to_byte_list(&proof) } -/// Recursively aggregate multiple already-aggregated proofs into one. -/// -/// Each child is a `(public_keys, proof_data)` pair where `public_keys` are the -/// attestation public keys of the validators covered by that child proof, and -/// `proof_data` is the serialized `AggregatedXMSS`. At least 2 children are required. +/// Recursively aggregate two or more already-aggregated Type-1 proofs into one. /// -/// This is used during block building to compact multiple proofs sharing the same -/// `AttestationData` into a single merged proof (leanSpec PR #510). +/// All children must bind to the same `(message, slot)`. Used during block +/// building to compact multiple proofs sharing an `AttestationData`. pub fn aggregate_proofs( children: Vec<(Vec, ByteListMiB)>, message: &H256, @@ -184,57 +237,24 @@ pub fn aggregate_proofs( ensure_prover_ready(); - // See `aggregate_mixed` for why this unzip-and-rezip dance is needed. - let (pks_list, aggs): (Vec>, Vec) = - deserialize_children(children)?.into_iter().unzip(); - let children_refs: Vec<(&[LeanSigPubKey], AggregatedXMSS)> = - pks_list.iter().map(Vec::as_slice).zip(aggs).collect(); - - let (_sorted_pubkeys, aggregate) = xmss_aggregate(&children_refs, vec![], &message.0, slot, 2); - - serialize_aggregate(aggregate) -} - -/// Deserialize child proofs from `(public_keys, proof_bytes)` pairs into -/// lean-multisig types. -fn deserialize_children( - children: Vec<(Vec, ByteListMiB)>, -) -> Result, AggregatedXMSS)>, AggregationError> { - children + let children_native: Vec = children .into_iter() .enumerate() - .map(|(i, (pubkeys, proof_data))| { - let lean_pks: Vec = - pubkeys.into_iter().map(|pk| pk.into_inner()).collect(); - let aggregate = AggregatedXMSS::deserialize(proof_data.iter().as_slice()) - .ok_or(AggregationError::ChildDeserializationFailed(i))?; - Ok((lean_pks, aggregate)) - }) - .collect() -} + .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) + .collect::>()?; + + let proof = aggregate_type_1(&children_native, vec![], message.0, slot, LOG_INV_RATE) + .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; -/// Serialize an `AggregatedXMSS` into the `ByteListMiB` wire format. -fn serialize_aggregate(aggregate: AggregatedXMSS) -> Result { - let serialized = aggregate.serialize(); - let serialized_len = serialized.len(); - ByteListMiB::try_from(serialized).map_err(|_| AggregationError::ProofTooBig(serialized_len)) + compress_type1_to_byte_list(&proof) } -/// Verify an aggregated signature proof. -/// -/// This function verifies that a set of validators (identified by their public keys) -/// all signed the same message at the same slot. +/// Verify a Type-1 aggregated signature proof. /// -/// # Arguments +/// Cryptographically verifies that every `public_key` signed `message` at `slot`. /// -/// * `proof_data` - The serialized aggregated proof -/// * `public_keys` - The public keys of the validators who allegedly signed -/// * `message` - The 32-byte message that was allegedly signed -/// * `slot` - The slot in which the signatures were allegedly created -/// -/// # Returns -/// -/// `Ok(())` if verification succeeds, or an error describing why it failed. +/// The verifier checks the bound `(message, slot)` matches what the caller +/// expects, defending against proofs reused from other binding contexts. pub fn verify_aggregated_signature( proof_data: &ByteListMiB, public_keys: Vec, @@ -243,30 +263,162 @@ pub fn verify_aggregated_signature( ) -> Result<(), VerificationError> { ensure_verifier_ready(); - // Convert public keys - let lean_pubkeys: Vec = public_keys + let lean_pubkeys = into_lean_pubkeys(public_keys); + let sig = LMType1::decompress_without_pubkeys(proof_data.iter().as_slice(), lean_pubkeys) + .ok_or(VerificationError::DeserializationFailed)?; + + if sig.info.without_pubkeys.message != message.0 || sig.info.without_pubkeys.slot != slot { + return Err(VerificationError::BindingMismatch { + expected_msg: *message, + expected_slot: slot, + got_msg: H256(sig.info.without_pubkeys.message), + got_slot: sig.info.without_pubkeys.slot, + }); + } + + verify_type_1(&sig)?; + Ok(()) +} + +// ===================================================================== +// Type-2 merge / verify / split (block-level merged proofs) +// ===================================================================== + +/// Merge many independent Type-1 multi-signatures into a single Type-2 proof. +/// +/// Each input is `(participant_pubkeys, type_1_proof_bytes)` where the bytes +/// are the `compress_without_pubkeys()` form of a `TypeOneMultiSignature`. +/// +/// The returned blob is the `compress_without_pubkeys()` form of the resulting +/// `TypeTwoMultiSignature`. A verifier decoding it back needs the per-component +/// pubkey sets in the same order. +pub fn merge_type_1s_into_type_2( + type_1s: Vec<(Vec, ByteListMiB)>, +) -> Result { + if type_1s.is_empty() { + return Err(AggregationError::EmptyInput); + } + + ensure_prover_ready(); + + let type_1s_native: Vec = type_1s .into_iter() - .map(ValidatorPublicKey::into_inner) + .enumerate() + .map(|(i, (pubkeys, proof_bytes))| decompress_type1(pubkeys, &proof_bytes, i)) + .collect::>()?; + + let merged = merge_many_type_1(type_1s_native, LOG_INV_RATE) + .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; + + compress_type2_to_byte_list(&merged) +} + +/// Verify a Type-2 merged proof against the per-component expected bindings. +/// +/// The verifier re-derives each component's `(message, slot, pubkeys)` from the +/// caller-supplied lists, checks they match what the proof binds, and then runs +/// the inner SNARK verifier. +pub fn verify_type_2_signature( + proof_data: &ByteListMiB, + pubkeys_per_component: Vec>, + expected_bindings: &[(H256, u32)], +) -> Result<(), VerificationError> { + if expected_bindings.len() != pubkeys_per_component.len() { + return Err(VerificationError::ComponentPubkeyMismatch { + components: expected_bindings.len(), + pubkey_sets: pubkeys_per_component.len(), + }); + } + + ensure_verifier_ready(); + + let pubkeys_per_info: Vec> = pubkeys_per_component + .into_iter() + .map(into_lean_pubkeys) .collect(); - // Deserialize the aggregate proof - let aggregate = AggregatedXMSS::deserialize(proof_data.iter().as_slice()) + let sig = LMType2::decompress_without_pubkeys(proof_data.iter().as_slice(), pubkeys_per_info) .ok_or(VerificationError::DeserializationFailed)?; - // Verify using lean-multisig - xmss_verify_aggregation(lean_pubkeys, &aggregate, &message.0, slot)?; + if sig.info.len() != expected_bindings.len() { + return Err(VerificationError::Type2ComponentCountMismatch { + expected: expected_bindings.len(), + got: sig.info.len(), + }); + } + for (idx, ((expected_msg, expected_slot), info)) in + expected_bindings.iter().zip(sig.info.iter()).enumerate() + { + if info.without_pubkeys.message != expected_msg.0 + || info.without_pubkeys.slot != *expected_slot + { + return Err(VerificationError::BindingMismatch { + expected_msg: *expected_msg, + expected_slot: *expected_slot, + got_msg: H256(info.without_pubkeys.message), + got_slot: info.without_pubkeys.slot, + }); + } + let _ = idx; // index reserved for richer diagnostics if needed + } + + verify_type_2(&sig)?; Ok(()) } +/// Split (disaggregate) a Type-2 merged proof into a single Type-1 proof for +/// the component bound to `message`. Generates a fresh SNARK; expensive. +/// +/// Mirrors leanSpec PR #717 `TypeTwoMultiSignature.split_by_msg`: the caller +/// supplies the expected message (an attestation data root or the block +/// root) and the wrapper locates the unique matching component inside the +/// decompressed proof. Returns the `compress_without_pubkeys()` form of the +/// resulting Type-1. +pub fn split_type_2_by_message( + proof_data: &ByteListMiB, + pubkeys_per_component: Vec>, + message: &H256, +) -> Result { + ensure_prover_ready(); + + let pubkeys_per_info: Vec> = pubkeys_per_component + .into_iter() + .map(into_lean_pubkeys) + .collect(); + + let type_2 = + LMType2::decompress_without_pubkeys(proof_data.iter().as_slice(), pubkeys_per_info) + .ok_or(AggregationError::DeserializationFailed)?; + + let matches: Vec = type_2 + .info + .iter() + .enumerate() + .filter_map(|(i, info)| (info.without_pubkeys.message == message.0).then_some(i)) + .collect(); + let index = match matches.as_slice() { + [i] => *i, + [] => return Err(AggregationError::UnknownMessage), + _ => return Err(AggregationError::MultipleMessages), + }; + + let component = split_type_2(type_2, index, LOG_INV_RATE) + .map_err(|err| AggregationError::ProverFailure(err.to_string()))?; + + compress_type1_to_byte_list(&component) +} + #[cfg(test)] mod tests { use super::*; use leansig::{serialization::Serializable, signature::SignatureScheme}; use rand::{SeedableRng, rngs::StdRng}; - // The signature scheme type used in ethlambda-types - type LeanSignatureScheme = leansig::signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8; + // The signature scheme type used in ethlambda-types (Dim46 to match + // production validator keys; lean-multisig's `aggregate_type_1` hard-codes + // `SIG_SIZE_FE = 7 + (V + LOG_LIFETIME) * 8 = 631` for V=46). + type LeanSignatureScheme = leansig::signature::generalized_xmss::instantiations_aborting::lifetime_2_to_the_32::SchemeAbortingTargetSumLifetime32Dim46Base8; /// Generate a test keypair and sign a message. /// @@ -408,4 +560,43 @@ mod tests { "Verification should have failed with wrong slot" ); } + + /// End-to-end Type-2 round-trip: produce two Type-1s (different (msg, slot)), + /// merge them into a Type-2, verify the Type-2, then split out one component + /// and verify it as a Type-1. + #[test] + #[ignore = "too slow"] + fn test_type_2_merge_verify_split_round_trip() { + let msg_a = H256::from([0x11u8; 32]); + let msg_b = H256::from([0x22u8; 32]); + let slot_a: u32 = 7; + let slot_b: u32 = 11; + + let (pk_a, sig_a) = generate_keypair_and_sign(101, 5, slot_a, &msg_a); + let (pk_b, sig_b) = generate_keypair_and_sign(102, 5, slot_b, &msg_b); + + let pa = aggregate_signatures(vec![pk_a.clone()], vec![sig_a], &msg_a, slot_a).unwrap(); + let pb = aggregate_signatures(vec![pk_b.clone()], vec![sig_b], &msg_b, slot_b).unwrap(); + + let merged = + merge_type_1s_into_type_2(vec![(vec![pk_a.clone()], pa), (vec![pk_b.clone()], pb)]) + .expect("merge"); + + verify_type_2_signature( + &merged, + vec![vec![pk_a.clone()], vec![pk_b.clone()]], + &[(msg_a, slot_a), (msg_b, slot_b)], + ) + .expect("verify type-2"); + + let split = split_type_2_by_message( + &merged, + vec![vec![pk_a.clone()], vec![pk_b.clone()]], + &msg_a, + ) + .expect("split"); + + verify_aggregated_signature(&split, vec![pk_a.clone()], &msg_a, slot_a) + .expect("verify split"); + } } diff --git a/crates/common/test-fixtures/src/fork_choice.rs b/crates/common/test-fixtures/src/fork_choice.rs index 75cd810e..2351a0da 100644 --- a/crates/common/test-fixtures/src/fork_choice.rs +++ b/crates/common/test-fixtures/src/fork_choice.rs @@ -11,7 +11,7 @@ use ethlambda_types::attestation::XmssSignature; use ethlambda_types::block::{ ByteListMiB, MAX_ATTESTATIONS_DATA, SignedBlock, TypeOneMultiSignature, TypeTwoMultiSignature, }; -use ethlambda_types::primitives::{H256, HashTreeRoot as _}; +use ethlambda_types::primitives::H256; use libssz::SszEncode as _; use serde::{Deserialize, Deserializer}; use std::collections::HashMap; @@ -168,7 +168,6 @@ impl BlockStepData { /// those scenarios. pub fn to_blank_signed_block(&self) -> SignedBlock { let block = self.to_block(); - let block_root = block.hash_tree_root(); let proof = if block.body.attestations.len() > MAX_ATTESTATIONS_DATA { ByteListMiB::default() } else { @@ -176,20 +175,12 @@ impl BlockStepData { .body .attestations .iter() - .map(|att| { - TypeOneMultiSignature::empty( - att.aggregation_bits.clone(), - att.data.hash_tree_root(), - att.data.slot, - ) - }) + .map(|att| TypeOneMultiSignature::empty(att.aggregation_bits.clone())) .collect(); let mut all = attestation_proofs; all.push(TypeOneMultiSignature::for_proposer( block.proposer_index, ByteListMiB::default(), - block_root, - block.slot, )); let merged = TypeTwoMultiSignature::from_type_1s(all); ByteListMiB::try_from(merged.to_ssz()).expect("merged proof fits in ByteListMiB") diff --git a/crates/common/test-fixtures/src/verify_signatures.rs b/crates/common/test-fixtures/src/verify_signatures.rs index d9a44f28..96866083 100644 --- a/crates/common/test-fixtures/src/verify_signatures.rs +++ b/crates/common/test-fixtures/src/verify_signatures.rs @@ -9,7 +9,6 @@ use ethlambda_types::attestation::{AggregationBits as EthAggregationBits, XmssSi use ethlambda_types::block::{ ByteListMiB, SignedBlock, TypeOneMultiSignature, TypeTwoMultiSignature, }; -use ethlambda_types::primitives::HashTreeRoot as _; use libssz::SszEncode as _; use serde::Deserialize; use std::collections::HashMap; @@ -73,7 +72,6 @@ pub struct TestSignedBlock { impl From for SignedBlock { fn from(value: TestSignedBlock) -> Self { let block: ethlambda_types::block::Block = value.block.into(); - let block_root = block.hash_tree_root(); let proposer_proof = ByteListMiB::try_from(value.signature.proposer_signature.to_vec()) .expect("XMSS signature fits in ByteListMiB"); @@ -82,10 +80,9 @@ impl From for SignedBlock { .attestation_signatures .data .into_iter() - .zip(block.body.attestations.iter()) - .map(|(att_sig, att)| { + .map(|att_sig| { let participants: EthAggregationBits = att_sig.participants.into(); - TypeOneMultiSignature::empty(participants, att.data.hash_tree_root(), att.data.slot) + TypeOneMultiSignature::empty(participants) }) .collect(); @@ -93,8 +90,6 @@ impl From for SignedBlock { all.push(TypeOneMultiSignature::for_proposer( block.proposer_index, proposer_proof, - block_root, - block.slot, )); let merged = TypeTwoMultiSignature::from_type_1s(all); let proof = ByteListMiB::try_from(merged.to_ssz()) @@ -148,7 +143,6 @@ impl TestSignedBlock { /// them through `verify_block_signatures`). pub fn try_into_signed_block_with_proofs(self) -> Result { let block: ethlambda_types::block::Block = self.block.into(); - let block_root = block.hash_tree_root(); let proposer_proof = ByteListMiB::try_from(self.signature.proposer_signature.to_vec()) .expect("XMSS signature fits in ByteListMiB"); @@ -157,9 +151,8 @@ impl TestSignedBlock { .attestation_signatures .data .into_iter() - .zip(block.body.attestations.iter()) .enumerate() - .map(|(index, (att_sig, att))| { + .map(|(index, att_sig)| { let participants: EthAggregationBits = att_sig.participants.into(); let raw = &att_sig.proof_data.data; let stripped = raw.strip_prefix("0x").unwrap_or(raw); @@ -172,12 +165,7 @@ impl TestSignedBlock { let len = bytes.len(); let proof_data = ByteListMiB::try_from(bytes) .map_err(|_| SignedBlockConvertError::ProofTooLarge { index, len })?; - Ok(TypeOneMultiSignature::new( - participants, - att.data.hash_tree_root(), - att.data.slot, - proof_data, - )) + Ok(TypeOneMultiSignature::new(participants, proof_data)) }) .collect::>()?; @@ -189,8 +177,6 @@ impl TestSignedBlock { all.push(TypeOneMultiSignature::for_proposer( block.proposer_index, proposer_proof, - block_root, - block.slot, )); let merged = TypeTwoMultiSignature::from_type_1s(all); let proof = ByteListMiB::try_from(merged.to_ssz()) diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index c5db7ad5..71b1e074 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -51,31 +51,26 @@ pub type ByteListMiB = ByteList<1_048_576>; // ============================================================================ // Type-1 / Type-2 multi-signature model // ============================================================================ - -/// Trusted `Evaluation` field carried inside Type-1 / Type-2 proofs. -/// -/// Upstream models this as a `Bytes32` placeholder until `lean_multisig_py` -/// bindings land with the concrete SSZ serialisation. Mirrored here as `H256`. -pub type BytecodeClaim = H256; - -/// Per-message metadata for a Type-1 (single-message) multi-signer proof. +// +// Wire format mirrors leanSpec PR #717: the proof envelope carries only what +// the verifier cannot rederive from the block body. `message` / `slot` / +// `bytecode_claim` are intentionally absent — the verifier reconstructs each +// component's binding from the block-body attestation it sits next to (plus +// the block root + slot for the proposer entry). + +/// Per-component metadata for a Type-1 multi-signer proof. /// -/// Carries everything a verifier needs to recompute the proof's binding inputs -/// without re-deriving from block content. Participants stay in bitfield form -/// for wire compactness; pubkeys are resolved at the binding boundary from the -/// validator registry. +/// Holds the participant bitfield and the per-component proof bytes in +/// compact no-pubkeys form. Inside a Type-2 envelope, `proof` is the standalone +/// Type-1 wire for this single component, enabling cheap disaggregation +/// without running a fresh SNARK. #[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)] pub struct TypeOneInfo { - /// The 32-byte message that was signed - /// (e.g. `hash_tree_root` of attestation data, or a block root). - pub message: H256, - /// The slot in which the signatures were created. - pub slot: u64, /// Bitfield indicating which validators contributed signatures. pub participants: AggregationBits, - /// Trusted evaluation tied to the proof. Recomputed by the verifier when - /// received externally. - pub bytecode_claim: BytecodeClaim, + /// Standalone Type-1 proof bytes (`compress_without_pubkeys`) for this + /// component. Used by split-by-msg and by re-broadcast paths. + pub proof: ByteListMiB, } /// Maximum number of distinct `AttestationData` entries permitted in a single @@ -93,73 +88,75 @@ pub const MAX_ATTESTATIONS_DATA: usize = 16; pub type TypeOneInfos = SszList; /// A Type-1 single-message proof aggregating signatures from many validators. +/// +/// The outer `proof` field is the canonical aggregated proof bytes; `info.proof` +/// holds the same bytes (kept aligned so a Type-1 embedded inside a Type-2's +/// info list reads identically standalone). `message` and `slot` live on the +/// caller-side block body, not on this envelope. #[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)] pub struct TypeOneMultiSignature { - /// Message, slot, participants, and trusted bytecode claim. + /// Per-component participant bitfield plus the standalone proof bytes. pub info: TypeOneInfo, - /// Raw aggregated proof bytes (`ExecutionProof` on the Rust side). + /// Aggregated proof bytes in compact no-pubkeys representation. pub proof: ByteListMiB, } /// A Type-2 merged proof covering many distinct messages. /// -/// On the wire a `SignedBlock` will carry the SSZ-serialised form of this -/// container as its single proof blob (introduced in a later phase). The -/// block-level info list enumerates every `(message, slot, participants)` -/// tuple the proof binds to. +/// `signed_block.proof` carries the SSZ-encoded form of this container. The +/// `info` list enumerates per-component (participants, standalone Type-1 +/// proof bytes); messages and slots are reconstructed at verify time from the +/// block body. #[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)] pub struct TypeTwoMultiSignature { - /// Per-message metadata, one entry per merged Type-1 proof. + /// Per-component metadata, one entry per merged Type-1 proof. pub info: TypeOneInfos, - /// Aggregation-level trusted evaluation. Recomputed on receive. - pub bytecode_claim: BytecodeClaim, - /// Raw merged proof bytes (`ExecutionProof` on the Rust side). + /// Merged proof bytes in compact no-pubkeys representation. pub proof: ByteListMiB, } impl TypeOneMultiSignature { - /// Build a Type-1 proof with the given participants, message, slot and - /// raw proof bytes. - pub fn new( - participants: AggregationBits, - message: H256, - slot: u64, - proof_data: ByteListMiB, - ) -> Self { + /// Build a Type-1 proof carrying the given participant bitfield and the + /// aggregated proof bytes. + /// + /// `info.proof` and the outer `proof` carry the same bytes. This mirrors + /// leanSpec PR #717's shape (`aggregate_type_1` returns + /// `TypeOneMultiSignature(info=TypeOneInfo(participants, proof=wire), + /// proof=wire)`) so that a Type-1 embedded inside a Type-2's `info[i]` + /// reads the same as a standalone Type-1. The cost is one extra heap copy + /// of ~225 KiB per Type-1 — acceptable in the gossip pipeline; if it + /// shows up in profiling, swap the inner `ByteListMiB` for an + /// `Arc` once SSZ derive supports it. + pub fn new(participants: AggregationBits, proof_data: ByteListMiB) -> Self { Self { info: TypeOneInfo { - message, - slot, participants, - bytecode_claim: BytecodeClaim::ZERO, + proof: proof_data.clone(), }, proof: proof_data, } } - /// Build an empty Type-1 proof with the given participants and message - /// metadata. `proof` bytes are left empty — useful as a placeholder when - /// actual aggregation is not yet performed (forkchoice tests, etc.). - pub fn empty(participants: AggregationBits, message: H256, slot: u64) -> Self { - Self::new(participants, message, slot, SszList::new()) + /// Build a Type-1 proof carrying the given participants and EMPTY proof + /// bytes. Useful as a placeholder in fork-choice payload caches where only + /// the participant set is needed; cannot drive a real Type-2 merge or + /// pass cryptographic verification. + pub fn empty(participants: AggregationBits) -> Self { + Self::new(participants, SszList::new()) } - /// Wrap a proposer's XMSS signature over a block root as a singleton Type-1. + /// Wrap a proposer's Type-1 proof bytes with the singleton participant set. /// - /// Used by block production and test fixtures to fold the proposer's - /// signature into the block-level Type-2 merged proof. - pub fn for_proposer( - proposer_index: u64, - proposer_signature: ByteListMiB, - block_root: H256, - slot: u64, - ) -> Self { + /// The bytes must be a real aggregated Type-1 over the proposer's XMSS + /// signature (e.g. from `ethlambda_crypto::aggregate_signatures`), not + /// raw XMSS bytes — `verify_type_2` rejects raw-XMSS placeholders. + pub fn for_proposer(proposer_index: u64, proposer_proof_bytes: ByteListMiB) -> Self { let mut participants = AggregationBits::with_length(proposer_index as usize + 1) .expect("validator index fits"); participants .set(proposer_index as usize, true) .expect("index within capacity"); - Self::new(participants, block_root, slot, proposer_signature) + Self::new(participants, proposer_proof_bytes) } /// Returns the validator indices that are set in the participants bitfield. @@ -169,21 +166,21 @@ impl TypeOneMultiSignature { } impl TypeTwoMultiSignature { - /// Merge a list of Type-1 single-message proofs into a single Type-2 - /// multi-message proof. Mirrors upstream leanSpec's `aggregate_type_2` - /// stub: the metadata list (`TypeOneInfos`) is faithfully preserved so a - /// verifier can re-derive the per-message binding inputs, but the merged - /// `proof` bytes are left empty until the `lean_multisig_py` bindings ship - /// real cryptographic merging. Block-level signature verification stays - /// structural-only in the meantime, and per-attestation crypto verification - /// continues to run at gossip ingestion. + /// Build a Type-2 envelope from a list of Type-1 components with EMPTY + /// merged proof bytes. Useful for tests that exercise the structural + /// fast-fail leg of `verify_block_signatures` (participants mismatch, + /// missing entries, …) without paying the lean-multisig SNARK cost. + /// + /// Production block production uses + /// [`ethlambda_crypto::merge_type_1s_into_type_2`] to produce a real + /// cryptographic Type-2 proof; do not use this helper for any path that + /// actually verifies the merged proof. pub fn from_type_1s(type_1s: Vec) -> Self { let infos: Vec = type_1s.into_iter().map(|t1| t1.info).collect(); let info = TypeOneInfos::try_from(infos) .expect("type-1 infos within MAX_ATTESTATIONS_DATA + 1 limit"); Self { info, - bytecode_claim: BytecodeClaim::ZERO, proof: ByteListMiB::default(), } } @@ -304,10 +301,8 @@ mod tests { fn sample_type_one_info() -> TypeOneInfo { TypeOneInfo { - message: H256([7u8; 32]), - slot: 42, participants: sample_bits(8, &[0, 3, 7]), - bytecode_claim: H256([1u8; 32]), + proof: ByteListMiB::try_from((0..32u8).collect::>()).unwrap(), } } @@ -316,13 +311,11 @@ mod tests { let info = sample_type_one_info(); let bytes = info.to_ssz(); let decoded = TypeOneInfo::from_ssz_bytes(&bytes).expect("decode"); - assert_eq!(decoded.message, info.message); - assert_eq!(decoded.slot, info.slot); - assert_eq!(decoded.bytecode_claim, info.bytecode_claim); assert_eq!( decoded.participants.as_bytes(), info.participants.as_bytes() ); + assert_eq!(decoded.proof.to_vec(), info.proof.to_vec()); } #[test] @@ -335,44 +328,41 @@ mod tests { let bytes = sig.to_ssz(); let decoded = TypeOneMultiSignature::from_ssz_bytes(&bytes).expect("decode"); assert_eq!(decoded.proof.to_vec(), proof_bytes); - assert_eq!(decoded.info.slot, sig.info.slot); + assert_eq!( + decoded.info.participants.as_bytes(), + sig.info.participants.as_bytes() + ); } #[test] fn type_two_multi_signature_ssz_round_trip() { let infos: Vec = (0..3) .map(|i| TypeOneInfo { - message: H256([i as u8; 32]), - slot: 100 + i as u64, participants: sample_bits(8, &[i, i + 1]), - bytecode_claim: H256([0xAA; 32]), + proof: ByteListMiB::try_from(vec![i as u8; 16]).unwrap(), }) .collect(); let merged_bytes: Vec = (0..128).map(|i| (i % 256) as u8).collect(); let sig = TypeTwoMultiSignature { info: TypeOneInfos::try_from(infos.clone()).unwrap(), - bytecode_claim: H256([0xBB; 32]), proof: ByteListMiB::try_from(merged_bytes.clone()).unwrap(), }; let bytes = sig.to_ssz(); let decoded = TypeTwoMultiSignature::from_ssz_bytes(&bytes).expect("decode"); assert_eq!(decoded.info.len(), 3); assert_eq!(decoded.proof.to_vec(), merged_bytes); - assert_eq!(decoded.bytecode_claim, sig.bytecode_claim); for (got, want) in decoded.info.iter().zip(infos.iter()) { - assert_eq!(got.slot, want.slot); - assert_eq!(got.message, want.message); + assert_eq!(got.participants.as_bytes(), want.participants.as_bytes()); + assert_eq!(got.proof.to_vec(), want.proof.to_vec()); } } #[test] fn type_one_infos_respects_limit() { let too_many: Vec = (0..18) - .map(|i| TypeOneInfo { - message: H256([i as u8; 32]), - slot: i as u64, + .map(|_| TypeOneInfo { participants: sample_bits(1, &[0]), - bytecode_claim: H256([0u8; 32]), + proof: ByteListMiB::default(), }) .collect(); assert!(TypeOneInfos::try_from(too_many).is_err()); diff --git a/crates/net/rpc/src/test_driver.rs b/crates/net/rpc/src/test_driver.rs index 7ceb30ae..2b7cc33e 100644 --- a/crates/net/rpc/src/test_driver.rs +++ b/crates/net/rpc/src/test_driver.rs @@ -44,7 +44,7 @@ use ethlambda_types::{ }, block::{Block, ByteListMiB, TypeOneMultiSignature}, checkpoint::Checkpoint, - primitives::{H256, HashTreeRoot}, + primitives::H256, state::{State, anchor_pair_is_consistent}, }; use serde::{Deserialize, Serialize}; @@ -391,12 +391,7 @@ fn apply_step(store: &mut Store, step: ForkChoiceStep) -> Result<(), String> { .map_err(|err| format!("aggregated proof data too large: {err:?}"))?; let data: ethlambda_types::attestation::AttestationData = att.data.into(); let aggregated = SignedAggregatedAttestation { - proof: TypeOneMultiSignature::new( - participants, - data.hash_tree_root(), - data.slot, - proof_data, - ), + proof: TypeOneMultiSignature::new(participants, proof_data), data, }; store::on_gossip_aggregated_attestation(store, aggregated).map_err(|e| e.to_string()) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 1597afb5..d732dcf4 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -1664,7 +1664,7 @@ mod tests { fn make_proof() -> TypeOneMultiSignature { use ethlambda_types::attestation::AggregationBits; - TypeOneMultiSignature::empty(AggregationBits::new(), H256::ZERO, 0) + TypeOneMultiSignature::empty(AggregationBits::new()) } /// Create a proof with a specific validator bit set (distinct participants). @@ -1672,7 +1672,7 @@ mod tests { use ethlambda_types::attestation::AggregationBits; let mut bits = AggregationBits::with_length(vid + 1).unwrap(); bits.set(vid, true).unwrap(); - TypeOneMultiSignature::empty(bits, H256::ZERO, 0) + TypeOneMultiSignature::empty(bits) } /// Create a proof with bits set for every validator in `vids`. @@ -1683,7 +1683,7 @@ mod tests { for &v in vids { bits.set(v as usize, true).unwrap(); } - TypeOneMultiSignature::empty(bits, H256::ZERO, 0) + TypeOneMultiSignature::empty(bits) } fn make_att_data(slot: u64) -> AttestationData {