From 6911128ed162832cdd38799511d62595f7721e54 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:24 +0100 Subject: [PATCH 01/25] xmss: implement Display and Error for the public error enums Co-Authored-By: Claude Fable 5 --- crates/xmss/src/xmss.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 015adb41..cb11b1ca 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -82,6 +82,19 @@ pub enum XmssKeyGenError { InvalidRange, } +impl std::fmt::Display for XmssKeyGenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidRange => write!( + f, + "invalid slot range (empty, reversed, or beyond the 2^{LOG_LIFETIME} lifetime)" + ), + } + } +} + +impl std::error::Error for XmssKeyGenError {} + fn fill(sequential: bool, data: &mut [T], f: impl Fn(usize, &mut T) + Sync) { if sequential { data.iter_mut().enumerate().for_each(|(i, out)| f(i, out)); @@ -231,6 +244,17 @@ pub enum XmssSignatureError { InvalidRandomness, } +impl std::fmt::Display for XmssSignatureError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SlotOutOfRange => write!(f, "slot is outside the key's valid range"), + Self::InvalidRandomness => write!(f, "randomness does not yield a valid WOTS encoding"), + } + } +} + +impl std::error::Error for XmssSignatureError {} + /// WARNING: XMSS is statefull signature, you should never sign with the same same `slot` twice. /// (Even signing twice with the same message, at the same slot, is insecure, due to the non-determinism of the randomness part of the signature) pub fn xmss_sign( @@ -319,6 +343,17 @@ pub enum XmssVerifyError { InvalidMerklePath, } +impl std::fmt::Display for XmssVerifyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidWots => write!(f, "invalid WOTS signature (encoding rejected or wrong chain tips)"), + Self::InvalidMerklePath => write!(f, "merkle path does not lead to the public key's root"), + } + } +} + +impl std::error::Error for XmssVerifyError {} + pub fn xmss_verify( pub_key: &XmssPublicKey, message: &[F; MESSAGE_LEN_FE], From 0ffb877faf8c329ffcc42b9faea085eac9064125 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:25 +0100 Subject: [PATCH 02/25] xmss: clean up signing internals - Check the slot range before grinding the encoding randomness (no expensive work on out-of-range slots). - Reuse the encoding found during grinding instead of recomputing it, which removes the unreachable InvalidRandomness error variant and the now-unused WotsSecretKey::sign_with_randomness. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/wots.rs | 13 +------------ crates/xmss/src/xmss.rs | 9 ++------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/crates/xmss/src/wots.rs b/crates/xmss/src/wots.rs index a362229e..230d7db5 100644 --- a/crates/xmss/src/wots.rs +++ b/crates/xmss/src/wots.rs @@ -41,18 +41,7 @@ impl WotsSecretKey { &self.public_key } - pub fn sign_with_randomness( - &self, - message: &[F; MESSAGE_LEN_FE], - slot: u32, - xmss_pub_key: &XmssPublicKey, - randomness: Randomness, - ) -> Option { - let encoding = wots_encode(message, slot, xmss_pub_key, &randomness)?; - Some(self.sign_with_encoding(randomness, &encoding, xmss_pub_key.public_param, slot)) - } - - fn sign_with_encoding( + pub(crate) fn sign_with_encoding( &self, randomness: Randomness, encoding: &[u8; V], diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index cb11b1ca..b8914fc6 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -241,14 +241,12 @@ pub fn xmss_key_gen( #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum XmssSignatureError { SlotOutOfRange, - InvalidRandomness, } impl std::fmt::Display for XmssSignatureError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::SlotOutOfRange => write!(f, "slot is outside the key's valid range"), - Self::InvalidRandomness => write!(f, "randomness does not yield a valid WOTS encoding"), } } } @@ -263,15 +261,12 @@ pub fn xmss_sign( message: &[F; MESSAGE_LEN_FE], slot: u32, ) -> Result { - let (randomness, _, _) = find_randomness_for_wots_encoding(message, slot, &secret_key.public_key(), rng); - if slot < secret_key.slot_start || slot > secret_key.slot_end { return Err(XmssSignatureError::SlotOutOfRange); } + let (randomness, encoding, _) = find_randomness_for_wots_encoding(message, slot, &secret_key.public_key(), rng); let wots_secret_key = gen_wots_secret_key(&secret_key.seed, slot, secret_key.public_param); - let wots_signature = wots_secret_key - .sign_with_randomness(message, slot, &secret_key.public_key(), randomness) - .ok_or(XmssSignatureError::InvalidRandomness)?; + let wots_signature = wots_secret_key.sign_with_encoding(randomness, &encoding, secret_key.public_param, slot); // Cache the bottom subtree covering `slot` (reused across its 2^split_level slots), then read the path. let subtree_index = (slot as u64) >> secret_key.split_level; let mut cache = secret_key.cache.lock().unwrap(); From 7940e5e10191f2131c2c6ce874fc8a80a66a3748 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:25 +0100 Subject: [PATCH 03/25] xmss: leanSig-style key generation API xmss_key_gen(rng, activation_slot, num_active_slots) -> (pk, sk), sampling the seed from the caller's RNG and validating the activation interval against the 2^32 lifetime. The 'sequential' flag is gone from the public API: key generation asks the pool whether it already runs inside a pool task - where a nested dispatch would panic - via the new parallel::is_in_pool_task(), and falls back to a sequential build in that case (e.g. generating many keys in a parallel batch). Bottom-subtree builds during signing remain always sequential: signing must never wait on the thread pool while holding the signing-cache mutex, or a pool task blocked on the same key would deadlock the pool, and hence the signer. Co-Authored-By: Claude Fable 5 --- crates/backend/parallel/src/lib.rs | 7 +++++ crates/xmss/src/signers_cache.rs | 6 ++--- crates/xmss/src/xmss.rs | 42 +++++++++++++++++++----------- crates/xmss/tests/xmss_tests.rs | 27 ++++++++++++++----- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/crates/backend/parallel/src/lib.rs b/crates/backend/parallel/src/lib.rs index 631dc8d0..079f8799 100644 --- a/crates/backend/parallel/src/lib.rs +++ b/crates/backend/parallel/src/lib.rs @@ -58,6 +58,13 @@ pub(crate) fn current_worker_id() -> usize { WORKER_ID.with(Cell::get) } +/// True while the calling thread is running a pool task, where a nested dispatch would panic. +/// Lets code that runs both from the dispatcher and from within tasks pick a sequential path. +#[must_use] +pub fn is_in_pool_task() -> bool { + IN_TASK.get() +} + /// Type-erased work unit. The `&dyn Fn` lifetime is erased to `'static`; it is dereferenced /// only inside a dispatch window during which the dispatcher blocks, so the borrow outlives /// every call. Range-based (`f(start, end)`) so a reduction looks up its per-worker diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 81140d5f..55be7f45 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -1,6 +1,6 @@ use backend::*; +use rand::SeedableRng; use rand::rngs::StdRng; -use rand::{RngExt, SeedableRng}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; @@ -60,9 +60,7 @@ fn cache_path(first_pubkey: &XmssPublicKey) -> PathBuf { fn compute_signer(index: usize) -> (XmssPublicKey, XmssSignature) { let mut rng = StdRng::seed_from_u64(index as u64); - let key_start = BENCHMARK_SLOT; - let key_end = BENCHMARK_SLOT + 1; - let (sk, pk) = xmss_key_gen(rng.random(), key_start, key_end, true).unwrap(); + let (pk, sk) = xmss_key_gen(&mut rng, BENCHMARK_SLOT as u64, 2).unwrap(); let sig = xmss_sign(&mut rng, &sk, &message_for_benchmark(), BENCHMARK_SLOT).unwrap(); (pk, sig) } diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index b8914fc6..886fa1b6 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -1,7 +1,7 @@ use std::sync::Mutex; use backend::*; -use rand::{CryptoRng, SeedableRng, rngs::StdRng}; +use rand::{CryptoRng, RngExt, SeedableRng, rngs::StdRng}; use serde::{Deserialize, Serialize}; use crate::*; @@ -184,21 +184,31 @@ fn build_subtree_layers( layers } -pub fn xmss_key_gen( - seed: [u8; 32], - slot_start: u32, - slot_end: u32, - sequential: bool, -) -> Result<(XmssSecretKey, XmssPublicKey), XmssKeyGenError> { - if slot_start > slot_end || slot_end as u64 >= (1 << LOG_LIFETIME) { +/// Generates a new key pair, active for the `num_active_slots` slots starting at +/// `activation_slot` (both ends must stay within the `2^LOG_LIFETIME` lifetime). +pub fn xmss_key_gen( + rng: &mut R, + activation_slot: u64, + num_active_slots: u64, +) -> Result<(XmssPublicKey, XmssSecretKey), XmssKeyGenError> { + let activation_end = activation_slot + .checked_add(num_active_slots) + .ok_or(XmssKeyGenError::InvalidRange)?; + if num_active_slots == 0 || activation_end > 1 << LOG_LIFETIME { return Err(XmssKeyGenError::InvalidRange); } + let seed: [u8; 32] = rng.random(); + + // The pool forbids nested dispatch: build sequentially when key gen itself already runs + // inside a pool task (e.g. generating many keys in a parallel batch). + let sequential = parallel::is_in_pool_task(); + let public_param: PublicParam = gen_public_param(&seed); - let lo = slot_start as u64; - let hi = slot_end as u64; + let lo = activation_slot; + let hi = activation_end - 1; // ~sqrt(R) leaves per bottom subtree; always <= LOG_LIFETIME/2 since R <= 2^LOG_LIFETIME. - let split_level = log2_ceil_usize((hi - lo + 1) as usize).div_ceil(2); + let split_level = log2_ceil_usize(num_active_slots as usize).div_ceil(2); // Roots of each bottom subtree, built one at a time so peak memory stays O(sqrt(R)). let first_subtree = lo >> split_level; @@ -227,15 +237,15 @@ pub fn xmss_key_gen( public_param, }; let secret_key = XmssSecretKey { - slot_start, - slot_end, + slot_start: activation_slot as u32, + slot_end: hi as u32, public_param, seed, split_level, top, cache: Mutex::new(None), }; - Ok((secret_key, pub_key)) + Ok((pub_key, secret_key)) } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] @@ -293,7 +303,9 @@ impl XmssSecretKey { } } - /// (Re)build the bottom subtree with the given index. + /// (Re)build the bottom subtree with the given index. Always sequential: signing must + /// never wait on the thread pool while it holds the signing-cache mutex (a pool task + /// blocked on the same key would deadlock the pool, and hence the signer). fn build_bottom_subtree(&self, subtree_index: u64) -> BottomSubtree { let (lo, hi) = subtree_bounds( self.slot_start as u64, diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 6d2721df..5fb72ce9 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -6,13 +6,10 @@ type F = KoalaBear; #[test] fn test_xmss_serialize_deserialize() { - let keygen_seed: [u8; 32] = std::array::from_fn(|i| i as u8); let message: [F; MESSAGE_LEN_FE] = std::array::from_fn(|i| F::from_usize(i * 3 + 7)); - let slot_start = 100; - let slot_end = 115; let slot = 110; - let (sk, pk) = xmss_key_gen(keygen_seed, slot_start, slot_end, false).unwrap(); + let (pk, sk) = xmss_key_gen(&mut StdRng::seed_from_u64(0), 100, 16).unwrap(); let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, &message, slot).unwrap(); let pk_bytes = postcard::to_allocvec(&pk).unwrap(); @@ -28,16 +25,32 @@ fn test_xmss_serialize_deserialize() { #[test] fn keygen_sign_verify() { - let keygen_seed: [u8; 32] = std::array::from_fn(|i| i as u8); let message: [F; MESSAGE_LEN_FE] = std::array::from_fn(|i| F::from_usize(i * 3 + 7)); for slot in [0, 1234, u32::MAX] { - let (sk, pk) = xmss_key_gen(keygen_seed, slot.saturating_sub(1), slot.saturating_add(2), false).unwrap(); - let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, &message, slot).unwrap(); + let activation_slot = (slot as u64).saturating_sub(1); + let num_active_slots = (slot as u64 + 3).min(1 << LOG_LIFETIME) - activation_slot; + let mut rng = StdRng::seed_from_u64(slot as u64); + let (pk, sk) = xmss_key_gen(&mut rng, activation_slot, num_active_slots).unwrap(); + let sig = xmss_sign(&mut rng, &sk, &message, slot).unwrap(); xmss_verify(&pk, &message, &sig, slot).unwrap(); } } +#[test] +fn keygen_rejects_invalid_ranges() { + let mut rng = StdRng::seed_from_u64(0); + assert_eq!(xmss_key_gen(&mut rng, 0, 0).unwrap_err(), XmssKeyGenError::InvalidRange); + assert_eq!( + xmss_key_gen(&mut rng, (1 << LOG_LIFETIME) - 1, 2).unwrap_err(), + XmssKeyGenError::InvalidRange + ); + assert_eq!( + xmss_key_gen(&mut rng, u64::MAX, u64::MAX).unwrap_err(), + XmssKeyGenError::InvalidRange + ); +} + #[test] #[ignore] fn encoding_grinding_bits() { From bbf9c3945e5ad02b9a344c17364a7af000eb889f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:25 +0100 Subject: [PATCH 04/25] xmss: sign 32-byte messages, hashed off-circuit into 8 field elements The public API now takes message: &[u8; 32] (like leanSig), in leanSig's parameter order: sign(rng, sk, slot, msg) / verify(pk, slot, msg, sig). The message is embedded injectively into 9 field elements (little-endian base-p decomposition, identical convention to leanSig's encode_message) and hashed with a domain-separated poseidon16_compress into the [F; 8] 'message' the WOTS encoding - and hence the snark, which is unchanged - consumes. hash_message() is public so the aggregation pipeline can compute the snark-side message at the boundary. encode_message is checked against independently computed base-p reference vectors. Co-Authored-By: Claude Fable 5 --- crates/rec_aggregation/src/benchmark.rs | 2 +- .../src/single_message_aggregation.rs | 2 +- crates/xmss/src/lib.rs | 76 ++++++++++++++++++- crates/xmss/src/signers_cache.rs | 12 +-- crates/xmss/src/xmss.rs | 13 ++-- crates/xmss/tests/xmss_tests.rs | 16 ++-- src/lib.rs | 5 +- 7 files changed, 105 insertions(+), 21 deletions(-) diff --git a/crates/rec_aggregation/src/benchmark.rs b/crates/rec_aggregation/src/benchmark.rs index 42d0c601..6a071d05 100644 --- a/crates/rec_aggregation/src/benchmark.rs +++ b/crates/rec_aggregation/src/benchmark.rs @@ -401,7 +401,7 @@ fn build_aggregation( let result = aggregate_single_message_signatures( &children, raw_xmss.clone(), - message_for_benchmark(), + xmss::hash_message(&message_for_benchmark()), BENCHMARK_SLOT, topology.log_inv_rate, ) diff --git a/crates/rec_aggregation/src/single_message_aggregation.rs b/crates/rec_aggregation/src/single_message_aggregation.rs index 4a6f7569..d7288a04 100644 --- a/crates/rec_aggregation/src/single_message_aggregation.rs +++ b/crates/rec_aggregation/src/single_message_aggregation.rs @@ -481,7 +481,7 @@ mod tests { init_aggregation_bytecode(); let log_inv_rate = 2; - let message = message_for_benchmark(); + let message = xmss::hash_message(&message_for_benchmark()); let slot: u32 = BENCHMARK_SLOT; let signatures = get_benchmark_signatures(); let raws_inner = signatures[0..10].to_vec(); diff --git a/crates/xmss/src/lib.rs b/crates/xmss/src/lib.rs index c85871ea..b69698a6 100644 --- a/crates/xmss/src/lib.rs +++ b/crates/xmss/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] -use backend::{DIGEST_LEN_FE, KoalaBear, POSEIDON1_WIDTH, PrimeCharacteristicRing, poseidon16_compress}; +use backend::{DIGEST_LEN_FE, KoalaBear, POSEIDON1_WIDTH, PrimeCharacteristicRing, PrimeField32, poseidon16_compress}; pub mod signers_cache; mod wots; @@ -23,6 +23,11 @@ pub const NUM_CHAIN_HASHES: usize = 110; pub const TARGET_SUM: usize = V * (CHAIN_LENGTH - 1) - NUM_CHAIN_HASHES; pub const NUM_ENCODING_FE: usize = V.div_ceil(24 / W); pub const RANDOMNESS_LEN_FE: usize = 6; +/// Byte length of the messages being signed. +pub const MESSAGE_LEN_BYTES: usize = 32; +/// Field elements of the injective base-p embedding of a message (p > 2^30, 9 * 30 >= 8 * 32). +pub const MESSAGE_EMBEDDING_LEN_FE: usize = 9; +/// Field elements of the hashed message, the form consumed by WOTS encoding (and the snark). pub const MESSAGE_LEN_FE: usize = 8; pub const PUBLIC_PARAM_LEN_FE: usize = 4; pub const PUB_KEY_FLAT_SIZE: usize = XMSS_DIGEST_LEN + PUBLIC_PARAM_LEN_FE; @@ -38,10 +43,45 @@ pub const TWEAK_TYPE_MERKLE: usize = 2; pub const TWEAK_TYPE_ENCODING: usize = 3; const _: () = assert!(V.is_multiple_of(2)); // For efficiency of the snark (we can batch chains in pairs) +const _: () = assert!(MESSAGE_EMBEDDING_LEN_FE * 30 >= MESSAGE_LEN_BYTES * 8); // Injective embedding +const _: () = assert!(MESSAGE_LEN_FE == DIGEST_LEN_FE); // hash_message output is one Poseidon digest +const _: () = assert!(1 + MESSAGE_EMBEDDING_LEN_FE <= POSEIDON1_WIDTH); // Domain sep + embedding fit +// Domain separators, placed in the first lane of a poseidon16_compress input. The window +// [336, 1024) is collision-free with every tweak first lane: chain tweaks with index_hi = 0 +// stay below 336 (sub_position <= V * CHAIN_LENGTH - 1), any other tweak is >= 1024. pub(crate) const PRF_DOMAINSEP_WOTS_SECRET_KEY: u32 = 1000; pub(crate) const PRF_DOMAINSEP_PUBLIC_PARAM: u32 = 1001; pub(crate) const PRF_DOMAINSEP_RANDOM_NODE: u32 = 1002; +pub(crate) const DOMAINSEP_MESSAGE_HASH: u32 = 1004; + +/// Injective embedding of a message into `MESSAGE_EMBEDDING_LEN_FE` field elements: +/// little-endian base-p decomposition of the message read as a little-endian integer +/// (the same convention as leanSig's `encode_message`). +pub fn encode_message(message: &[u8; MESSAGE_LEN_BYTES]) -> [F; MESSAGE_EMBEDDING_LEN_FE] { + let p = u64::from(F::ORDER_U32); + let mut words: [u32; MESSAGE_LEN_BYTES / 4] = + std::array::from_fn(|i| u32::from_le_bytes(message[4 * i..4 * (i + 1)].try_into().unwrap())); + std::array::from_fn(|_| { + // Long division of the little-endian `words` integer by p; the remainder is the limb. + let mut rem: u64 = 0; + for word in words.iter_mut().rev() { + let cur = (rem << 32) | u64::from(*word); + *word = (cur / p) as u32; + rem = cur % p; + } + F::from_u64(rem) + }) +} + +/// Off-circuit message hash: what gets signed (and what the snark consumes as "message") is +/// this domain-separated Poseidon digest of the 32-byte message. +pub fn hash_message(message: &[u8; MESSAGE_LEN_BYTES]) -> [F; MESSAGE_LEN_FE] { + let mut input = [F::ZERO; POSEIDON1_WIDTH]; + input[0] = F::from_u32(DOMAINSEP_MESSAGE_HASH); + input[1..1 + MESSAGE_EMBEDDING_LEN_FE].copy_from_slice(&encode_message(message)); + poseidon16_compress(input) +} pub(crate) fn poseidon_prf(domain: u32, seed: &[u8; 32], indices: [usize; 2]) -> [F; DIGEST_LEN_FE] { let mut input = [F::ZERO; 16]; @@ -106,3 +146,37 @@ pub(crate) fn build_right_chain_input(public_param: &PublicParam) -> [F; DIGEST_ right[..PUBLIC_PARAM_LEN_FE].copy_from_slice(public_param); right } + +#[cfg(test)] +mod tests { + use super::*; + + /// Expected limbs computed independently (big-integer base-p decomposition, little-endian). + #[test] + fn encode_message_reference_vectors() { + let cases: [([u8; 32], [u32; MESSAGE_EMBEDDING_LEN_FE]); 3] = [ + ( + std::array::from_fn(|i| i as u8), + [ + 158200685, 22817125, 768861932, 1220633732, 741473605, 1829125427, 227592113, 282695284, 33, + ], + ), + ( + [0xFF; 32], + [ + 1539525976, 1261153412, 1969546126, 1544481308, 1871195519, 936857536, 333911385, 1230415057, 272, + ], + ), + ( + std::array::from_fn(|i| if i % 2 == 0 { 0x00 } else { 0xFF }), + [ + 1914907182, 1596164347, 55024625, 1538471654, 1366473412, 361154807, 606204774, 1101267152, 271, + ], + ), + ]; + for (message, expected) in cases { + assert_eq!(encode_message(&message).map(|l| l.as_canonical_u32()), expected); + } + assert_eq!(encode_message(&[0; 32]), [F::ZERO; MESSAGE_EMBEDDING_LEN_FE]); + } +} diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 55be7f45..7a5d9550 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -19,11 +19,11 @@ pub fn get_benchmark_signatures() -> &'static Vec<(XmssPublicKey, XmssSignature) pub const BENCHMARK_SLOT: u32 = 111; pub const NUM_BENCHMARK_SIGNERS: usize = 10_000; -pub fn message_for_benchmark() -> [F; MESSAGE_LEN_FE] { - std::array::from_fn(F::from_usize) +pub fn message_for_benchmark() -> [u8; MESSAGE_LEN_BYTES] { + std::array::from_fn(|i| i as u8) } -const CACHE_SCHEMA_VERSION: u32 = 4; +const CACHE_SCHEMA_VERSION: u32 = 5; #[derive(Serialize, Deserialize)] struct SignersCacheFile { @@ -35,7 +35,7 @@ fn cache_footprint(first_pubkey: &XmssPublicKey) -> u128 { let mut input = [F::ZERO; 16]; input[0] = F::from_usize(NUM_BENCHMARK_SIGNERS); input[1] = F::from_u32(BENCHMARK_SLOT); - input[2..2 + MESSAGE_LEN_FE].copy_from_slice(&message_for_benchmark()); + input[2..2 + MESSAGE_LEN_FE].copy_from_slice(&hash_message(&message_for_benchmark())); input[2 + MESSAGE_LEN_FE..][..XMSS_DIGEST_LEN].copy_from_slice(&first_pubkey.merkle_root); let digest = poseidon16_compress(input); digest[..4] @@ -61,7 +61,7 @@ fn cache_path(first_pubkey: &XmssPublicKey) -> PathBuf { fn compute_signer(index: usize) -> (XmssPublicKey, XmssSignature) { let mut rng = StdRng::seed_from_u64(index as u64); let (pk, sk) = xmss_key_gen(&mut rng, BENCHMARK_SLOT as u64, 2).unwrap(); - let sig = xmss_sign(&mut rng, &sk, &message_for_benchmark(), BENCHMARK_SLOT).unwrap(); + let sig = xmss_sign(&mut rng, &sk, BENCHMARK_SLOT, &message_for_benchmark()).unwrap(); (pk, sig) } @@ -121,7 +121,7 @@ fn test_signature_cache() { let signatures = get_benchmark_signatures(); parallel::for_each_index(signatures.len(), |i| { let (pk, sig) = &signatures[i]; - xmss_verify(pk, &message_for_benchmark(), sig, BENCHMARK_SLOT) + xmss_verify(pk, BENCHMARK_SLOT, &message_for_benchmark(), sig) .unwrap_or_else(|_| panic!("Signature {} failed to verify", i)); }); } diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 886fa1b6..0847ad79 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -268,13 +268,15 @@ impl std::error::Error for XmssSignatureError {} pub fn xmss_sign( rng: &mut R, secret_key: &XmssSecretKey, - message: &[F; MESSAGE_LEN_FE], slot: u32, + message: &[u8; MESSAGE_LEN_BYTES], ) -> Result { if slot < secret_key.slot_start || slot > secret_key.slot_end { return Err(XmssSignatureError::SlotOutOfRange); } - let (randomness, encoding, _) = find_randomness_for_wots_encoding(message, slot, &secret_key.public_key(), rng); + let message_fe = hash_message(message); + let (randomness, encoding, _) = + find_randomness_for_wots_encoding(&message_fe, slot, &secret_key.public_key(), rng); let wots_secret_key = gen_wots_secret_key(&secret_key.seed, slot, secret_key.public_param); let wots_signature = wots_secret_key.sign_with_encoding(randomness, &encoding, secret_key.public_param, slot); // Cache the bottom subtree covering `slot` (reused across its 2^split_level slots), then read the path. @@ -363,13 +365,14 @@ impl std::error::Error for XmssVerifyError {} pub fn xmss_verify( pub_key: &XmssPublicKey, - message: &[F; MESSAGE_LEN_FE], - signature: &XmssSignature, slot: u32, + message: &[u8; MESSAGE_LEN_BYTES], + signature: &XmssSignature, ) -> Result<(), XmssVerifyError> { + let message_fe = hash_message(message); let wots_public_key = signature .wots_signature - .recover_public_key(message, slot, pub_key) + .recover_public_key(&message_fe, slot, pub_key) .ok_or(XmssVerifyError::InvalidWots)?; let mut current_hash = wots_public_key.hash(pub_key.public_param, slot); for (level, neighbour) in signature.merkle_proof.iter().enumerate() { diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 5fb72ce9..af6f428d 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -6,11 +6,11 @@ type F = KoalaBear; #[test] fn test_xmss_serialize_deserialize() { - let message: [F; MESSAGE_LEN_FE] = std::array::from_fn(|i| F::from_usize(i * 3 + 7)); + let message: [u8; MESSAGE_LEN_BYTES] = std::array::from_fn(|i| (i * 3 + 7) as u8); let slot = 110; let (pk, sk) = xmss_key_gen(&mut StdRng::seed_from_u64(0), 100, 16).unwrap(); - let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, &message, slot).unwrap(); + let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, slot, &message).unwrap(); let pk_bytes = postcard::to_allocvec(&pk).unwrap(); let pk2: XmssPublicKey = postcard::from_bytes(&pk_bytes).unwrap(); @@ -20,20 +20,24 @@ fn test_xmss_serialize_deserialize() { let sig2: XmssSignature = postcard::from_bytes(&sig_bytes).unwrap(); assert_eq!(sig, sig2); - xmss_verify(&pk2, &message, &sig2, slot).unwrap(); + xmss_verify(&pk2, slot, &message, &sig2).unwrap(); } #[test] fn keygen_sign_verify() { - let message: [F; MESSAGE_LEN_FE] = std::array::from_fn(|i| F::from_usize(i * 3 + 7)); + let message: [u8; MESSAGE_LEN_BYTES] = std::array::from_fn(|i| (i * 3 + 7) as u8); for slot in [0, 1234, u32::MAX] { let activation_slot = (slot as u64).saturating_sub(1); let num_active_slots = (slot as u64 + 3).min(1 << LOG_LIFETIME) - activation_slot; let mut rng = StdRng::seed_from_u64(slot as u64); let (pk, sk) = xmss_key_gen(&mut rng, activation_slot, num_active_slots).unwrap(); - let sig = xmss_sign(&mut rng, &sk, &message, slot).unwrap(); - xmss_verify(&pk, &message, &sig, slot).unwrap(); + let sig = xmss_sign(&mut rng, &sk, slot, &message).unwrap(); + xmss_verify(&pk, slot, &message, &sig).unwrap(); + + let mut other_message = message; + other_message[0] ^= 1; + assert!(xmss_verify(&pk, slot, &other_message, &sig).is_err()); } } diff --git a/src/lib.rs b/src/lib.rs index 521bc65d..12e4aea4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,10 @@ pub use rec_aggregation::{ merge_single_message_aggregates, split_multi_message_aggregate, verify_multi_message_aggregate, verify_single_message_aggregate, }; -pub use xmss::{MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, xmss_key_gen, xmss_sign, xmss_verify}; +pub use xmss::{ + MESSAGE_LEN_BYTES, MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, hash_message, xmss_key_gen, + xmss_sign, xmss_verify, +}; pub type F = KoalaBear; From fb934ccb5d5e88c4965a85e061d940b1dbd027c9 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:25 +0100 Subject: [PATCH 05/25] xmss: derandomize signing xmss_sign(&sk, slot, message) no longer takes an RNG: the encoding randomness for attempt i is Poseidon-derived from (secret seed, slot, attempt, hashed message). Re-signing the same (slot, message) pair now returns the identical signature, so an accidental double-sign of the same message is harmless (signing a different message at the same slot remains fatal, as in any synchronized scheme). Grinding is bounded by MAX_SIGNING_ATTEMPTS (expected ~a few hundred attempts for target_sum = 184); exceeding it returns EncodingAttemptsExceeded instead of looping forever. The unbounded find_randomness_for_wots_encoding is gone. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/lib.rs | 5 +++++ crates/xmss/src/signers_cache.rs | 4 ++-- crates/xmss/src/wots.rs | 16 -------------- crates/xmss/src/xmss.rs | 37 ++++++++++++++++++++++++++------ crates/xmss/tests/xmss_tests.rs | 35 ++++++++++++++++++++++++------ 5 files changed, 67 insertions(+), 30 deletions(-) diff --git a/crates/xmss/src/lib.rs b/crates/xmss/src/lib.rs index b69698a6..72be2ed0 100644 --- a/crates/xmss/src/lib.rs +++ b/crates/xmss/src/lib.rs @@ -53,8 +53,13 @@ const _: () = assert!(1 + MESSAGE_EMBEDDING_LEN_FE <= POSEIDON1_WIDTH); // Domai pub(crate) const PRF_DOMAINSEP_WOTS_SECRET_KEY: u32 = 1000; pub(crate) const PRF_DOMAINSEP_PUBLIC_PARAM: u32 = 1001; pub(crate) const PRF_DOMAINSEP_RANDOM_NODE: u32 = 1002; +pub(crate) const PRF_DOMAINSEP_SIGNATURE_RANDOMNESS: u32 = 1003; pub(crate) const DOMAINSEP_MESSAGE_HASH: u32 = 1004; +/// Signing grinds the encoding randomness until the encoding is valid (expected: a few hundred +/// attempts); this bound only exists so a broken configuration fails instead of looping forever. +pub const MAX_SIGNING_ATTEMPTS: usize = 100_000; + /// Injective embedding of a message into `MESSAGE_EMBEDDING_LEN_FE` field elements: /// little-endian base-p decomposition of the message read as a little-endian integer /// (the same convention as leanSig's `encode_message`). diff --git a/crates/xmss/src/signers_cache.rs b/crates/xmss/src/signers_cache.rs index 7a5d9550..5aa05177 100644 --- a/crates/xmss/src/signers_cache.rs +++ b/crates/xmss/src/signers_cache.rs @@ -23,7 +23,7 @@ pub fn message_for_benchmark() -> [u8; MESSAGE_LEN_BYTES] { std::array::from_fn(|i| i as u8) } -const CACHE_SCHEMA_VERSION: u32 = 5; +const CACHE_SCHEMA_VERSION: u32 = 6; #[derive(Serialize, Deserialize)] struct SignersCacheFile { @@ -61,7 +61,7 @@ fn cache_path(first_pubkey: &XmssPublicKey) -> PathBuf { fn compute_signer(index: usize) -> (XmssPublicKey, XmssSignature) { let mut rng = StdRng::seed_from_u64(index as u64); let (pk, sk) = xmss_key_gen(&mut rng, BENCHMARK_SLOT as u64, 2).unwrap(); - let sig = xmss_sign(&mut rng, &sk, BENCHMARK_SLOT, &message_for_benchmark()).unwrap(); + let sig = xmss_sign(&sk, BENCHMARK_SLOT, &message_for_benchmark()).unwrap(); (pk, sig) } diff --git a/crates/xmss/src/wots.rs b/crates/xmss/src/wots.rs index 230d7db5..c230067e 100644 --- a/crates/xmss/src/wots.rs +++ b/crates/xmss/src/wots.rs @@ -114,22 +114,6 @@ pub fn iterate_hash( }) } -pub fn find_randomness_for_wots_encoding( - message: &[F; MESSAGE_LEN_FE], - slot: u32, - xmss_pub_key: &XmssPublicKey, - rng: &mut impl CryptoRng, -) -> (Randomness, [u8; V], usize) { - let mut num_iters = 0; - loop { - num_iters += 1; - let randomness = rng.random(); - if let Some(encoding) = wots_encode(message, slot, xmss_pub_key, &randomness) { - return (randomness, encoding, num_iters); - } - } -} - pub fn wots_encode( message: &[F; MESSAGE_LEN_FE], slot: u32, diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 0847ad79..756feb18 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -251,22 +251,42 @@ pub fn xmss_key_gen( #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum XmssSignatureError { SlotOutOfRange, + EncodingAttemptsExceeded, } impl std::fmt::Display for XmssSignatureError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::SlotOutOfRange => write!(f, "slot is outside the key's valid range"), + Self::EncodingAttemptsExceeded => { + write!(f, "no valid WOTS encoding found within {MAX_SIGNING_ATTEMPTS} attempts") + } } } } impl std::error::Error for XmssSignatureError {} -/// WARNING: XMSS is statefull signature, you should never sign with the same same `slot` twice. -/// (Even signing twice with the same message, at the same slot, is insecure, due to the non-determinism of the randomness part of the signature) -pub fn xmss_sign( - rng: &mut R, +/// Deterministic encoding randomness: a seed-keyed Poseidon PRF over (slot, attempt), mixed +/// with the hashed message by one more compression. +fn derive_signature_randomness( + seed: &[u8; 32], + slot: u32, + message_fe: &[F; MESSAGE_LEN_FE], + attempt: usize, +) -> Randomness { + let prf = poseidon_prf(PRF_DOMAINSEP_SIGNATURE_RANDOMNESS, seed, [slot as usize, attempt]); + let mut input = [F::ZERO; POSEIDON1_WIDTH]; + input[..DIGEST_LEN_FE].copy_from_slice(&prf); + input[DIGEST_LEN_FE..].copy_from_slice(message_fe); + poseidon16_compress(input)[..RANDOMNESS_LEN_FE].try_into().unwrap() +} + +/// WARNING: XMSS is a stateful signature scheme, never sign two different messages at the same +/// `slot`. Signing is derandomized (the encoding randomness is derived from the secret key, +/// slot, and message), so calling this twice with the same (slot, message) pair returns the +/// same signature and is harmless. +pub fn xmss_sign( secret_key: &XmssSecretKey, slot: u32, message: &[u8; MESSAGE_LEN_BYTES], @@ -275,8 +295,13 @@ pub fn xmss_sign( return Err(XmssSignatureError::SlotOutOfRange); } let message_fe = hash_message(message); - let (randomness, encoding, _) = - find_randomness_for_wots_encoding(&message_fe, slot, &secret_key.public_key(), rng); + let pub_key = secret_key.public_key(); + let (randomness, encoding) = (0..MAX_SIGNING_ATTEMPTS) + .find_map(|attempt| { + let randomness = derive_signature_randomness(&secret_key.seed, slot, &message_fe, attempt); + wots_encode(&message_fe, slot, &pub_key, &randomness).map(|encoding| (randomness, encoding)) + }) + .ok_or(XmssSignatureError::EncodingAttemptsExceeded)?; let wots_secret_key = gen_wots_secret_key(&secret_key.seed, slot, secret_key.public_param); let wots_signature = wots_secret_key.sign_with_encoding(randomness, &encoding, secret_key.public_param, slot); // Cache the bottom subtree covering `slot` (reused across its 2^split_level slots), then read the path. diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index af6f428d..79bafc8d 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -1,5 +1,5 @@ use backend::*; -use rand::{SeedableRng, rngs::StdRng}; +use rand::{RngExt, SeedableRng, rngs::StdRng}; use xmss::*; type F = KoalaBear; @@ -10,7 +10,7 @@ fn test_xmss_serialize_deserialize() { let slot = 110; let (pk, sk) = xmss_key_gen(&mut StdRng::seed_from_u64(0), 100, 16).unwrap(); - let sig = xmss_sign(&mut StdRng::seed_from_u64(slot as u64), &sk, slot, &message).unwrap(); + let sig = xmss_sign(&sk, slot, &message).unwrap(); let pk_bytes = postcard::to_allocvec(&pk).unwrap(); let pk2: XmssPublicKey = postcard::from_bytes(&pk_bytes).unwrap(); @@ -32,7 +32,7 @@ fn keygen_sign_verify() { let num_active_slots = (slot as u64 + 3).min(1 << LOG_LIFETIME) - activation_slot; let mut rng = StdRng::seed_from_u64(slot as u64); let (pk, sk) = xmss_key_gen(&mut rng, activation_slot, num_active_slots).unwrap(); - let sig = xmss_sign(&mut rng, &sk, slot, &message).unwrap(); + let sig = xmss_sign(&sk, slot, &message).unwrap(); xmss_verify(&pk, slot, &message, &sig).unwrap(); let mut other_message = message; @@ -41,6 +41,24 @@ fn keygen_sign_verify() { } } +#[test] +fn signing_is_deterministic() { + let message: [u8; MESSAGE_LEN_BYTES] = std::array::from_fn(|i| i as u8); + let (_, sk) = xmss_key_gen(&mut StdRng::seed_from_u64(7), 40, 10).unwrap(); + assert_eq!( + xmss_sign(&sk, 42, &message).unwrap(), + xmss_sign(&sk, 42, &message).unwrap() + ); + assert_eq!( + xmss_sign(&sk, 39, &message).unwrap_err(), + XmssSignatureError::SlotOutOfRange + ); + assert_eq!( + xmss_sign(&sk, 50, &message).unwrap_err(), + XmssSignatureError::SlotOutOfRange + ); +} + #[test] fn keygen_rejects_invalid_ranges() { let mut rng = StdRng::seed_from_u64(0); @@ -70,9 +88,14 @@ fn encoding_grinding_bits() { let message: [F; MESSAGE_LEN_FE] = Default::default(); let slot = i as u32; let mut rng = StdRng::seed_from_u64(i as u64); - let (_randomness, _encoding, num_iters) = - find_randomness_for_wots_encoding(&message, slot, &xmss_pub_key, &mut rng); - num_iters + let mut num_iters = 0; + loop { + num_iters += 1; + let randomness: [F; RANDOMNESS_LEN_FE] = rng.random(); + if wots_encode(&message, slot, &xmss_pub_key, &randomness).is_some() { + break num_iters; + } + } }, |a, b| a + b, ); From 2a0e85036781e7300a209b636985be1a4dcf7497 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:25 +0100 Subject: [PATCH 06/25] xmss: explicit prepare(slot) to warm the signing cache The first signature after crossing into a new bottom subtree pays an O(sqrt(R)) rebuild inside xmss_sign. XmssSecretKey::prepare(slot) lets callers do that work off the signing critical path; signing itself is unchanged (same lazy cache, now factored into cached_bottom_subtree). Co-Authored-By: Claude Fable 5 --- crates/xmss/src/xmss.rs | 26 ++++++++++++++++++++------ crates/xmss/tests/xmss_tests.rs | 17 +++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 756feb18..82ba8d64 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -304,12 +304,7 @@ pub fn xmss_sign( .ok_or(XmssSignatureError::EncodingAttemptsExceeded)?; let wots_secret_key = gen_wots_secret_key(&secret_key.seed, slot, secret_key.public_param); let wots_signature = wots_secret_key.sign_with_encoding(randomness, &encoding, secret_key.public_param, slot); - // Cache the bottom subtree covering `slot` (reused across its 2^split_level slots), then read the path. - let subtree_index = (slot as u64) >> secret_key.split_level; - let mut cache = secret_key.cache.lock().unwrap(); - if cache.as_ref().is_none_or(|s| s.subtree_index != subtree_index) { - *cache = Some(secret_key.build_bottom_subtree(subtree_index)); - } + let cache = secret_key.cached_bottom_subtree(slot); let sub = cache.as_ref().unwrap(); let merkle_proof = std::array::from_fn(|level| { let neighbour_index = ((slot as u64) >> level) ^ 1; @@ -330,6 +325,25 @@ impl XmssSecretKey { } } + /// Warms the signing cache for `slot`: when the next signing slot is known in advance, + /// calling this ahead of time makes the subsequent `xmss_sign` faster. + pub fn prepare(&self, slot: u32) -> Result<(), XmssSignatureError> { + if slot < self.slot_start || slot > self.slot_end { + return Err(XmssSignatureError::SlotOutOfRange); + } + drop(self.cached_bottom_subtree(slot)); + Ok(()) + } + + fn cached_bottom_subtree(&self, slot: u32) -> std::sync::MutexGuard<'_, Option> { + let subtree_index = (slot as u64) >> self.split_level; + let mut cache = self.cache.lock().unwrap(); + if cache.as_ref().is_none_or(|s| s.subtree_index != subtree_index) { + *cache = Some(self.build_bottom_subtree(subtree_index)); + } + cache + } + /// (Re)build the bottom subtree with the given index. Always sequential: signing must /// never wait on the thread pool while it holds the signing-cache mutex (a pool task /// blocked on the same key would deadlock the pool, and hence the signer). diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 79bafc8d..6f6e70c2 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -59,6 +59,23 @@ fn signing_is_deterministic() { ); } +#[test] +fn prepare_warms_the_signing_cache() { + let message: [u8; MESSAGE_LEN_BYTES] = std::array::from_fn(|i| i as u8); + let (pk, sk) = xmss_key_gen(&mut StdRng::seed_from_u64(3), 1000, 300).unwrap(); + + // Signatures are unaffected by preparation (it only warms the cache). + let cold = xmss_sign(&sk, 1299, &message).unwrap(); + sk.prepare(1000).unwrap(); // different subtree: rebuilds the cache + sk.prepare(1299).unwrap(); // back again + let warm = xmss_sign(&sk, 1299, &message).unwrap(); + assert_eq!(cold, warm); + xmss_verify(&pk, 1299, &message, &warm).unwrap(); + + assert_eq!(sk.prepare(999).unwrap_err(), XmssSignatureError::SlotOutOfRange); + assert_eq!(sk.prepare(1300).unwrap_err(), XmssSignatureError::SlotOutOfRange); +} + #[test] fn keygen_rejects_invalid_ranges() { let mut rng = StdRng::seed_from_u64(0); From e4deb1157c717a55daead2b3f50db90f9ea4b77b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:25 +0100 Subject: [PATCH 07/25] xmss: serde persistence for XmssSecretKey Serializes (seed, slot range, top tree): storing the top tree makes reload cheap (no re-hashing of the key's whole range), while the derived fields are recomputed and the tree shape is revalidated against the range on deserialization. The bottom-subtree cache restarts empty. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/xmss.rs | 50 +++++++++++++++++++++++++++++++++ crates/xmss/tests/xmss_tests.rs | 12 ++++++++ 2 files changed, 62 insertions(+) diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 82ba8d64..5fbdcfb1 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -29,6 +29,22 @@ pub(crate) struct BottomSubtree { layers: Vec>, } +/// Persists (seed, slot range, top tree). The top tree is stored so that loading a key is +/// cheap (no re-hashing of the whole range); the derived fields (public_param, split_level) +/// are recomputed and the tree shape revalidated. The bottom-subtree cache restarts empty. +impl Serialize for XmssSecretKey { + fn serialize(&self, s: S) -> Result { + (&self.seed, self.slot_start, self.slot_end, &self.top).serialize(s) + } +} + +impl<'de> Deserialize<'de> for XmssSecretKey { + fn deserialize>(d: D) -> Result { + let (seed, slot_start, slot_end, top) = <([u8; 32], u32, u32, Vec>)>::deserialize(d)?; + Self::from_parts(seed, slot_start, slot_end, top).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct XmssSignature { pub wots_signature: WotsSignature, @@ -335,6 +351,40 @@ impl XmssSecretKey { Ok(()) } + /// Rebuild a secret key from its persisted parts, recomputing the derived fields and + /// revalidating the top tree's shape (its content is trusted, exactly like the seed). + pub(crate) fn from_parts( + seed: [u8; 32], + slot_start: u32, + slot_end: u32, + top: Vec>, + ) -> Result { + if slot_start > slot_end { + return Err("invalid slot range"); + } + let (lo, hi) = (slot_start as u64, slot_end as u64); + let split_level = log2_ceil_usize((hi - lo + 1) as usize).div_ceil(2); + let expected_layer_lens = + (split_level..=LOG_LIFETIME).map(|level| ((hi >> level) - (lo >> level) + 1) as usize); + if top.len() != LOG_LIFETIME - split_level + 1 + || top + .iter() + .zip(expected_layer_lens) + .any(|(layer, len)| layer.len() != len) + { + return Err("top tree shape does not match the slot range"); + } + Ok(Self { + slot_start, + slot_end, + public_param: gen_public_param(&seed), + seed, + split_level, + top, + cache: Mutex::new(None), + }) + } + fn cached_bottom_subtree(&self, slot: u32) -> std::sync::MutexGuard<'_, Option> { let subtree_index = (slot as u64) >> self.split_level; let mut cache = self.cache.lock().unwrap(); diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 6f6e70c2..ea22cced 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -21,6 +21,18 @@ fn test_xmss_serialize_deserialize() { assert_eq!(sig, sig2); xmss_verify(&pk2, slot, &message, &sig2).unwrap(); + + // Secret key: persist and reload, then check the reloaded key behaves identically. + let sk_bytes = postcard::to_allocvec(&sk).unwrap(); + let sk2: XmssSecretKey = postcard::from_bytes(&sk_bytes).unwrap(); + assert_eq!(sk2.public_key(), pk); + assert_eq!(xmss_sign(&sk2, slot, &message).unwrap(), sig); + + // A top tree whose shape does not match the slot range must be rejected. + let (seed, start, end, mut top): ([u8; 32], u32, u32, Vec>) = postcard::from_bytes(&sk_bytes).unwrap(); + top.pop(); + let corrupted = postcard::to_allocvec(&(seed, start, end, top)).unwrap(); + assert!(postcard::from_bytes::(&corrupted).is_err()); } #[test] From 8eccadfeaffb0bc71bbdab27a1ddcbd7f50a626f Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:25 +0100 Subject: [PATCH 08/25] xmss: SSZ Encode/Decode for public key and signature Manual ethereum_ssz implementations; field elements are canonical u32 little-endian, rejected on decode when non-canonical. - XmssPublicKey: fixed 32 bytes (root | public_param) - XmssSignature: fixed 1208 bytes (chain_tips | randomness | merkle_proof) Only these two appear in consensus objects. The secret key deliberately gets no SSZ: it never crosses the consensus layer, serde covers local persistence, and a future keystore standard (a la EIP-2335 for BLS, which encrypts the raw scalar, not an SSZ object) should own the canonical secret-key format - for this scheme the minimal secret material is just the 32-byte seed and the activation range. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 2231 +++++++++++++++++++++++--- Cargo.toml | 1 + crates/xmss/Cargo.toml | 1 + crates/xmss/src/lib.rs | 2 + crates/xmss/src/ssz_serialization.rs | 143 ++ crates/xmss/tests/xmss_tests.rs | 31 + 6 files changed, 2202 insertions(+), 207 deletions(-) create mode 100644 crates/xmss/src/ssz_serialization.rs diff --git a/Cargo.lock b/Cargo.lock index 15cb51f3..9e393130 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,54 @@ dependencies = [ "poly", ] +[[package]] +name = "alloy-primitives" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f007e257069855bdf21d27762fd3f3705a613f805c9a08309bf353503f081d71" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "fixed-cache", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.5", + "rapidhash", + "ruint", + "rustc-hash", + "secp256k1", + "serde", + "sha3", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" +dependencies = [ + "arrayvec", + "bytes", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -85,331 +133,1314 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "atomic-polyfill" -version = "1.0.3" +name = "ark-ff" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" dependencies = [ - "critical-section", + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", ] [[package]] -name = "backend" -version = "0.1.0" -dependencies = [ - "air", - "fiat-shamir", - "field", - "koala-bear", - "parallel", - "poly", - "sumcheck", - "symetric", - "utils", - "whir", - "zk-alloc", +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", ] [[package]] -name = "bitflags" -version = "2.12.1" +name = "ark-ff" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] [[package]] -name = "block-buffer" -version = "0.10.4" +name = "ark-ff" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" dependencies = [ - "generic-array", + "ark-ff-asm 0.6.0", + "ark-ff-macros 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "educe", + "num-bigint", + "num-traits", + "zeroize", ] [[package]] -name = "byteorder" -version = "1.5.0" +name = "ark-ff-asm" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] [[package]] -name = "cfg-if" -version = "1.0.4" +name = "ark-ff-asm" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] [[package]] -name = "chacha20" -version = "0.10.0" +name = "ark-ff-asm" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core", + "quote", + "syn 2.0.117", ] [[package]] -name = "clap" -version = "4.6.1" +name = "ark-ff-asm" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" dependencies = [ - "clap_builder", - "clap_derive", + "quote", + "syn 2.0.117", ] [[package]] -name = "clap_builder" -version = "4.6.0" +name = "ark-ff-macros" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", ] [[package]] -name = "clap_derive" -version = "4.6.1" +name = "ark-ff-macros" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "heck", + "num-bigint", + "num-traits", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] -name = "clap_lex" -version = "1.1.0" +name = "ark-ff-macros" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "cobs" +name = "ark-ff-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-serialize" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" dependencies = [ - "thiserror", + "ark-std 0.3.0", + "digest 0.9.0", ] [[package]] -name = "colorchoice" -version = "1.0.5" +name = "ark-serialize" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "ark-serialize" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" dependencies = [ - "libc", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", ] [[package]] -name = "cpufeatures" +name = "ark-serialize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.6.0", + "digest 0.10.7", + "num-bigint", + "serde_with", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-std" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ - "libc", + "num-traits", + "rand 0.8.7", ] [[package]] -name = "critical-section" -version = "1.2.0" +name = "ark-std" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.7", +] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "ark-std" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ - "generic-array", - "typenum", + "num-traits", + "rand 0.8.7", ] [[package]] -name = "digest" -version = "0.10.7" +name = "ark-std" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" dependencies = [ - "block-buffer", - "crypto-common", + "num-traits", + "rand 0.8.7", ] [[package]] -name = "embedded-io" -version = "0.4.0" +name = "arrayvec" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] -name = "embedded-io" -version = "0.6.1" +name = "atomic-polyfill" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] [[package]] -name = "equivalent" -version = "1.0.2" +name = "auto_impl" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "fiat-shamir" +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backend" version = "0.1.0" dependencies = [ + "air", + "fiat-shamir", "field", "koala-bear", "parallel", - "serde", + "poly", + "sumcheck", "symetric", "utils", + "whir", + "zk-alloc", ] [[package]] -name = "field" -version = "0.1.0" +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" dependencies = [ - "parallel", - "paste", - "rand", + "bitcoin-internals", + "hex-conservative 1.2.0", "serde", - "utils", ] [[package]] -name = "foldhash" -version = "0.1.5" +name = "bitcoin-internals" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" [[package]] -name = "generic-array" -version = "0.14.7" +name = "bitcoin-io" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" dependencies = [ - "typenum", - "version_check", + "bitcoin-consensus-encoding", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "bitcoin_hashes" +version = "0.14.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "rand_core", - "wasip2", - "wasip3", + "bitcoin-io", + "hex-conservative 0.2.2", ] [[package]] -name = "hash32" -version = "0.2.1" +name = "bitflags" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ - "byteorder", + "funty", + "radium", + "tap", + "wyz", ] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "foldhash", + "generic-array", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "block-buffer" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] [[package]] -name = "heapless" -version = "0.7.17" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin", - "stable_deref_trait", + "tinyvec", ] [[package]] -name = "heck" -version = "0.5.0" +name = "bumpalo" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] -name = "id-arena" -version = "2.3.0" +name = "byte-slice-cast" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "enum-ordinalize" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "ethereum_serde_utils" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38df44a7a271ab43835678f9215b53cc2523e4714a215da6643d83dc110245da" +dependencies = [ + "alloy-primitives", + "hex", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "ethereum_ssz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e462875ad8693755ea8913d6e905715c76ea4836e2254e18c9cf0f7a8f8c2a13" +dependencies = [ + "alloy-primitives", + "ethereum_serde_utils", + "itertools 0.14.0", + "serde", + "serde_derive", + "smallvec", + "typenum", +] + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-shamir" +version = "0.1.0" +dependencies = [ + "field", + "koala-bear", + "parallel", + "serde", + "symetric", + "utils", +] + +[[package]] +name = "field" +version = "0.1.0" +dependencies = [ + "parallel", + "paste", + "rand 0.10.1", + "serde", + "utils", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-cache" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fe63500644ef0269fe6b744e7e5dc5c20b5eebf3d881bc2be53f194636f6583" +dependencies = [ + "equivalent", + "rapidhash", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.7", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version 0.4.1", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "include_dir" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +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 = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ - "include_dir_macros", + "either", ] [[package]] -name = "include_dir_macros" -version = "0.7.4" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "proc-macro2", - "quote", + "cfg-if", + "futures-util", + "wasm-bindgen", ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "k256" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "keccak" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] [[package]] -name = "itoa" -version = "1.0.18" +name = "keccak-asm" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "dd5dc2c0d691cbf7595cde551ced329cca99c2387c2cbc97754c5d0cd045d3ee" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] [[package]] name = "koala-bear" @@ -417,11 +1448,26 @@ version = "0.1.0" dependencies = [ "field", "paste", - "rand", + "rand 0.10.1", "serde", "utils", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy_static" version = "1.5.0" @@ -435,7 +1481,7 @@ dependencies = [ "backend", "clap", "lean_vm", - "rand", + "rand 0.10.1", "rec_aggregation", "serde_json", "sub_protocols", @@ -453,7 +1499,7 @@ dependencies = [ "lean_vm", "pest", "pest_derive", - "rand", + "rand 0.10.1", "sub_protocols", "xmss", ] @@ -465,7 +1511,7 @@ dependencies = [ "backend", "lean_compiler", "lean_vm", - "rand", + "rand 0.10.1", "rec_aggregation", "serde", "serde_json", @@ -495,6 +1541,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "lock_api" version = "0.4.14" @@ -534,6 +1586,41 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "objc2" version = "0.6.4" @@ -578,6 +1665,34 @@ dependencies = [ "system-info", ] +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "paste" version = "1.0.15" @@ -614,7 +1729,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -633,6 +1748,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "poly" version = "0.1.0" @@ -640,7 +1765,7 @@ dependencies = [ "field", "koala-bear", "parallel", - "rand", + "rand 0.10.1", "serde", "system-info", "utils", @@ -660,6 +1785,21 @@ dependencies = [ "serde", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -667,7 +1807,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", ] [[package]] @@ -679,6 +1839,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + [[package]] name = "quote" version = "1.0.45" @@ -688,12 +1863,46 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", +] + [[package]] name = "rand" version = "0.10.1" @@ -701,8 +1910,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom", - "rand_core", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", + "serde", ] [[package]] @@ -711,6 +1959,24 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", +] + [[package]] name = "rec_aggregation" version = "0.1.0" @@ -724,42 +1990,211 @@ dependencies = [ "objc2-foundation", "postcard", "serde", - "sub_protocols", - "tracing", - "xmss", + "sub_protocols", + "tracing", + "xmss", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "ruint" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45caf26f647c19115bf9c453c70ffe4a4a3a6390dceebd942610584f99b8ddce" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "ark-ff 0.6.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.7", + "rand 0.9.5", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] -name = "regex-automata" -version = "0.4.14" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", ] [[package]] -name = "regex-syntax" -version = "0.8.10" +name = "secp256k1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.5", + "secp256k1-sys", +] [[package]] -name = "rustc_version" -version = "0.4.1" +name = "secp256k1-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" dependencies = [ - "semver", + "cc", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "semver" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] [[package]] name = "semver" @@ -767,6 +2202,15 @@ version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.228" @@ -794,7 +2238,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -810,6 +2254,25 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "time", +] + [[package]] name = "sha2" version = "0.10.9" @@ -818,7 +2281,27 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + +[[package]] +name = "sha3-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6287fd675f713484342a89cbf0a386abef5f15919cfad607e5e1f19e1e15331" +dependencies = [ + "cc", + "cfg-if", ] [[package]] @@ -830,6 +2313,28 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -845,12 +2350,28 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -864,10 +2385,16 @@ dependencies = [ "backend", "lean_prover", "lean_vm", - "rand", + "rand 0.10.1", "tracing", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "sumcheck" version = "0.1.0" @@ -892,6 +2419,17 @@ dependencies = [ "zk-alloc", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -903,6 +2441,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "system-info" version = "0.1.0" @@ -910,6 +2459,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "thiserror" version = "2.0.18" @@ -927,7 +2482,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -939,6 +2494,81 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.44" @@ -958,7 +2588,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1025,12 +2655,36 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -1065,6 +2719,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" @@ -1083,6 +2743,51 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1100,7 +2805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -1113,8 +2818,8 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", - "semver", + "indexmap 2.14.0", + "semver 1.0.28", ] [[package]] @@ -1126,7 +2831,7 @@ dependencies = [ "koala-bear", "parallel", "poly", - "rand", + "rand 0.10.1", "sumcheck", "symetric", "system-info", @@ -1159,12 +2864,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1174,6 +2932,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -1208,9 +2975,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.14.0", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1226,7 +2993,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -1239,7 +3006,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -1258,9 +3025,9 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", - "semver", + "semver 1.0.28", "serde", "serde_derive", "serde_json", @@ -1268,16 +3035,66 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xmss" version = "0.1.0" dependencies = [ "backend", + "ethereum_ssz", "postcard", - "rand", + "rand 0.10.1", "serde", ] +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zk-alloc" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ab668271..334b8305 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ serde = { version = "1.0.228", features = ["derive"] } tracing-subscriber = { version = "0.3.23", features = ["std", "env-filter"] } tracing-forest = { version = "0.3.0", features = ["ansi", "smallvec"] } postcard = { version = "1.1.3", features = ["alloc"] } +ssz = { package = "ethereum_ssz", version = "0.10" } include_dir = "0.7" libc = "0.2" paste = "1" diff --git a/crates/xmss/Cargo.toml b/crates/xmss/Cargo.toml index d0a1d0ed..9900d69b 100644 --- a/crates/xmss/Cargo.toml +++ b/crates/xmss/Cargo.toml @@ -11,6 +11,7 @@ rand.workspace = true backend.workspace = true serde.workspace = true postcard.workspace = true +ssz.workspace = true [dev-dependencies] postcard.workspace = true \ No newline at end of file diff --git a/crates/xmss/src/lib.rs b/crates/xmss/src/lib.rs index 72be2ed0..7f2a7815 100644 --- a/crates/xmss/src/lib.rs +++ b/crates/xmss/src/lib.rs @@ -2,6 +2,8 @@ use backend::{DIGEST_LEN_FE, KoalaBear, POSEIDON1_WIDTH, PrimeCharacteristicRing, PrimeField32, poseidon16_compress}; pub mod signers_cache; +mod ssz_serialization; +pub use ssz_serialization::{PUB_KEY_SSZ_LEN, SIGNATURE_SSZ_LEN}; mod wots; pub use wots::*; mod xmss; diff --git a/crates/xmss/src/ssz_serialization.rs b/crates/xmss/src/ssz_serialization.rs new file mode 100644 index 00000000..5b370ce3 --- /dev/null +++ b/crates/xmss/src/ssz_serialization.rs @@ -0,0 +1,143 @@ +//! SSZ encodings (Ethereum consensus-layer compatibility) for the public key and the +//! signature — the two objects that appear in consensus data. The secret key never does, +//! so it only gets serde persistence. Field elements are serialized as their canonical +//! u32, little-endian; non-canonical values are rejected on decode. +//! +//! - `XmssPublicKey`: fixed length, `merkle_root | public_param` (32 bytes). +//! - `XmssSignature`: fixed length, `chain_tips | randomness | merkle_proof` (1208 bytes). + +use backend::{PrimeCharacteristicRing, PrimeField32}; +use ssz::{Decode, DecodeError, Encode}; + +use crate::*; + +const FE_BYTES: usize = 4; +const DIGEST_BYTES: usize = XMSS_DIGEST_LEN * FE_BYTES; + +/// SSZ length of an encoded public key: 32 bytes. +pub const PUB_KEY_SSZ_LEN: usize = PUB_KEY_FLAT_SIZE * FE_BYTES; +/// SSZ length of an encoded signature: 1208 bytes. +pub const SIGNATURE_SSZ_LEN: usize = (WOTS_SIG_SIZE_FE + LOG_LIFETIME * XMSS_DIGEST_LEN) * FE_BYTES; + +fn append_fes(buf: &mut Vec, fes: &[F]) { + for fe in fes { + buf.extend_from_slice(&fe.as_canonical_u32().to_le_bytes()); + } +} + +/// Parses `N` field elements, rejecting non-canonical (>= p) encodings. +fn read_fes(bytes: &[u8]) -> Result<[F; N], DecodeError> { + debug_assert_eq!(bytes.len(), N * FE_BYTES); + let mut out = [F::ZERO; N]; + for (fe, chunk) in out.iter_mut().zip(bytes.chunks_exact(FE_BYTES)) { + let value = u32::from_le_bytes(chunk.try_into().unwrap()); + if value >= F::ORDER_U32 { + return Err(DecodeError::BytesInvalid(format!( + "non-canonical field element: {value}" + ))); + } + *fe = F::from_u32(value); + } + Ok(out) +} + +fn check_len(bytes: &[u8], expected: usize) -> Result<(), DecodeError> { + if bytes.len() != expected { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected, + }); + } + Ok(()) +} + +impl Encode for XmssPublicKey { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + PUB_KEY_SSZ_LEN + } + + fn ssz_bytes_len(&self) -> usize { + PUB_KEY_SSZ_LEN + } + + fn ssz_append(&self, buf: &mut Vec) { + append_fes(buf, &self.merkle_root); + append_fes(buf, &self.public_param); + } +} + +impl Decode for XmssPublicKey { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + PUB_KEY_SSZ_LEN + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + check_len(bytes, PUB_KEY_SSZ_LEN)?; + Ok(Self { + merkle_root: read_fes(&bytes[..XMSS_DIGEST_LEN * FE_BYTES])?, + public_param: read_fes(&bytes[XMSS_DIGEST_LEN * FE_BYTES..])?, + }) + } +} + +impl Encode for XmssSignature { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + SIGNATURE_SSZ_LEN + } + + fn ssz_bytes_len(&self) -> usize { + SIGNATURE_SSZ_LEN + } + + fn ssz_append(&self, buf: &mut Vec) { + for chain_tip in &self.wots_signature.chain_tips { + append_fes(buf, chain_tip); + } + append_fes(buf, &self.wots_signature.randomness); + for neighbour in &self.merkle_proof { + append_fes(buf, neighbour); + } + } +} + +impl Decode for XmssSignature { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + SIGNATURE_SSZ_LEN + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + check_len(bytes, SIGNATURE_SSZ_LEN)?; + let mut digests = bytes.chunks_exact(DIGEST_BYTES); + let mut chain_tips = [[F::ZERO; XMSS_DIGEST_LEN]; V]; + for chain_tip in &mut chain_tips { + *chain_tip = read_fes(digests.next().unwrap())?; + } + let randomness_start = V * DIGEST_BYTES; + let randomness = read_fes(&bytes[randomness_start..randomness_start + RANDOMNESS_LEN_FE * FE_BYTES])?; + let mut digests = bytes[randomness_start + RANDOMNESS_LEN_FE * FE_BYTES..].chunks_exact(DIGEST_BYTES); + let mut merkle_proof = [[F::ZERO; XMSS_DIGEST_LEN]; LOG_LIFETIME]; + for neighbour in &mut merkle_proof { + *neighbour = read_fes(digests.next().unwrap())?; + } + Ok(Self { + wots_signature: WotsSignature { chain_tips, randomness }, + merkle_proof, + }) + } +} diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index ea22cced..600e63f0 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -102,6 +102,37 @@ fn keygen_rejects_invalid_ranges() { ); } +#[test] +fn ssz_roundtrip() { + use ssz::{Decode, Encode}; + + let message: [u8; MESSAGE_LEN_BYTES] = std::array::from_fn(|i| i as u8); + let slot = 110; + let (pk, sk) = xmss_key_gen(&mut StdRng::seed_from_u64(1), 100, 16).unwrap(); + let sig = xmss_sign(&sk, slot, &message).unwrap(); + + // Public key: fixed 32 bytes. + let pk_bytes = pk.as_ssz_bytes(); + assert_eq!(pk_bytes.len(), PUB_KEY_SSZ_LEN); + assert_eq!(pk_bytes.len(), 32); + assert_eq!(XmssPublicKey::from_ssz_bytes(&pk_bytes).unwrap(), pk); + + // Signature: fixed 1208 bytes. + let sig_bytes = sig.as_ssz_bytes(); + assert_eq!(sig_bytes.len(), SIGNATURE_SSZ_LEN); + assert_eq!(sig_bytes.len(), 1208); + assert_eq!(XmssSignature::from_ssz_bytes(&sig_bytes).unwrap(), sig); + + // Non-canonical field elements are rejected. + let mut bad_pk = pk_bytes.clone(); + bad_pk[..4].copy_from_slice(&u32::MAX.to_le_bytes()); + assert!(XmssPublicKey::from_ssz_bytes(&bad_pk).is_err()); + + // Wrong lengths are rejected. + assert!(XmssPublicKey::from_ssz_bytes(&pk_bytes[1..]).is_err()); + assert!(XmssSignature::from_ssz_bytes(&sig_bytes[..SIGNATURE_SSZ_LEN - 1]).is_err()); +} + #[test] #[ignore] fn encoding_grinding_bits() { From 14aef12c581bc11c83dbde9b6a210b98ea1f9c63 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 08:36:25 +0100 Subject: [PATCH 09/25] tests: migrate root multisignature tests to the new xmss API Missed in the per-commit migration because cargo check without --all-targets skips the root integration tests. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/lib.rs | 2 +- tests/test_multisignatures.rs | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/xmss/src/lib.rs b/crates/xmss/src/lib.rs index 7f2a7815..71c70565 100644 --- a/crates/xmss/src/lib.rs +++ b/crates/xmss/src/lib.rs @@ -47,7 +47,7 @@ pub const TWEAK_TYPE_ENCODING: usize = 3; const _: () = assert!(V.is_multiple_of(2)); // For efficiency of the snark (we can batch chains in pairs) const _: () = assert!(MESSAGE_EMBEDDING_LEN_FE * 30 >= MESSAGE_LEN_BYTES * 8); // Injective embedding const _: () = assert!(MESSAGE_LEN_FE == DIGEST_LEN_FE); // hash_message output is one Poseidon digest -const _: () = assert!(1 + MESSAGE_EMBEDDING_LEN_FE <= POSEIDON1_WIDTH); // Domain sep + embedding fit +const _: () = assert!(MESSAGE_EMBEDDING_LEN_FE < POSEIDON1_WIDTH); // Domain sep + embedding fit // Domain separators, placed in the first lane of a poseidon16_compress input. The window // [336, 1024) is collision-free with every tweak first lane: chain tweaks with index_hi = 0 diff --git a/tests/test_multisignatures.rs b/tests/test_multisignatures.rs index a3c5eee2..1468d2f8 100644 --- a/tests/test_multisignatures.rs +++ b/tests/test_multisignatures.rs @@ -12,6 +12,7 @@ use rec_aggregation::{ split_multi_message_aggregate_by_message, }; use xmss::{ + hash_message, signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}, xmss_key_gen, xmss_sign, xmss_verify, }; @@ -24,15 +25,15 @@ fn serialize_arena_tests() -> MutexGuard<'static, ()> { #[test] fn test_xmss_signature() { - let start_slot = 111; - let end_slot = 200; + let activation_slot = 111; + let num_active_slots = 90; let slot: u32 = 124; let mut rng: StdRng = StdRng::seed_from_u64(0); - let message = rng.random(); + let message: [u8; 32] = rng.random(); - let (secret_key, pub_key) = xmss_key_gen(rng.random(), start_slot, end_slot, false).unwrap(); - let signature = xmss_sign(&mut rng, &secret_key, &message, slot).unwrap(); - xmss_verify(&pub_key, &message, &signature, slot).unwrap(); + let (pub_key, secret_key) = xmss_key_gen(&mut rng, activation_slot, num_active_slots).unwrap(); + let signature = xmss_sign(&secret_key, slot, &message).unwrap(); + xmss_verify(&pub_key, slot, &message, &signature).unwrap(); } #[test] @@ -55,7 +56,7 @@ fn test_single_message_aggregation() { setup_prover(); let log_inv_rate = 2; // [1, 2, 3 or 4] (lower = faster but bigger proofs) - let message = message_for_benchmark(); + let message = hash_message(&message_for_benchmark()); let slot: u32 = BENCHMARK_SLOT; let signatures = get_benchmark_signatures(); @@ -89,20 +90,21 @@ fn test_multi_message_aggregation() { let log_inv_rate = 2; // [1, 2, 3 or 4] (lower = faster but bigger proofs) let slot_a = BENCHMARK_SLOT; - let message_a = message_for_benchmark(); + let message_a = hash_message(&message_for_benchmark()); let signatures = get_benchmark_signatures(); let raws_a = signatures[0..3].to_vec(); let slot_b = BENCHMARK_SLOT + 1; let mut rng_b: StdRng = StdRng::seed_from_u64(17); - let message_b: [_; 8] = std::array::from_fn(|_| rng_b.random()); + let message_b_bytes: [u8; 32] = rng_b.random(); + let message_b = hash_message(&message_b_bytes); assert!(message_b != message_a && slot_b != slot_a); let raws_b: Vec<_> = (0..2) .map(|_| { - let (sk, pk) = xmss_key_gen(rng_b.random(), slot_b, slot_b, false).unwrap(); - let sig = xmss_sign(&mut rng_b, &sk, &message_b, slot_b).unwrap(); + let (pk, sk) = xmss_key_gen(&mut rng_b, u64::from(slot_b), 1).unwrap(); + let sig = xmss_sign(&sk, slot_b, &message_b_bytes).unwrap(); (pk, sig) }) .collect(); From fb8ee1fa0a6f6739925404057b4120a350944959 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 20:10:49 +0100 Subject: [PATCH 10/25] tests: concurrent signing on a shared xmss key Pool tasks and a plain thread signing with one shared key: exercises the cache-mutex contention paths (sequential subtree builds under the lock, so no signer ever waits on the pool while holding it) and checks that derandomization yields identical signatures from either context. Co-Authored-By: Claude Fable 5 --- crates/xmss/tests/xmss_tests.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 600e63f0..4e782a8e 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -88,6 +88,35 @@ fn prepare_warms_the_signing_cache() { assert_eq!(sk.prepare(1300).unwrap_err(), XmssSignatureError::SlotOutOfRange); } +/// One key shared between signers inside pool tasks and on a plain thread. Bottom-subtree +/// builds during signing are always sequential, so a signer holding the cache mutex never +/// waits on the pool (which would deadlock with the pool tasks blocked on the same key). +/// Derandomization makes the signatures identical regardless of who produced them. +#[test] +fn concurrent_signing_on_shared_key() { + let message: [u8; MESSAGE_LEN_BYTES] = std::array::from_fn(|i| i as u8); + let (pk, sk) = xmss_key_gen(&mut StdRng::seed_from_u64(9), 0, 1 << 10).unwrap(); + let (pk, sk) = (&pk, &sk); + + std::thread::scope(|scope| { + let plain_thread = scope.spawn(move || { + (0..1024u32) + .step_by(97) + .map(|slot| (slot, xmss_sign(sk, slot, &message).unwrap())) + .collect::>() + }); + parallel::for_each_index(64, |i| { + let slot = (i * 61 % 1024) as u32; + let sig = xmss_sign(sk, slot, &message).unwrap(); + xmss_verify(pk, slot, &message, &sig).unwrap(); + }); + for (slot, sig) in plain_thread.join().unwrap() { + xmss_verify(pk, slot, &message, &sig).unwrap(); + assert_eq!(sig, xmss_sign(sk, slot, &message).unwrap()); + } + }); +} + #[test] fn keygen_rejects_invalid_ranges() { let mut rng = StdRng::seed_from_u64(0); From 97b345a54da98debf9a7f92bc41d7d732b59ff4b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 22:49:55 +0100 Subject: [PATCH 11/25] xmss: update spec for the new API --- crates/xmss/xmss.md | 56 ++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/crates/xmss/xmss.md b/crates/xmss/xmss.md index e1538e1c..5bffdb6d 100644 --- a/crates/xmss/xmss.md +++ b/crates/xmss/xmss.md @@ -6,16 +6,26 @@ KoalaBear (p = 2^31 - 2^24 + 1). ## Hash function -[Poseidon](https://eprint.iacr.org/2019/458), in compression mode (feedforward addition). Input: 16 field elements. Output: 8 field elements. We denote it `H`. Chain hashes, Merkle hashes, and the final WOTS-pubkey hash truncate the output to 4 field elements (`n`); the encoding step and the intermediate WOTS-pubkey sponge states keep the full 8 elements. +[Poseidon](https://eprint.iacr.org/2019/458), in compression mode (feedforward addition). Input: 16 field elements. Output: 8 field elements. We denote it `H`. Chain hashes, Merkle hashes, and the final WOTS-pubkey hash truncate the output to 4 field elements (`n`); the message hash, the encoding step, and the intermediate WOTS-pubkey sponge states keep the full 8 elements. -## Sizes (in field elements) +## Sizes -- `n = 4`: digest size -- `|pp| = 4`: public parameter -- `|randomness| = 6`: signature randomness -- `|msg| = 8`: message size +- `n = 4`: digest size (field elements) +- `|pp| = 4`: public parameter (field elements) +- `|randomness| = 6`: signature randomness (field elements) +- `|msg| = 32` bytes: raw message size (embedded into 9 field elements, hashed to 8, see below) - `|tweak| = 2`: tweak (domain separation: `encoding`, `chain`, `wots_pk`, `merkle`) +## Message hashing (off-circuit) + +The signed message is 32 raw bytes. It is embedded injectively into 9 field elements, then hashed with a domain separator to 8 field elements: + +`msg_fe = H(1004 | limb_0 … limb_8 | 0^6)` + +Everything below (the WOTS encoding, and hence the snark) operates on `msg_fe`; this hash is computed off-circuit. + +Domain separators (first Poseidon lane): 1000 = WOTS secret key PRF, 1001 = public parameter PRF, 1002 = random filler node PRF, 1003 = signature randomness PRF, 1004 = message hash. The window [336, 1024) cannot collide with any tweak first lane. + ## WOTS (Winternitz One Time Signature) - `v = 42`: number of hash chains @@ -26,23 +36,37 @@ KoalaBear (p = 2^31 - 2^24 + 1). `log_lifetime = 32`: a key is valid for up to `2^32` slots. `log_lifetime` corresponds to the Merkle tree height. +## Signing (derandomized) + +To sign `msg` at `slot`: + +1. `msg_fe = hash_message(msg)`. +2. For `attempt = 0, 1, ...`: `randomness = H(H(1003 | seed | slot | attempt) | msg_fe)`. Keep the first attempt whose WOTS encoding (step 2 of verification) is valid. +3. Walk chain `i` from its secret pre-image for `e_i` steps; the signature is `(randomness, chain_tips, merkle_proof)`. + +Signing is deterministic: re-signing the same `(slot, msg)` pair returns the identical signature (and is therefore harmless). Signing two *different* messages at the same slot remains forbidden — XMSS is a stateful, synchronized scheme. + +The first signature after crossing into a new bottom subtree rebuilds an `O(sqrt(R))` cache; `XmssSecretKey::prepare(slot)` can do this ahead of time, off the signing critical path. + ## Verification -Inputs: public key `(merkle_root, pp)`, message `msg`, slot `s`, signature `(randomness, chain_tips, merkle_proof)`. +Inputs: public key `(merkle_root, pp)`, 32-byte message `msg`, slot `s`, signature `(randomness, chain_tips, merkle_proof)`. + +1. **Hash message** (off-circuit): `msg_fe = hash_message(msg)`. +2. **Encode**: compute the 8-limb digest `D = H(H(msg_fe | randomness | tweak_encoding(s)) | pp | 0000)`. For each limb `D_i`, take the canonical representative `D_i = low + 2^24 · high` (with `low ∈ [0, 2^24)`, `high ∈ [0, 128)`) and reject if `high == 127` (equivalently `D_i == −1`). This guarantees an uniform encoding. Concatenate the 24-bit `low` parts of the 8 limbs in little-endian order to get 192 bits, then take the first `v · w = 126` bits split into `v = 42` little-endian chunks of `w = 3` bits → encoding `(e_0, ..., e_{v-1})` with each `e_i ∈ [0, chain_length)`. Reject if `sum(e_i) ≠ target_sum`. +3. **Recover WOTS public key**: for each `i`, walk chain `i` from `chain_tips[i]` for `chain_length - 1 - e_i` steps, where each step is `H(tweak_chain(i, step, s) | 00 | previous_value | pp | 0000)` truncated to `n`. +4. **Hash WOTS public key**: T-sponge with replacement over the `v` recovered chain ends, with IV `[tweak_wots_pk(s) | 00 | pp]`, ingesting two chain end digests at a time. Output is the Merkle leaf. +5. **Walk Merkle path**: for `level = 0..log_lifetime`, combine the current node with `merkle_proof[level]` (left/right determined by bit `level` of `s`) via `H(tweak_merkle(level+1, parent_index) | 00 | pp | left | right)` truncated to `n`. +6. **Check root**: accept iff the final hash equals `merkle_root`. -1. **Encode**: compute the 8-limb digest `D = H(H(msg | randomness | tweak_encoding(s)) | pp | 0000)`. For each limb `D_i`, take the canonical representative `D_i = low + 2^24 · high` (with `low ∈ [0, 2^24)`, `high ∈ [0, 128)`) and reject if `high == 127` (equivalently `D_i == −1`). This guarantees an uniform encoding. Concatenate the 24-bit `low` parts of the 8 limbs in little-endian order to get 192 bits, then take the first `v · w = 126` bits split into `v = 42` little-endian chunks of `w = 3` bits → encoding `(e_0, ..., e_{v-1})` with each `e_i ∈ [0, chain_length)`. Reject if `sum(e_i) ≠ target_sum`. -2. **Recover WOTS public key**: for each `i`, walk chain `i` from `chain_tips[i]` for `chain_length - 1 - e_i` steps, where each step is `H(tweak_chain(i, step, s) | 00 | previous_value | pp | 0000)` truncated to `n`. -3. **Hash WOTS public key**: T-sponge with replacement over the `v` recovered chain ends, with IV `[tweak_wots_pk(s) | 00 | pp]`, ingesting two chain end digests at a time. Output is the Merkle leaf. -4. **Walk Merkle path**: for `level = 0..log_lifetime`, combine the current node with `merkle_proof[level]` (left/right determined by bit `level` of `s`) via `H(tweak_merkle(level+1, parent_index) | 00 | pp | left | right)` truncated to `n`. -5. **Check root**: accept iff the final hash equals `merkle_root`. +## Data size +- Public key: 32 bytes (`merkle_root | pp`) +- Signature: 1208 bytes (`chain_tips | randomness | merkle_proof`) -> Below IPv6 [MTU](https://fr.wikipedia.org/wiki/Maximum_transmission_unit) (1280 bytes). ## Security -target = 123,9 ≈ 124 bits of classical security in the ROM, and ≈ 62 bits of quantum security in the QROM, with an analysis inspired by the section 3.1 of [Tight adaptive reprogramming in the QROM](https://arxiv.org/pdf/2010.15103). TODO write the complete proof. +target ≈ 124 bits of classical security in the ROM, and ≈ 62 bits of quantum security in the QROM, with an analysis inspired by the section 3.1 of [Tight adaptive reprogramming in the QROM](https://arxiv.org/pdf/2010.15103). TODO write the complete proof. ## Signature size -**1171 bytes** `log2(p).(|randomness| + n.(v + log_lifetime))` - -below IPv6 [MTU](https://fr.wikipedia.org/wiki/Maximum_transmission_unit) (1280 bytes) From e717a9f13da802b44ef36f5656669842d18421c8 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:01:03 +0100 Subject: [PATCH 12/25] xmss: stop exporting the WOTS internals Only WotsSignature (a field of XmssSignature) is API; the one-time secret keys (whose pre-image array was even a public field), chain walking, public-key recovery, and the encoding are building blocks of xmss_sign / xmss_verify that no external caller should touch. The grinding measurement moves in-crate as a unit test with them. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/lib.rs | 4 +++- crates/xmss/src/wots.rs | 40 ++++++++++++++++++++++++++++++++- crates/xmss/src/xmss.rs | 1 + crates/xmss/tests/xmss_tests.rs | 32 +------------------------- 4 files changed, 44 insertions(+), 33 deletions(-) diff --git a/crates/xmss/src/lib.rs b/crates/xmss/src/lib.rs index 71c70565..442588b2 100644 --- a/crates/xmss/src/lib.rs +++ b/crates/xmss/src/lib.rs @@ -5,7 +5,9 @@ pub mod signers_cache; mod ssz_serialization; pub use ssz_serialization::{PUB_KEY_SSZ_LEN, SIGNATURE_SSZ_LEN}; mod wots; -pub use wots::*; +// The rest of the WOTS layer (one-time secret keys, chain walking, encoding) is a private +// building block of xmss_sign / xmss_verify; only the signature type is part of the API. +pub use wots::WotsSignature; mod xmss; pub use xmss::*; diff --git a/crates/xmss/src/wots.rs b/crates/xmss/src/wots.rs index c230067e..59fb1ebd 100644 --- a/crates/xmss/src/wots.rs +++ b/crates/xmss/src/wots.rs @@ -6,7 +6,7 @@ use crate::*; #[derive(Debug)] pub struct WotsSecretKey { - pub pre_images: [Digest; V], + pre_images: [Digest; V], public_key: WotsPublicKey, } @@ -164,3 +164,41 @@ fn is_valid_encoding(encoding: &[u8]) -> bool { } true } + +#[cfg(test)] +mod tests { + use rand::{RngExt, SeedableRng, rngs::StdRng}; + + use super::*; + + /// Measures the average number of randomness attempts before a valid encoding. + #[test] + #[ignore] + fn encoding_grinding_bits() { + let n = 100; + let xmss_pub_key = XmssPublicKey { + merkle_root: Default::default(), + public_param: Default::default(), + }; + let total_iters = parallel::map_reduce( + n, + || 0usize, + |i| { + let message: [F; MESSAGE_LEN_FE] = Default::default(); + let slot = i as u32; + let mut rng = StdRng::seed_from_u64(i as u64); + let mut num_iters = 0; + loop { + num_iters += 1; + let randomness: Randomness = rng.random(); + if wots_encode(&message, slot, &xmss_pub_key, &randomness).is_some() { + break num_iters; + } + } + }, + |a, b| a + b, + ); + let grinding = ((total_iters as f64) / (n as f64)).log2(); + println!("Average grinding bits: {:.1}", grinding); + } +} diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 5fbdcfb1..d593410b 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -4,6 +4,7 @@ use backend::*; use rand::{CryptoRng, RngExt, SeedableRng, rngs::StdRng}; use serde::{Deserialize, Serialize}; +use crate::wots::*; use crate::*; /// Memory-optimized secret key for a range of R = slot_end - slot_start + 1 slots: O(sqrt(R) + diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 4e782a8e..4d40d459 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -1,5 +1,5 @@ use backend::*; -use rand::{RngExt, SeedableRng, rngs::StdRng}; +use rand::{SeedableRng, rngs::StdRng}; use xmss::*; type F = KoalaBear; @@ -161,33 +161,3 @@ fn ssz_roundtrip() { assert!(XmssPublicKey::from_ssz_bytes(&pk_bytes[1..]).is_err()); assert!(XmssSignature::from_ssz_bytes(&sig_bytes[..SIGNATURE_SSZ_LEN - 1]).is_err()); } - -#[test] -#[ignore] -fn encoding_grinding_bits() { - let n = 100; - let xmss_pub_key = XmssPublicKey { - merkle_root: Default::default(), - public_param: Default::default(), - }; - let total_iters = parallel::map_reduce( - n, - || 0usize, - |i| { - let message: [F; MESSAGE_LEN_FE] = Default::default(); - let slot = i as u32; - let mut rng = StdRng::seed_from_u64(i as u64); - let mut num_iters = 0; - loop { - num_iters += 1; - let randomness: [F; RANDOMNESS_LEN_FE] = rng.random(); - if wots_encode(&message, slot, &xmss_pub_key, &randomness).is_some() { - break num_iters; - } - } - }, - |a, b| a + b, - ); - let grinding = ((total_iters as f64) / (n as f64)).log2(); - println!("Average grinding bits: {:.1}", grinding); -} From e6173a354c7b01b01eecfd81ff1cdbe49e0b7de8 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:01:57 +0100 Subject: [PATCH 13/25] xmss: redact secret material from Debug output XmssSecretKey's derived Debug printed the seed (and the seed-derived tree); one {:?} in a log line would leak the key. The manual impl shows only the slot range and split level. WotsSecretKey (crate-internal) loses Debug entirely. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/wots.rs | 2 +- crates/xmss/src/xmss.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/xmss/src/wots.rs b/crates/xmss/src/wots.rs index 59fb1ebd..8fac3837 100644 --- a/crates/xmss/src/wots.rs +++ b/crates/xmss/src/wots.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::*; -#[derive(Debug)] +/// No Debug: `pre_images` are the one-time secret keys. pub struct WotsSecretKey { pre_images: [Digest; V], public_key: WotsPublicKey, diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index d593410b..3e117d30 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -11,7 +11,6 @@ use crate::*; /// LOG_LIFETIME) instead of O(R). Stores the top tree (in-range band plus a thin spine) and one /// cached bottom subtree, cut at split_level = log2(R)/2. Out-of-range nodes are deterministic /// gen_random_node fillers; see `xmss_small_memory.tex` for the picture. -#[derive(Debug)] pub struct XmssSecretKey { pub(crate) slot_start: u32, // inclusive pub(crate) slot_end: u32, // inclusive @@ -23,6 +22,16 @@ pub struct XmssSecretKey { pub(crate) cache: Mutex>, } +impl std::fmt::Debug for XmssSecretKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("XmssSecretKey") + .field("slot_start", &self.slot_start) + .field("slot_end", &self.slot_end) + .field("split_level", &self.split_level) + .finish_non_exhaustive() + } +} + /// Bottom subtree covering the last-signed slot; its leaf range is derived from `subtree_index`. #[derive(Debug)] pub(crate) struct BottomSubtree { From 05441473f3fcc711d6c393163b725c34bfd90dc4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:03:25 +0100 Subject: [PATCH 14/25] xmss: gate the benchmark signer fixtures behind a 'test-utils' feature signers_cache generates 10k keys, writes caches under target/, and prints progress - benchmark machinery, not signature API. It is now compiled only for in-crate tests or under the new 'test-utils' feature, which rec_aggregation and the root crate enable explicitly (postcard becomes an optional dependency tied to it). Co-Authored-By: Claude Fable 5 --- Cargo.toml | 2 +- crates/rec_aggregation/Cargo.toml | 2 +- crates/xmss/Cargo.toml | 5 ++++- crates/xmss/src/lib.rs | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 334b8305..feab8558 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,7 @@ rec_aggregation.workspace = true zk-alloc.workspace = true sub_protocols.workspace = true lean_vm.workspace = true -xmss.workspace = true +xmss = { workspace = true, features = ["test-utils"] } backend.workspace = true serde_json.workspace = true system-info.workspace = true diff --git a/crates/rec_aggregation/Cargo.toml b/crates/rec_aggregation/Cargo.toml index 3e0dda15..e8d42db1 100644 --- a/crates/rec_aggregation/Cargo.toml +++ b/crates/rec_aggregation/Cargo.toml @@ -10,7 +10,7 @@ workspace = true prox-gaps-conjecture = ["lean_prover/prox-gaps-conjecture"] [dependencies] -xmss.workspace = true +xmss = { workspace = true, features = ["test-utils"] } tracing.workspace = true include_dir.workspace = true sub_protocols.workspace = true diff --git a/crates/xmss/Cargo.toml b/crates/xmss/Cargo.toml index 9900d69b..5dc286e5 100644 --- a/crates/xmss/Cargo.toml +++ b/crates/xmss/Cargo.toml @@ -6,11 +6,14 @@ edition.workspace = true [lints] workspace = true +[features] +test-utils = ["dep:postcard"] + [dependencies] rand.workspace = true backend.workspace = true serde.workspace = true -postcard.workspace = true +postcard = { workspace = true, optional = true } ssz.workspace = true [dev-dependencies] diff --git a/crates/xmss/src/lib.rs b/crates/xmss/src/lib.rs index 442588b2..ee1966f3 100644 --- a/crates/xmss/src/lib.rs +++ b/crates/xmss/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] use backend::{DIGEST_LEN_FE, KoalaBear, POSEIDON1_WIDTH, PrimeCharacteristicRing, PrimeField32, poseidon16_compress}; +#[cfg(any(test, feature = "test-utils"))] pub mod signers_cache; mod ssz_serialization; pub use ssz_serialization::{PUB_KEY_SSZ_LEN, SIGNATURE_SSZ_LEN}; From aaf5bc6df822cdd9881a5c87d90f5fc8abad6d01 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:04:25 +0100 Subject: [PATCH 15/25] xmss: fix flaten -> flatten Co-Authored-By: Claude Fable 5 --- crates/rec_aggregation/src/single_message_aggregation.rs | 6 +++--- crates/xmss/src/xmss.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/rec_aggregation/src/single_message_aggregation.rs b/crates/rec_aggregation/src/single_message_aggregation.rs index d7288a04..fdeef162 100644 --- a/crates/rec_aggregation/src/single_message_aggregation.rs +++ b/crates/rec_aggregation/src/single_message_aggregation.rs @@ -117,7 +117,7 @@ impl SingleMessageInfo { } pub(crate) fn hash_pubkeys(pub_keys: &[XmssPublicKey]) -> [F; DIGEST_LEN] { - let flat: Vec = pub_keys.iter().flat_map(|pk| pk.flaten().into_iter()).collect(); + let flat: Vec = pub_keys.iter().flat_map(|pk| pk.flatten().into_iter()).collect(); poseidon_hash_slice(&flat) } @@ -369,10 +369,10 @@ pub(crate) fn aggregate_single_message_signatures_with_min_padding( let mut pubkeys_blob: ArenaVec = ArenaVec::with_capacity((n_sigs + n_dup) * PUB_KEY_FLAT_SIZE); for pk in &global_pub_keys { - pubkeys_blob.extend_from_slice(&pk.flaten()); + pubkeys_blob.extend_from_slice(&pk.flatten()); } for pk in &dup_pub_keys { - pubkeys_blob.extend_from_slice(&pk.flaten()); + pubkeys_blob.extend_from_slice(&pk.flatten()); } let (merkle_leaf_blobs, merkle_path_blobs) = diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 3e117d30..a49d7bc9 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -72,7 +72,7 @@ pub struct XmssPublicKey { } impl XmssPublicKey { - pub fn flaten(&self) -> [F; PUB_KEY_FLAT_SIZE] { + pub fn flatten(&self) -> [F; PUB_KEY_FLAT_SIZE] { let mut output = [F::default(); PUB_KEY_FLAT_SIZE]; output[..XMSS_DIGEST_LEN].copy_from_slice(&self.merkle_root); output[XMSS_DIGEST_LEN..].copy_from_slice(&self.public_param); From 8893ba059fe252509215a1e9c1d9851841f0699b Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:05:08 +0100 Subject: [PATCH 16/25] xmss: make the Digest / PublicParam / Randomness / F aliases public They already appeared in public signatures (pub merkle_root: Digest, ...), so rustdoc rendered types downstream users could not name. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/lib.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/xmss/src/lib.rs b/crates/xmss/src/lib.rs index ee1966f3..f3cf4cae 100644 --- a/crates/xmss/src/lib.rs +++ b/crates/xmss/src/lib.rs @@ -15,10 +15,14 @@ pub use xmss::*; pub const XMSS_DIGEST_LEN: usize = 4; pub(crate) const TWEAK_LEN: usize = 2; -type F = KoalaBear; -type Digest = [F; XMSS_DIGEST_LEN]; -type PublicParam = [F; PUBLIC_PARAM_LEN_FE]; -type Randomness = [F; RANDOMNESS_LEN_FE]; +/// The base field of the scheme. +pub type F = KoalaBear; +/// A truncated Poseidon digest: hash-chain values and Merkle tree nodes. +pub type Digest = [F; XMSS_DIGEST_LEN]; +/// The per-key public parameter of the tweakable hash. +pub type PublicParam = [F; PUBLIC_PARAM_LEN_FE]; +/// The encoding randomness carried in every signature. +pub type Randomness = [F; RANDOMNESS_LEN_FE]; // WOTS pub const V: usize = 42; From a6aa69d34745a0c379f3b6f2bfc4145947101ace Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:10:06 +0100 Subject: [PATCH 17/25] xmss: deterministic key generation from a caller-provided seed xmss_key_gen_from_seed(seed, activation_slot, num_active_slots): the same (seed, range) always regenerates the same key pair, so a backed-up seed is enough to recover a key. xmss_key_gen becomes a thin wrapper that samples the seed from the RNG. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/xmss.rs | 11 ++++++++++- crates/xmss/tests/xmss_tests.rs | 18 ++++++++++++++++++ src/lib.rs | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index a49d7bc9..397db65e 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -216,6 +216,16 @@ pub fn xmss_key_gen( rng: &mut R, activation_slot: u64, num_active_slots: u64, +) -> Result<(XmssPublicKey, XmssSecretKey), XmssKeyGenError> { + xmss_key_gen_from_seed(rng.random(), activation_slot, num_active_slots) +} + +/// Deterministic [`xmss_key_gen`]: the same (seed, activation range) always regenerates the +/// same key pair. The seed is the key's entire secret material. +pub fn xmss_key_gen_from_seed( + seed: [u8; 32], + activation_slot: u64, + num_active_slots: u64, ) -> Result<(XmssPublicKey, XmssSecretKey), XmssKeyGenError> { let activation_end = activation_slot .checked_add(num_active_slots) @@ -223,7 +233,6 @@ pub fn xmss_key_gen( if num_active_slots == 0 || activation_end > 1 << LOG_LIFETIME { return Err(XmssKeyGenError::InvalidRange); } - let seed: [u8; 32] = rng.random(); // The pool forbids nested dispatch: build sequentially when key gen itself already runs // inside a pool task (e.g. generating many keys in a parallel batch). diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index 4d40d459..c60e9dab 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -117,6 +117,24 @@ fn concurrent_signing_on_shared_key() { }); } +#[test] +fn keygen_from_seed_is_deterministic() { + let message: [u8; MESSAGE_LEN_BYTES] = std::array::from_fn(|i| i as u8); + let seed = [42u8; 32]; + + let (pk1, sk1) = xmss_key_gen_from_seed(seed, 100, 16).unwrap(); + let (pk2, sk2) = xmss_key_gen_from_seed(seed, 100, 16).unwrap(); + assert_eq!(pk1, pk2); + assert_eq!( + xmss_sign(&sk1, 110, &message).unwrap(), + xmss_sign(&sk2, 110, &message).unwrap() + ); + + // Same seed, different activation range: a different key. + let (pk3, _) = xmss_key_gen_from_seed(seed, 100, 32).unwrap(); + assert_ne!(pk1, pk3); +} + #[test] fn keygen_rejects_invalid_ranges() { let mut rng = StdRng::seed_from_u64(0); diff --git a/src/lib.rs b/src/lib.rs index 12e4aea4..13f9513b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ pub use rec_aggregation::{ }; pub use xmss::{ MESSAGE_LEN_BYTES, MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, hash_message, xmss_key_gen, - xmss_sign, xmss_verify, + xmss_key_gen_from_seed, xmss_sign, xmss_verify, }; pub type F = KoalaBear; From 571571e28ac17fa2afc82f00fd78bba9cae36aa3 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:12:22 +0100 Subject: [PATCH 18/25] lean-multisig: re-export the xmss error types and SSZ length constants Without them a lean-multisig user could not match on the errors returned by the re-exported xmss_key_gen / xmss_sign / xmss_verify without adding a direct xmss dependency. Co-Authored-By: Claude Fable 5 --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 13f9513b..0946b486 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,8 @@ pub use rec_aggregation::{ verify_single_message_aggregate, }; pub use xmss::{ - MESSAGE_LEN_BYTES, MESSAGE_LEN_FE, XmssPublicKey, XmssSecretKey, XmssSignature, hash_message, xmss_key_gen, + MESSAGE_LEN_BYTES, MESSAGE_LEN_FE, PUB_KEY_SSZ_LEN, SIGNATURE_SSZ_LEN, XmssKeyGenError, XmssPublicKey, + XmssSecretKey, XmssSignature, XmssSignatureError, XmssVerifyError, hash_message, xmss_key_gen, xmss_key_gen_from_seed, xmss_sign, xmss_verify, }; From f44b3dbe16caab27be5522d9e4ac7e3237db1714 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:14:27 +0100 Subject: [PATCH 19/25] xmss: version the secret-key persistence format A leading format-version byte in the serde encoding, checked on decode, so a future layout change fails with an explicit error (and can be migrated) instead of a confusing shape mismatch. pk/sig SSZ formats are consensus-spec'd and stay untagged. Co-Authored-By: Claude Fable 5 --- crates/xmss/src/xmss.rs | 26 +++++++++++++++++++++----- crates/xmss/tests/xmss_tests.rs | 10 ++++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index 397db65e..ed62b5e2 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -39,18 +39,34 @@ pub(crate) struct BottomSubtree { layers: Vec>, } -/// Persists (seed, slot range, top tree). The top tree is stored so that loading a key is -/// cheap (no re-hashing of the whole range); the derived fields (public_param, split_level) -/// are recomputed and the tree shape revalidated. The bottom-subtree cache restarts empty. +/// Format version of the persisted secret key; bump on layout changes. +const SECRET_KEY_FORMAT_VERSION: u8 = 1; + +/// Persists (version, seed, slot range, top tree). The top tree is stored so that loading a +/// key is cheap (no re-hashing of the whole range); the derived fields (public_param, +/// split_level) are recomputed and the tree shape revalidated. The bottom-subtree cache +/// restarts empty. impl Serialize for XmssSecretKey { fn serialize(&self, s: S) -> Result { - (&self.seed, self.slot_start, self.slot_end, &self.top).serialize(s) + ( + SECRET_KEY_FORMAT_VERSION, + &self.seed, + self.slot_start, + self.slot_end, + &self.top, + ) + .serialize(s) } } impl<'de> Deserialize<'de> for XmssSecretKey { fn deserialize>(d: D) -> Result { - let (seed, slot_start, slot_end, top) = <([u8; 32], u32, u32, Vec>)>::deserialize(d)?; + let (version, seed, slot_start, slot_end, top) = <(u8, [u8; 32], u32, u32, Vec>)>::deserialize(d)?; + if version != SECRET_KEY_FORMAT_VERSION { + return Err(serde::de::Error::custom(format!( + "unsupported secret key format version {version} (expected {SECRET_KEY_FORMAT_VERSION})" + ))); + } Self::from_parts(seed, slot_start, slot_end, top).map_err(serde::de::Error::custom) } } diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index c60e9dab..d839873e 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -29,10 +29,16 @@ fn test_xmss_serialize_deserialize() { assert_eq!(xmss_sign(&sk2, slot, &message).unwrap(), sig); // A top tree whose shape does not match the slot range must be rejected. - let (seed, start, end, mut top): ([u8; 32], u32, u32, Vec>) = postcard::from_bytes(&sk_bytes).unwrap(); + let (version, seed, start, end, mut top): (u8, [u8; 32], u32, u32, Vec>) = + postcard::from_bytes(&sk_bytes).unwrap(); top.pop(); - let corrupted = postcard::to_allocvec(&(seed, start, end, top)).unwrap(); + let corrupted = postcard::to_allocvec(&(version, seed, start, end, top)).unwrap(); assert!(postcard::from_bytes::(&corrupted).is_err()); + + // An unknown format version must be rejected. + let mut wrong_version = sk_bytes.clone(); + wrong_version[0] ^= 1; + assert!(postcard::from_bytes::(&wrong_version).is_err()); } #[test] From 2cbc7a7df5bc1045724ae58f1af0f33fb371ec56 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:28:33 +0100 Subject: [PATCH 20/25] rec_aggregation: messages are 32 bytes across the aggregation API aggregate_single_message_signatures, SingleMessageInfo.message, and split_multi_message_aggregate_by_message now take the raw 32-byte message; the Poseidon message hash the snark consumes is computed internally (once per aggregation / input-data build). Callers no longer deal in field-element messages anywhere. Co-Authored-By: Claude Fable 5 --- crates/rec_aggregation/src/benchmark.rs | 2 +- .../src/multi_message_aggregation.rs | 2 +- .../src/single_message_aggregation.rs | 22 +++++++++---------- tests/test_multisignatures.rs | 10 ++++----- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/rec_aggregation/src/benchmark.rs b/crates/rec_aggregation/src/benchmark.rs index 6a071d05..42d0c601 100644 --- a/crates/rec_aggregation/src/benchmark.rs +++ b/crates/rec_aggregation/src/benchmark.rs @@ -401,7 +401,7 @@ fn build_aggregation( let result = aggregate_single_message_signatures( &children, raw_xmss.clone(), - xmss::hash_message(&message_for_benchmark()), + message_for_benchmark(), BENCHMARK_SLOT, topology.log_inv_rate, ) diff --git a/crates/rec_aggregation/src/multi_message_aggregation.rs b/crates/rec_aggregation/src/multi_message_aggregation.rs index f5bdce57..2eff6752 100644 --- a/crates/rec_aggregation/src/multi_message_aggregation.rs +++ b/crates/rec_aggregation/src/multi_message_aggregation.rs @@ -205,7 +205,7 @@ pub fn verify_multi_message_aggregate(sig: &MultiMessageAggregateSignature) -> R pub fn split_multi_message_aggregate_by_message( multi_message: MultiMessageAggregateSignature, - message: [F; DIGEST_LEN], + message: [u8; xmss::MESSAGE_LEN_BYTES], log_inv_rate: usize, ) -> Result { let Some(index) = multi_message.info.iter().position(|info| info.message == message) else { diff --git a/crates/rec_aggregation/src/single_message_aggregation.rs b/crates/rec_aggregation/src/single_message_aggregation.rs index fdeef162..fb222fc9 100644 --- a/crates/rec_aggregation/src/single_message_aggregation.rs +++ b/crates/rec_aggregation/src/single_message_aggregation.rs @@ -7,8 +7,8 @@ use tracing::instrument; use xmss::CHAIN_LENGTH; use xmss::make_tweak; use xmss::{ - LOG_LIFETIME, MESSAGE_LEN_FE, PUB_KEY_FLAT_SIZE, TWEAK_TYPE_CHAIN, TWEAK_TYPE_ENCODING, TWEAK_TYPE_MERKLE, - TWEAK_TYPE_WOTS_PK, V, WOTS_SIG_SIZE_FE, XmssPublicKey, XmssSignature, + LOG_LIFETIME, MESSAGE_LEN_BYTES, PUB_KEY_FLAT_SIZE, TWEAK_TYPE_CHAIN, TWEAK_TYPE_ENCODING, TWEAK_TYPE_MERKLE, + TWEAK_TYPE_WOTS_PK, V, WOTS_SIG_SIZE_FE, XmssPublicKey, XmssSignature, hash_message, }; use serde::{Deserialize, Serialize}; @@ -33,7 +33,7 @@ pub(crate) const TWEAK_TABLE_SIZE_FE_PADDED: usize = (N_TWEAKS * TWEAK_SLOT_SIZE #[derive(Debug, Clone, PartialEq, Eq)] pub struct SingleMessageInfo { - pub message: [F; MESSAGE_LEN_FE], + pub message: [u8; MESSAGE_LEN_BYTES], pub slot: u32, pub pubkeys: Vec, pub bytecode_claim: Evaluation, // value is trusted to be correct (should be recomputed when receiving a proof from an untrusted source) @@ -55,7 +55,7 @@ impl Serialize for SingleMessageInfo { impl<'de> Deserialize<'de> for SingleMessageInfo { fn deserialize>(d: D) -> Result { let (message, slot, pubkeys, bytecode_claim_point) = - <([F; MESSAGE_LEN_FE], u32, Vec, MultilinearPoint)>::deserialize(d)?; + <([u8; MESSAGE_LEN_BYTES], u32, Vec, MultilinearPoint)>::deserialize(d)?; let bytecode = try_get_aggregation_bytecode().ok_or_else(|| serde::de::Error::custom("bytecode not initialized"))?; if bytecode_claim_point.len() != bytecode.cumulated_n_vars() { @@ -107,7 +107,7 @@ impl SingleMessageInfo { build_single_message_input_data( self.pubkeys.len(), &hash_pubkeys(&self.pubkeys), - &self.message, + &hash_message(&self.message), self.slot, &tweaks_hash, &self.bytecode_claim_flat(), @@ -165,7 +165,7 @@ fn compute_merkle_chunks_for_slot(slot: u32) -> Vec { pub(crate) fn build_single_message_input_data( n_sigs: usize, pubkeys_hash: &[F; DIGEST_LEN], - message: &[F; MESSAGE_LEN_FE], + hashed_message: &[F; DIGEST_LEN], slot: u32, tweaks_hash: &[F; DIGEST_LEN], bytecode_claim_flat: &[F], @@ -181,7 +181,7 @@ pub(crate) fn build_single_message_input_data( data.extend(std::iter::repeat_n(F::ZERO, claim_padding)); data.extend_from_slice(&fiat_shamir_domain_sep(bytecode)); data.extend_from_slice(pubkeys_hash); - data.extend_from_slice(message); + data.extend_from_slice(hashed_message); data.extend(compute_merkle_chunks_for_slot(slot)); data.extend_from_slice(tweaks_hash); data @@ -209,7 +209,7 @@ pub fn verify_single_message_aggregate(sig: &SingleMessageAggregateSignature) -> pub fn aggregate_single_message_signatures( children: &[SingleMessageAggregateSignature], raw_xmss: Vec<(XmssPublicKey, XmssSignature)>, - message: [F; MESSAGE_LEN_FE], + message: [u8; MESSAGE_LEN_BYTES], slot: u32, log_inv_rate: usize, ) -> Result { @@ -226,7 +226,7 @@ pub fn aggregate_single_message_signatures( pub(crate) fn aggregate_single_message_signatures_with_min_padding( children: &[SingleMessageAggregateSignature], mut raw_xmss: Vec<(XmssPublicKey, XmssSignature)>, - message: [F; MESSAGE_LEN_FE], + message: [u8; MESSAGE_LEN_BYTES], slot: u32, log_inv_rate: usize, min_table_log_n_rows: BTreeMap, @@ -298,7 +298,7 @@ pub(crate) fn aggregate_single_message_signatures_with_min_padding( let pub_input_data = build_single_message_input_data( n_sigs, &hash_pubkeys(&global_pub_keys), - message, + &hash_message(message), slot, &tweaks_hash, &reduced_claims.final_claim_flat(), @@ -481,7 +481,7 @@ mod tests { init_aggregation_bytecode(); let log_inv_rate = 2; - let message = xmss::hash_message(&message_for_benchmark()); + let message = message_for_benchmark(); let slot: u32 = BENCHMARK_SLOT; let signatures = get_benchmark_signatures(); let raws_inner = signatures[0..10].to_vec(); diff --git a/tests/test_multisignatures.rs b/tests/test_multisignatures.rs index 1468d2f8..fc2fd32b 100644 --- a/tests/test_multisignatures.rs +++ b/tests/test_multisignatures.rs @@ -12,7 +12,6 @@ use rec_aggregation::{ split_multi_message_aggregate_by_message, }; use xmss::{ - hash_message, signers_cache::{BENCHMARK_SLOT, get_benchmark_signatures, message_for_benchmark}, xmss_key_gen, xmss_sign, xmss_verify, }; @@ -56,7 +55,7 @@ fn test_single_message_aggregation() { setup_prover(); let log_inv_rate = 2; // [1, 2, 3 or 4] (lower = faster but bigger proofs) - let message = hash_message(&message_for_benchmark()); + let message = message_for_benchmark(); let slot: u32 = BENCHMARK_SLOT; let signatures = get_benchmark_signatures(); @@ -90,21 +89,20 @@ fn test_multi_message_aggregation() { let log_inv_rate = 2; // [1, 2, 3 or 4] (lower = faster but bigger proofs) let slot_a = BENCHMARK_SLOT; - let message_a = hash_message(&message_for_benchmark()); + let message_a = message_for_benchmark(); let signatures = get_benchmark_signatures(); let raws_a = signatures[0..3].to_vec(); let slot_b = BENCHMARK_SLOT + 1; let mut rng_b: StdRng = StdRng::seed_from_u64(17); - let message_b_bytes: [u8; 32] = rng_b.random(); - let message_b = hash_message(&message_b_bytes); + let message_b: [u8; 32] = rng_b.random(); assert!(message_b != message_a && slot_b != slot_a); let raws_b: Vec<_> = (0..2) .map(|_| { let (pk, sk) = xmss_key_gen(&mut rng_b, u64::from(slot_b), 1).unwrap(); - let sig = xmss_sign(&sk, slot_b, &message_b_bytes).unwrap(); + let sig = xmss_sign(&sk, slot_b, &message_b).unwrap(); (pk, sig) }) .collect(); From 4f7bec9f0ae94b018c8f6027ca92dffcb6f5de03 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:31:00 +0100 Subject: [PATCH 21/25] xmss: stop exposing the field-element message form The public API deals exclusively in 32-byte messages: MESSAGE_LEN_FE, MESSAGE_EMBEDDING_LEN_FE, and encode_message become crate-private, and hash_message is doc(hidden) (it stays technically pub only because rec_aggregation needs it for the snark public input). The zkDSL placeholder uses DIGEST_LEN, which the hashed message is. Co-Authored-By: Claude Fable 5 --- crates/rec_aggregation/src/compilation.rs | 6 +++--- crates/xmss/src/lib.rs | 9 +++++---- src/lib.rs | 5 ++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/rec_aggregation/src/compilation.rs b/crates/rec_aggregation/src/compilation.rs index 7f9bf76e..08e7b34c 100644 --- a/crates/rec_aggregation/src/compilation.rs +++ b/crates/rec_aggregation/src/compilation.rs @@ -9,7 +9,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::OnceLock; use sub_protocols::{N_VARS_TO_SEND_GKR_COEFFS, min_stacked_n_vars, total_whir_statements}; use tracing::instrument; -use xmss::{LOG_LIFETIME, MESSAGE_LEN_FE, PUBLIC_PARAM_LEN_FE, RANDOMNESS_LEN_FE, TARGET_SUM, V, W, XMSS_DIGEST_LEN}; +use xmss::{LOG_LIFETIME, PUBLIC_PARAM_LEN_FE, RANDOMNESS_LEN_FE, TARGET_SUM, V, W, XMSS_DIGEST_LEN}; use crate::bytecode_claims::bytecode_reduction_sumcheck_proof_size; use crate::single_message_aggregation::TWEAK_TABLE_SIZE_FE_PADDED; @@ -51,7 +51,7 @@ pub(crate) const MULTI_MESSAGE_FLAG: usize = 0; pub(crate) const BYTECODE_CLAIM_OFFSET: usize = DIGEST_LEN; /// Single-message component data: pubkeys_hash | message | merkle_chunks | tweaks_hash. -pub(crate) const COMPONENT_DATA_SIZE: usize = DIGEST_LEN + MESSAGE_LEN_FE + N_MERKLE_CHUNKS_FOR_SLOT + DIGEST_LEN; +pub(crate) const COMPONENT_DATA_SIZE: usize = DIGEST_LEN + DIGEST_LEN + N_MERKLE_CHUNKS_FOR_SLOT + DIGEST_LEN; // pubkeys_hash + hashed message + merkle chunks + tweaks_hash pub(crate) fn bytecode_claim_size_padded(program_log_size: usize) -> usize { let bytecode_point_n_vars = program_log_size + log2_ceil_usize(N_INSTRUCTION_COLUMNS); @@ -417,7 +417,7 @@ fn build_replacements(log_inner_bytecode: usize, bytecode_zero_eval: F) -> BTree replacements.insert("W_PLACEHOLDER".to_string(), W.to_string()); replacements.insert("TARGET_SUM_PLACEHOLDER".to_string(), TARGET_SUM.to_string()); replacements.insert("LOG_LIFETIME_PLACEHOLDER".to_string(), LOG_LIFETIME.to_string()); - replacements.insert("MESSAGE_LEN_PLACEHOLDER".to_string(), MESSAGE_LEN_FE.to_string()); + replacements.insert("MESSAGE_LEN_PLACEHOLDER".to_string(), DIGEST_LEN.to_string()); // the hashed message is one digest replacements.insert("RANDOMNESS_LEN_PLACEHOLDER".to_string(), RANDOMNESS_LEN_FE.to_string()); replacements.insert( "PUBLIC_PARAM_LEN_FE_PLACEHOLDER".to_string(), diff --git a/crates/xmss/src/lib.rs b/crates/xmss/src/lib.rs index f3cf4cae..27de788b 100644 --- a/crates/xmss/src/lib.rs +++ b/crates/xmss/src/lib.rs @@ -35,9 +35,9 @@ pub const RANDOMNESS_LEN_FE: usize = 6; /// Byte length of the messages being signed. pub const MESSAGE_LEN_BYTES: usize = 32; /// Field elements of the injective base-p embedding of a message (p > 2^30, 9 * 30 >= 8 * 32). -pub const MESSAGE_EMBEDDING_LEN_FE: usize = 9; +pub(crate) const MESSAGE_EMBEDDING_LEN_FE: usize = 9; /// Field elements of the hashed message, the form consumed by WOTS encoding (and the snark). -pub const MESSAGE_LEN_FE: usize = 8; +pub(crate) const MESSAGE_LEN_FE: usize = 8; pub const PUBLIC_PARAM_LEN_FE: usize = 4; pub const PUB_KEY_FLAT_SIZE: usize = XMSS_DIGEST_LEN + PUBLIC_PARAM_LEN_FE; pub const WOTS_SIG_SIZE_FE: usize = RANDOMNESS_LEN_FE + V * XMSS_DIGEST_LEN; @@ -72,7 +72,7 @@ pub const MAX_SIGNING_ATTEMPTS: usize = 100_000; /// Injective embedding of a message into `MESSAGE_EMBEDDING_LEN_FE` field elements: /// little-endian base-p decomposition of the message read as a little-endian integer /// (the same convention as leanSig's `encode_message`). -pub fn encode_message(message: &[u8; MESSAGE_LEN_BYTES]) -> [F; MESSAGE_EMBEDDING_LEN_FE] { +pub(crate) fn encode_message(message: &[u8; MESSAGE_LEN_BYTES]) -> [F; MESSAGE_EMBEDDING_LEN_FE] { let p = u64::from(F::ORDER_U32); let mut words: [u32; MESSAGE_LEN_BYTES / 4] = std::array::from_fn(|i| u32::from_le_bytes(message[4 * i..4 * (i + 1)].try_into().unwrap())); @@ -90,7 +90,8 @@ pub fn encode_message(message: &[u8; MESSAGE_LEN_BYTES]) -> [F; MESSAGE_EMBEDDIN /// Off-circuit message hash: what gets signed (and what the snark consumes as "message") is /// this domain-separated Poseidon digest of the 32-byte message. -pub fn hash_message(message: &[u8; MESSAGE_LEN_BYTES]) -> [F; MESSAGE_LEN_FE] { +#[doc(hidden)] +pub fn hash_message(message: &[u8; MESSAGE_LEN_BYTES]) -> [F; DIGEST_LEN_FE] { let mut input = [F::ZERO; POSEIDON1_WIDTH]; input[0] = F::from_u32(DOMAINSEP_MESSAGE_HASH); input[1..1 + MESSAGE_EMBEDDING_LEN_FE].copy_from_slice(&encode_message(message)); diff --git a/src/lib.rs b/src/lib.rs index 0946b486..aa95c378 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,9 +8,8 @@ pub use rec_aggregation::{ verify_single_message_aggregate, }; pub use xmss::{ - MESSAGE_LEN_BYTES, MESSAGE_LEN_FE, PUB_KEY_SSZ_LEN, SIGNATURE_SSZ_LEN, XmssKeyGenError, XmssPublicKey, - XmssSecretKey, XmssSignature, XmssSignatureError, XmssVerifyError, hash_message, xmss_key_gen, - xmss_key_gen_from_seed, xmss_sign, xmss_verify, + MESSAGE_LEN_BYTES, PUB_KEY_SSZ_LEN, SIGNATURE_SSZ_LEN, XmssKeyGenError, XmssPublicKey, XmssSecretKey, + XmssSignature, XmssSignatureError, XmssVerifyError, xmss_key_gen, xmss_key_gen_from_seed, xmss_sign, xmss_verify, }; pub type F = KoalaBear; From ec2d9298d80b8977b3a87c33930484c7b03d5cc5 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:32:58 +0100 Subject: [PATCH 22/25] rec_aggregation: document the aggregation entry points Co-Authored-By: Claude Fable 5 --- crates/rec_aggregation/src/multi_message_aggregation.rs | 5 +++++ crates/rec_aggregation/src/single_message_aggregation.rs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/rec_aggregation/src/multi_message_aggregation.rs b/crates/rec_aggregation/src/multi_message_aggregation.rs index 2eff6752..556c5da1 100644 --- a/crates/rec_aggregation/src/multi_message_aggregation.rs +++ b/crates/rec_aggregation/src/multi_message_aggregation.rs @@ -91,6 +91,8 @@ fn build_multi_message_input_data(digests: &[[F; DIGEST_LEN]], bytecode_claim_fl data } +/// Merge single-message aggregates (each with its own (message, slot)) into one +/// multi-message aggregate attested by a single proof. pub fn merge_single_message_aggregates( single_messages: Vec, log_inv_rate: usize, @@ -187,6 +189,7 @@ pub fn merge_single_message_aggregates( }) } +/// Verify a multi-message aggregate against its per-component `info` (message, slot, pubkeys). pub fn verify_multi_message_aggregate(sig: &MultiMessageAggregateSignature) -> Result { if sig.info.is_empty() || sig.info.len() > MAX_RECURSIONS { return Err(ProofError::InvalidProof); @@ -203,6 +206,8 @@ pub fn verify_multi_message_aggregate(sig: &MultiMessageAggregateSignature) -> R verify_inner(input_data, sig.proof.proof.clone()) } +/// [`split_multi_message_aggregate`] for the unique component with the given message; +/// errors if no component (or more than one) carries it. pub fn split_multi_message_aggregate_by_message( multi_message: MultiMessageAggregateSignature, message: [u8; xmss::MESSAGE_LEN_BYTES], diff --git a/crates/rec_aggregation/src/single_message_aggregation.rs b/crates/rec_aggregation/src/single_message_aggregation.rs index fb222fc9..1b2f7c2e 100644 --- a/crates/rec_aggregation/src/single_message_aggregation.rs +++ b/crates/rec_aggregation/src/single_message_aggregation.rs @@ -197,7 +197,10 @@ fn encode_wots_signature(sig: &XmssSignature) -> ArenaVec { data } -// assumes `bytecode_value` in SingleMessageAggregateSignature::proof is correct (it should not be read / deserialized from an untrusted source) +/// Verify a single-message aggregate against its own `info` (message, slot, pubkeys). +/// +/// Assumes `sig.info.bytecode_claim.value` is correct: it must not come from an untrusted +/// source without recomputation (deserializing via serde recomputes it). pub fn verify_single_message_aggregate(sig: &SingleMessageAggregateSignature) -> Result { check_single_message_pubkeys(&sig.info.pubkeys).map_err(|_| ProofError::InvalidProof)?; verify_inner(sig.info.build_input_data(), sig.proof.proof.clone()) From b09392d900eb104803485d5b3bd0af507e4329a4 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:38:37 +0100 Subject: [PATCH 23/25] xmss: expose the key's activation slot range Co-Authored-By: Claude Fable 5 --- crates/xmss/src/xmss.rs | 5 +++++ crates/xmss/tests/xmss_tests.rs | 1 + 2 files changed, 6 insertions(+) diff --git a/crates/xmss/src/xmss.rs b/crates/xmss/src/xmss.rs index ed62b5e2..1fcc3a89 100644 --- a/crates/xmss/src/xmss.rs +++ b/crates/xmss/src/xmss.rs @@ -376,6 +376,11 @@ impl XmssSecretKey { } } + /// The slots this key can sign for. + pub const fn activation_slots(&self) -> std::ops::RangeInclusive { + self.slot_start..=self.slot_end + } + /// Warms the signing cache for `slot`: when the next signing slot is known in advance, /// calling this ahead of time makes the subsequent `xmss_sign` faster. pub fn prepare(&self, slot: u32) -> Result<(), XmssSignatureError> { diff --git a/crates/xmss/tests/xmss_tests.rs b/crates/xmss/tests/xmss_tests.rs index d839873e..67555a16 100644 --- a/crates/xmss/tests/xmss_tests.rs +++ b/crates/xmss/tests/xmss_tests.rs @@ -131,6 +131,7 @@ fn keygen_from_seed_is_deterministic() { let (pk1, sk1) = xmss_key_gen_from_seed(seed, 100, 16).unwrap(); let (pk2, sk2) = xmss_key_gen_from_seed(seed, 100, 16).unwrap(); assert_eq!(pk1, pk2); + assert_eq!(sk1.activation_slots(), 100..=115); assert_eq!( xmss_sign(&sk1, 110, &message).unwrap(), xmss_sign(&sk2, 110, &message).unwrap() From 4178ee1049dc552dfdeb2d61ad9a1683e832ed08 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:39:43 +0100 Subject: [PATCH 24/25] ci: check xmss standalone with default features The workspace build always enables xmss's 'test-utils' feature via rec_aggregation, so the configuration downstream users get (no feature, optional postcard off) was never built in CI. Co-Authored-By: Claude Fable 5 --- .github/workflows/rust.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5ea263b5..14b71bde 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -76,6 +76,10 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Clippy Check run: cargo clippy --workspace --all-targets -- -Dwarnings + # The workspace build always enables xmss's 'test-utils' feature (rec_aggregation turns + # it on); this checks the default-features configuration downstream users get. + - name: Clippy Check (xmss standalone, default features) + run: cargo clippy -p xmss --all-targets -- -Dwarnings cargo-fmt: runs-on: ubuntu-latest From 618b77f60b8929aea68913834bd3f578bd7854bb Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Thu, 23 Jul 2026 23:46:48 +0100 Subject: [PATCH 25/25] xmss: fix new nightly clippy lint (as_chunks over chunks_exact) Co-Authored-By: Claude Fable 5 --- crates/xmss/src/ssz_serialization.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/xmss/src/ssz_serialization.rs b/crates/xmss/src/ssz_serialization.rs index 5b370ce3..4bac1ccb 100644 --- a/crates/xmss/src/ssz_serialization.rs +++ b/crates/xmss/src/ssz_serialization.rs @@ -29,8 +29,8 @@ fn append_fes(buf: &mut Vec, fes: &[F]) { fn read_fes(bytes: &[u8]) -> Result<[F; N], DecodeError> { debug_assert_eq!(bytes.len(), N * FE_BYTES); let mut out = [F::ZERO; N]; - for (fe, chunk) in out.iter_mut().zip(bytes.chunks_exact(FE_BYTES)) { - let value = u32::from_le_bytes(chunk.try_into().unwrap()); + for (fe, chunk) in out.iter_mut().zip(bytes.as_chunks::().0) { + let value = u32::from_le_bytes(*chunk); if value >= F::ORDER_U32 { return Err(DecodeError::BytesInvalid(format!( "non-canonical field element: {value}" @@ -123,14 +123,17 @@ impl Decode for XmssSignature { fn from_ssz_bytes(bytes: &[u8]) -> Result { check_len(bytes, SIGNATURE_SSZ_LEN)?; - let mut digests = bytes.chunks_exact(DIGEST_BYTES); + let mut digests = bytes.as_chunks::().0.iter(); let mut chain_tips = [[F::ZERO; XMSS_DIGEST_LEN]; V]; for chain_tip in &mut chain_tips { *chain_tip = read_fes(digests.next().unwrap())?; } let randomness_start = V * DIGEST_BYTES; let randomness = read_fes(&bytes[randomness_start..randomness_start + RANDOMNESS_LEN_FE * FE_BYTES])?; - let mut digests = bytes[randomness_start + RANDOMNESS_LEN_FE * FE_BYTES..].chunks_exact(DIGEST_BYTES); + let mut digests = bytes[randomness_start + RANDOMNESS_LEN_FE * FE_BYTES..] + .as_chunks::() + .0 + .iter(); let mut merkle_proof = [[F::ZERO; XMSS_DIGEST_LEN]; LOG_LIFETIME]; for neighbour in &mut merkle_proof { *neighbour = read_fes(digests.next().unwrap())?;