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
39 changes: 27 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ clap = { version = "4.3", features = ["derive", "env"] }
# SSZ codec for the two wire types) and the `rand` that signing draws from, so
# the whole crypto stack arrives as one dependency at one revision.
# Pinned to a `main` commit for reproducible builds; bump the rev to track main.
leanvm = { git = "https://github.com/leanEthereum/leanVM.git", rev = "5a4f55c1138759f43f78483a7b70fde973e4a1ee" }
leanvm = { git = "https://github.com/leanEthereum/leanVM.git", rev = "362a7c9b58ca29f8d57fb788ad6064b01e87deaa" }

# Secret-key (de)serialization for the leanVM xmss key format.
postcard = { version = "1.1.3", features = ["alloc"] }
Expand Down
102 changes: 65 additions & 37 deletions crates/common/crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
use ethlambda_types::{block::ByteList512KiB, primitives::H256};

use crate::signature::{ValidatorPublicKey, ValidatorSignature};
use leanvm::{AggregateSignature, WireKeys, XmssGroup, aggregate, xmss};
use leanvm::{ClaimSelection, EthereumProof, SignatureClaims, XmssClaimGroup, aggregate, xmss};
use std::sync::{Mutex, MutexGuard};
use thiserror::Error;
use tracing::error;
Expand Down Expand Up @@ -189,34 +189,51 @@ pub enum VerificationError {
///
/// Getting this structure wrong is not caught at decode: it changes the digest
/// the proof is checked against, so it surfaces as a verification failure.
fn wire_keys(components: &[SignerSet]) -> Result<WireKeys, ConflictingMessages> {
let mut groups: Vec<XmssGroup> = Vec::with_capacity(components.len());
fn wire_keys(components: &[SignerSet]) -> Result<SignatureClaims, ConflictingMessages> {
let mut groups: Vec<XmssClaimGroup> = Vec::with_capacity(components.len());
for component in components {
let keys = component.public_keys.iter().map(|pk| pk.as_inner().clone());
match groups.iter_mut().find(|(slot, ..)| *slot == component.slot) {
Some((_, message, group_keys)) => {
if *message != component.message.0 {
match groups
.iter_mut()
.find(|group| group.epoch == component.slot)
{
Some(group) => {
if group.message != component.message.0 {
return Err(ConflictingMessages {
slot: component.slot,
});
}
group_keys.extend(keys);
group.keys.extend(keys);
}
None => groups.push((component.slot, component.message.0, keys.collect())),
None => groups.push(XmssClaimGroup {
epoch: component.slot,
message: component.message.0,
keys: keys.collect(),
}),
}
}
for (_, _, keys) in &mut groups {
sort_dedup(keys);
for group in &mut groups {
sort_dedup(&mut group.keys);
}
groups.sort_unstable_by_key(|(slot, ..)| *slot);
Ok((groups, Vec::new()))
groups.sort_unstable_by_key(|group| group.epoch);
Ok(SignatureClaims {
xmss: groups,
sphincs: Vec::new(),
})
}

/// [`wire_keys`] for a single claim, which cannot conflict with itself.
fn one_group(message: &H256, slot: u32, public_keys: &[ValidatorPublicKey]) -> WireKeys {
fn one_group(message: &H256, slot: u32, public_keys: &[ValidatorPublicKey]) -> SignatureClaims {
let mut keys: Vec<_> = public_keys.iter().map(|pk| pk.as_inner().clone()).collect();
sort_dedup(&mut keys);
(vec![(slot, message.0, keys)], Vec::new())
SignatureClaims {
xmss: vec![XmssClaimGroup {
epoch: slot,
message: message.0,
keys,
}],
sphincs: Vec::new(),
}
}

/// A group's keys as leanVM's signer set requires them: strictly sorted, so the
Expand Down Expand Up @@ -250,19 +267,19 @@ fn decompress_children(
children: Vec<(Vec<ValidatorPublicKey>, ByteList512KiB)>,
message: &H256,
slot: u32,
) -> Result<Vec<AggregateSignature>, AggregationError> {
) -> Result<Vec<EthereumProof>, AggregationError> {
children
.into_iter()
.enumerate()
.map(|(index, (pubkeys, proof_bytes))| {
let keys = one_group(message, slot, &pubkeys);
AggregateSignature::from_bytes_without_pubkeys(proof_bytes.iter().as_slice(), keys)
EthereumProof::from_bytes_without_pubkeys(proof_bytes.iter().as_slice(), keys)
.map_err(|_| AggregationError::ChildDeserializationFailed(index))
})
.collect()
}

fn compress_to_byte_list(sig: &AggregateSignature) -> Result<ByteList512KiB, AggregationError> {
fn compress_to_byte_list(sig: &EthereumProof) -> Result<ByteList512KiB, AggregationError> {
let serialized = sig.to_bytes_without_pubkeys();
let len = serialized.len();
ByteList512KiB::try_from(serialized).map_err(|_| AggregationError::ProofTooBig(len))
Expand Down Expand Up @@ -323,7 +340,8 @@ pub fn aggregate_signatures(

let _permit = acquire_prover();

let proof = aggregate(&[], raw_xmss, vec![], None, LOG_INV_RATE).map_err(aggregation_failed)?;
let proof =
aggregate(&[], raw_xmss, vec![], &[], None, LOG_INV_RATE).map_err(aggregation_failed)?;

compress_to_byte_list(&proof)
}
Expand Down Expand Up @@ -373,7 +391,7 @@ pub fn aggregate_mixed(

let _permit = acquire_prover();

let proof = aggregate(&children_native, raw_xmss, vec![], None, LOG_INV_RATE)
let proof = aggregate(&children_native, raw_xmss, vec![], &[], None, LOG_INV_RATE)
.map_err(aggregation_failed)?;

compress_to_byte_list(&proof)
Expand Down Expand Up @@ -410,7 +428,7 @@ pub fn aggregate_proofs(

let _permit = acquire_prover();

let proof = aggregate(&children_native, vec![], vec![], None, LOG_INV_RATE)
let proof = aggregate(&children_native, vec![], vec![], &[], None, LOG_INV_RATE)
.map_err(aggregation_failed)?;

compress_to_byte_list(&proof)
Expand Down Expand Up @@ -440,7 +458,7 @@ pub fn verify_aggregated_signature(
}

let keys = one_group(message, slot, &public_keys);
let sig = AggregateSignature::from_bytes_without_pubkeys(proof_data.iter().as_slice(), keys)
let sig = EthereumProof::from_bytes_without_pubkeys(proof_data.iter().as_slice(), keys)
.map_err(|_| VerificationError::DeserializationFailed)?;

sig.verify()
Expand Down Expand Up @@ -484,19 +502,19 @@ pub fn merge_type_1s_into_type_2(
return Ok(dummy);
}

let type_1s_native: Vec<AggregateSignature> = type_1s
let type_1s_native: Vec<EthereumProof> = type_1s
.iter()
.enumerate()
.map(|(index, (claim, proof_bytes))| {
let keys = one_group(&claim.message, claim.slot, &claim.public_keys);
AggregateSignature::from_bytes_without_pubkeys(proof_bytes.iter().as_slice(), keys)
EthereumProof::from_bytes_without_pubkeys(proof_bytes.iter().as_slice(), keys)
.map_err(|_| AggregationError::ChildDeserializationFailed(index))
})
.collect::<Result<_, _>>()?;

let _permit = acquire_prover();

let merged = aggregate(&type_1s_native, vec![], vec![], None, LOG_INV_RATE)
let merged = aggregate(&type_1s_native, vec![], vec![], &[], None, LOG_INV_RATE)
.map_err(aggregation_failed)?;

compress_to_byte_list(&merged)
Expand All @@ -517,7 +535,7 @@ pub fn verify_type_2_signature(
}

let keys = wire_keys(components)?;
let sig = AggregateSignature::from_bytes_without_pubkeys(proof_data, keys)
let sig = EthereumProof::from_bytes_without_pubkeys(proof_data, keys)
.map_err(|_| VerificationError::DeserializationFailed)?;

sig.verify()
Expand Down Expand Up @@ -549,26 +567,35 @@ pub fn split_type_2_by_message(
}

let keys = wire_keys(components)?;
let type_2 = AggregateSignature::from_bytes_without_pubkeys(proof_data, keys)
let type_2 = EthereumProof::from_bytes_without_pubkeys(proof_data, keys)
.map_err(|_| AggregationError::DeserializationFailed)?;

// A slot carries one message, so a message that appears at all appears in
// exactly one group unless two slots signed the very same bytes.
let mut matches = type_2
.xmss_signers()
.iter()
.filter(|(_, group_message, _)| *group_message == message.0);
.filter(|group| group.message == message.0);
let group = match (matches.next(), matches.next()) {
(Some(group), None) => group.clone(),
(None, _) => return Err(AggregationError::UnknownMessage),
(Some(_), Some(_)) => return Err(AggregationError::MultipleMessages),
};

let declare: WireKeys = (vec![group], Vec::new());
let kept = SignatureClaims {
xmss: vec![group],
sphincs: Vec::new(),
};
// No blobs: ethlambda makes no LeanDA claim, so the selection publishes the
// one signature group and no DA roots.
let declare = ClaimSelection {
signatures: &kept,
da_commitments: &[],
};

let _permit = acquire_prover();

let component = aggregate(&[type_2], vec![], vec![], Some(&declare), LOG_INV_RATE)
let component = aggregate(&[type_2], vec![], vec![], &[], Some(declare), LOG_INV_RATE)
.map_err(aggregation_failed)?;

compress_to_byte_list(&component)
Expand Down Expand Up @@ -649,18 +676,19 @@ mod tests {
SignerSet::new(msg_a, 4, vec![pk(2)]),
SignerSet::new(msg_b, 9, vec![pk(1), pk(2)]),
];
let (groups, sphincs) = wire_keys(&components).expect("one message per slot");
let claims = wire_keys(&components).expect("one message per slot");
let groups = &claims.xmss;

assert!(sphincs.is_empty(), "ethlambda signs XMSS only");
let slots: Vec<u32> = groups.iter().map(|(slot, ..)| *slot).collect();
assert!(claims.sphincs.is_empty(), "ethlambda signs XMSS only");
let slots: Vec<u32> = groups.iter().map(|group| group.epoch).collect();
assert_eq!(slots, vec![4, 9], "groups sorted by slot");
assert_eq!(groups[0].1, msg_a.0);
assert_eq!(groups[1].1, msg_b.0);
assert_eq!(groups[0].2.len(), 1);
assert_eq!(groups[0].message, msg_a.0);
assert_eq!(groups[1].message, msg_b.0);
assert_eq!(groups[0].keys.len(), 1);
// Keys 1, 2, 3 unioned across the two slot-9 claims, deduplicated.
assert_eq!(groups[1].2.len(), 3);
assert_eq!(groups[1].keys.len(), 3);
assert!(
groups[1].2.windows(2).all(|w| w[0] < w[1]),
groups[1].keys.windows(2).all(|w| w[0] < w[1]),
"keys strictly sorted"
);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/benchmarking.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ otherwise, because a mis-attributed report is worse than no report.
Block-building benchmark β€” synthetic workload (mock crypto)
validators=8 warmup_slots=8 iterations=10 proofs_per_data=1 seed=42
enable_proposer_aggregation=false max_attestations_per_block=3
ethlambda/v0.1.0/aarch64-apple-darwin/rustc-v1.97.1 leanvm=5a4f55c1 os=macos arch=aarch64 threads=14
ethlambda/v0.1.0/aarch64-apple-darwin/rustc-v1.97.1 leanvm=362a7c9b os=macos arch=aarch64 threads=14

iter compact select_payloads stf_simulate overhead wall root
1 0.000ms 0.002ms 0.015ms 0.068ms 0.085ms 0x7282cc99
Expand Down
2 changes: 1 addition & 1 deletion docs/keygen.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ hash_function: BLAKE2s
encoding: TargetSum
pubkey_bytes: 32
lifetime: 4294967296
leanvm_rev: 5a4f55c1138759f43f78483a7b70fde973e4a1ee
leanvm_rev: 362a7c9b58ca29f8d57fb788ad6064b01e87deaa
log_num_active_epochs: 18
num_active_epochs: 262144
num_validators: 3
Expand Down
Loading