From 4b0fbe85bdb79e24e30addc9df565150ce51e943 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Mon, 23 Mar 2026 23:42:34 +0100 Subject: [PATCH 1/5] wip --- MODIFS.md | 5 + src/array.rs | 2 +- src/signature/generalized_xmss.rs | 2 +- .../instantiations_aborting.rs | 17 ++-- src/symmetric/message_hash.rs | 2 + src/symmetric/message_hash/aborting.rs | 1 + src/symmetric/message_hash/poseidon.rs | 4 +- src/symmetric/tweak_hash/poseidon.rs | 97 +++++++++++-------- 8 files changed, 77 insertions(+), 53 deletions(-) create mode 100644 MODIFS.md diff --git a/MODIFS.md b/MODIFS.md new file mode 100644 index 0000000..2706001 --- /dev/null +++ b/MODIFS.md @@ -0,0 +1,5 @@ +# Modifs + +- replacement sponge +- t-sponge +- data ordering in chains \ No newline at end of file diff --git a/src/array.rs b/src/array.rs index cd0f898..0ef0b3a 100644 --- a/src/array.rs +++ b/src/array.rs @@ -8,7 +8,7 @@ use crate::serialization::Serializable; use p3_field::{PrimeCharacteristicRing, PrimeField32, RawDataSerializable}; /// A wrapper around an array of field elements that implements SSZ Encode/Decode. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct FieldArray(pub [F; N]); diff --git a/src/signature/generalized_xmss.rs b/src/signature/generalized_xmss.rs index 3d6c795..72ab34f 100644 --- a/src/signature/generalized_xmss.rs +++ b/src/signature/generalized_xmss.rs @@ -188,7 +188,7 @@ impl Decode for GeneralizedXMSSSign /// Public key for GeneralizedXMSSSignatureScheme /// It contains a Merkle root and a parameter for the tweakable hash -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] pub struct GeneralizedXMSSPublicKey { root: TH::Domain, parameter: TH::Parameter, diff --git a/src/signature/generalized_xmss/instantiations_aborting.rs b/src/signature/generalized_xmss/instantiations_aborting.rs index ddd5942..9ac132a 100644 --- a/src/signature/generalized_xmss/instantiations_aborting.rs +++ b/src/signature/generalized_xmss/instantiations_aborting.rs @@ -4,7 +4,7 @@ pub mod lifetime_2_to_the_32 { use crate::{ inc_encoding::target_sum::TargetSumEncoding, signature::generalized_xmss::{ - GeneralizedXMSSPublicKey, GeneralizedXMSSSignature, GeneralizedXMSSSignatureScheme, + GeneralizedXMSSPublicKey, GeneralizedXMSSSecretKey, GeneralizedXMSSSignature, GeneralizedXMSSSignatureScheme }, symmetric::{ message_hash::aborting::AbortingHypercubeMessageHash, prf::shake_to_field::ShakePRFtoF, @@ -43,9 +43,10 @@ pub mod lifetime_2_to_the_32 { type PRF = ShakePRFtoF; type IE = TargetSumEncoding; - pub type SIGAbortingTargetSumLifetime32Dim64Base8 = + pub type SchemeAbortingTargetSumLifetime32Dim64Base8 = GeneralizedXMSSSignatureScheme; pub type PubKeyAbortingTargetSumLifetime32Dim64Base8 = GeneralizedXMSSPublicKey; + pub type SecretKeyAbortingTargetSumLifetime32Dim64Base8 = GeneralizedXMSSSecretKey; pub type SigAbortingTargetSumLifetime32Dim64Base8 = GeneralizedXMSSSignature; #[cfg(test)] @@ -121,7 +122,7 @@ pub mod lifetime_2_to_the_6 { type PRF = ShakePRFtoF; type IE = TargetSumEncoding; - pub type SIGAbortingTargetSumLifetime6Dim46Base8 = + pub type SchemeAbortingTargetSumLifetime6Dim46Base8 = GeneralizedXMSSSignatureScheme; #[cfg(test)] @@ -130,19 +131,19 @@ pub mod lifetime_2_to_the_6 { SignatureScheme, test_templates::test_signature_scheme_correctness, }; - use super::SIGAbortingTargetSumLifetime6Dim46Base8; + use super::SchemeAbortingTargetSumLifetime6Dim46Base8; #[test] pub fn test_correctness() { - test_signature_scheme_correctness::( + test_signature_scheme_correctness::( 2, 0, - SIGAbortingTargetSumLifetime6Dim46Base8::LIFETIME as usize, + SchemeAbortingTargetSumLifetime6Dim46Base8::LIFETIME as usize, ); - test_signature_scheme_correctness::( + test_signature_scheme_correctness::( 11, 0, - SIGAbortingTargetSumLifetime6Dim46Base8::LIFETIME as usize, + SchemeAbortingTargetSumLifetime6Dim46Base8::LIFETIME as usize, ); } } diff --git a/src/symmetric/message_hash.rs b/src/symmetric/message_hash.rs index 95fc4e5..0ba2181 100644 --- a/src/symmetric/message_hash.rs +++ b/src/symmetric/message_hash.rs @@ -5,6 +5,8 @@ use rand::RngExt; use crate::MESSAGE_LENGTH; use crate::serialization::Serializable; +pub use poseidon::encode_message; + /// Trait to model a hash function used for message hashing. /// /// This is a variant of a tweakable hash function that we use for diff --git a/src/symmetric/message_hash/aborting.rs b/src/symmetric/message_hash/aborting.rs index 90818ff..7b8a971 100644 --- a/src/symmetric/message_hash/aborting.rs +++ b/src/symmetric/message_hash/aborting.rs @@ -14,6 +14,7 @@ use crate::array::FieldArray; /// Given p = Q * w^z + alpha, each Poseidon output field element A_i is: /// 1) checked to be less than Q * w^z, and if not the hash aborts /// 2) decomposed as d_i = floor(A_i / Q), then d_i is written in base w with z digits. +#[derive(Debug, Clone, Copy)] pub struct AbortingHypercubeMessageHash< const PARAMETER_LEN: usize, const RAND_LEN_FE: usize, diff --git a/src/symmetric/message_hash/poseidon.rs b/src/symmetric/message_hash/poseidon.rs index 5605cbf..f4fe81e 100644 --- a/src/symmetric/message_hash/poseidon.rs +++ b/src/symmetric/message_hash/poseidon.rs @@ -114,11 +114,11 @@ pub(crate) fn poseidon_message_hash_fe< let epoch_fe = encode_epoch::(epoch); // now, we hash randomness, parameters, epoch, message using PoseidonCompress - let combined_input_vec: Vec = randomness + let combined_input_vec: Vec = message_fe .iter() .chain(parameter.iter()) .chain(epoch_fe.iter()) - .chain(message_fe.iter()) + .chain(randomness.iter()) .copied() .collect(); diff --git a/src/symmetric/tweak_hash/poseidon.rs b/src/symmetric/tweak_hash/poseidon.rs index 286d09e..9f8e087 100644 --- a/src/symmetric/tweak_hash/poseidon.rs +++ b/src/symmetric/tweak_hash/poseidon.rs @@ -161,7 +161,7 @@ fn poseidon_safe_domain_separator( poseidon_compress::(perm, &input) } -/// Poseidon Sponge Hash Function +/// Poseidon T-Sponge with "Replacement" Hash Function /// /// Absorbs an arbitrary-length input using the Poseidon sponge construction /// and outputs `OUT_LEN` field elements. Domain separation is achieved by @@ -179,13 +179,22 @@ fn poseidon_safe_domain_separator( /// - `input`: message to hash (any length). /// /// ### Sponge Construction -/// This follows the classic sponge structure: -/// - **Absorption**: inputs are added chunk-by-chunk into the first `rate` elements of the state. -/// - **Squeezing**: outputs are read from the first `rate` elements of the state, permuted as needed. +/// This follows the classic sponge structure with capacity-first layout: +/// - The state is `[capacity | rate]`, i.e., the first elements hold the capacity, +/// followed by the rate elements. +/// - **Absorption**: inputs are written into the rate part of the state (`state[cap_len..]`). +/// - **Squeezing**: outputs are read from the rate part of the state, permuted as needed. +/// +/// ### "T-Sponge" +/// This means we use Poseidon in compresson mode (not a permutation), at each step. +/// +/// ### "Replacement" +/// This means we "replace" the rate elements of the state with the input chunk, instead +/// of adding (in the sense of finite field addition). /// /// ### Panics /// - If `capacity_value.len() >= WIDTH` -fn poseidon_sponge( +fn poseidon_replacement_t_sponge( perm: &P, capacity_value: &[A], input: &[A], @@ -200,11 +209,12 @@ where capacity_value.len() < WIDTH, "Capacity length must be smaller than the state width." ); - let rate = WIDTH - capacity_value.len(); + let cap_len = capacity_value.len(); + let rate = WIDTH - cap_len; // initialize let mut state = [A::ZERO; WIDTH]; - state[rate..].copy_from_slice(capacity_value); + state[..cap_len].copy_from_slice(capacity_value); // Instead of converting the input to a vector, resizing and feeding the data into the // sponge, we instead fill in the vector from all chunks until we are left with a non @@ -213,21 +223,23 @@ where // 1. fill in all full chunks and permute let mut it = input.chunks_exact(rate); for chunk in &mut it { - // add chunk elements into the first `rate` many elements of the `state` - for (s, &x) in state.iter_mut().take(rate).zip(chunk) { - *s += x; + // write chunk elements into the `rate` part of the state + for (s, &x) in state[cap_len..].iter_mut().zip(chunk) { + *s = x; // 'replacement' sponge } - perm.permute_mut(&mut state); + state = poseidon_compress::(perm, &state); // T-sponge } // 2. Fill the remainder and pad with zeros. // NOTE: This zero-padding is secure for constant-size inputs but may be insecure elsewhere. if !it.remainder().is_empty() { + let num_remainder = it.remainder().len(); for (i, x) in it.remainder().iter().enumerate() { - state[i] += *x; + state[cap_len + i] = *x; } - // Since we only *add* to the state, positions beyond the remainder remain zero - // (their initial value), so no explicit zero-padding is needed. - perm.permute_mut(&mut state); + for s in &mut state[cap_len + num_remainder..] { + *s = A::ZERO; + } + state = poseidon_compress::(perm, &state); // T-sponge } // 3. squeeze @@ -235,11 +247,11 @@ where let mut out_index = 0; while out_index < OUT_LEN { let chunk_size = (OUT_LEN - out_index).min(rate); - out[out_index..out_index + chunk_size].copy_from_slice(&state[..chunk_size]); + out[out_index..out_index + chunk_size].copy_from_slice(&state[cap_len..][..chunk_size]); out_index += chunk_size; if out_index < OUT_LEN { // no need to permute in last iteration, `state` is local variable - perm.permute_mut(&mut state); + state = poseidon_compress::(perm, &state); // T-sponge } } out @@ -249,7 +261,7 @@ where /// /// Note: HASH_LEN, TWEAK_LEN, CAPACITY, and PARAMETER_LEN must /// be given in the unit "number of field elements". -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] pub struct PoseidonTweakHash< const PARAMETER_LEN: usize, const HASH_LEN: usize, @@ -343,18 +355,17 @@ impl< match message { [single] => { - // we compress parameter, tweak, message + // we compress message, parameter, tweak let perm = poseidon1_16(); - // Build input on stack: [parameter | tweak | message] + // Build input on stack: [message | parameter | tweak] let mut combined_input = [F::ZERO; CHAIN_COMPRESSION_WIDTH]; - combined_input[..PARAMETER_LEN].copy_from_slice(¶meter.0); - combined_input[PARAMETER_LEN..PARAMETER_LEN + TWEAK_LEN].copy_from_slice(&tweak_fe); - combined_input[PARAMETER_LEN + TWEAK_LEN..PARAMETER_LEN + TWEAK_LEN + HASH_LEN] - .copy_from_slice(&single.0); + combined_input[..HASH_LEN].copy_from_slice(&single.0); + combined_input[HASH_LEN..][..PARAMETER_LEN].copy_from_slice(¶meter.0); + combined_input[HASH_LEN + PARAMETER_LEN..][..TWEAK_LEN].copy_from_slice(&tweak_fe); FieldArray( - poseidon_compress::( + poseidon_compress::<_, _, CHAIN_COMPRESSION_WIDTH, HASH_LEN>( &perm, &combined_input, ), @@ -376,7 +387,7 @@ impl< .copy_from_slice(&right.0); FieldArray( - poseidon_compress::( + poseidon_compress::<_, _, MERGE_COMPRESSION_WIDTH, HASH_LEN>( &perm, &combined_input, ), @@ -400,11 +411,12 @@ impl< HASH_LEN as u32, ]; let capacity_value = poseidon_safe_domain_separator::(&perm, &lengths); - FieldArray(poseidon_sponge::( - &perm, - &capacity_value, - &combined_input, - )) + FieldArray(poseidon_replacement_t_sponge::< + _, + _, + MERGE_COMPRESSION_WIDTH, + HASH_LEN, + >(&perm, &capacity_value, &combined_input)) } _ => FieldArray([F::ONE; HASH_LEN]), // Unreachable case, added for safety } @@ -593,9 +605,10 @@ impl< // Cache strategy: process one chain at a time to maximize locality. // All epochs for that chain stay in registers across iterations. - // Offsets for chain compression: [parameter | tweak | current_value] - let chain_tweak_offset = PARAMETER_LEN; - let chain_value_offset = PARAMETER_LEN + TWEAK_LEN; + // Offsets for chain compression: [current_value | parameter | tweak] + let chain_value_offset = 0; + let chain_parameter_offset = HASH_LEN; + let chain_tweak_offset = HASH_LEN + PARAMETER_LEN; for (chain_index, packed_chain) in packed_chains.iter_mut().enumerate().take(num_chains) @@ -607,11 +620,17 @@ impl< let pos = (step + 1) as u8; // Assemble the packed input for the hash function. - // Layout: [parameter | tweak | current_value] + // Layout: [current_value | parameter | tweak] let mut packed_input = [PackedF::ZERO; CHAIN_COMPRESSION_WIDTH]; + // Copy current chain value (already packed) + packed_input[chain_value_offset..chain_value_offset + HASH_LEN] + .copy_from_slice(packed_chain); + // Copy pre-packed parameter - packed_input[..PARAMETER_LEN].copy_from_slice(&packed_parameter); + packed_input + [chain_parameter_offset..chain_parameter_offset + PARAMETER_LEN] + .copy_from_slice(&packed_parameter); // Pack tweaks directly into destination pack_fn_into::( @@ -623,10 +642,6 @@ impl< }, ); - // Copy current chain value (already packed) - packed_input[chain_value_offset..chain_value_offset + HASH_LEN] - .copy_from_slice(packed_chain); - // Apply the hash function to advance the chain. // This single call processes all epochs in parallel. *packed_chain = @@ -678,7 +693,7 @@ impl< // Apply the sponge hash to produce the leaf. // This absorbs all chain ends and squeezes out the final hash. - poseidon_sponge::( + poseidon_replacement_t_sponge::( &sponge_perm, &capacity_val, packed_leaf_input, From 489b425a60c34cfe851dafa935ebd7f023bced54 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 24 Mar 2026 10:12:32 +0100 Subject: [PATCH 2/5] no T-sponge for now --- src/symmetric/tweak_hash/poseidon.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/symmetric/tweak_hash/poseidon.rs b/src/symmetric/tweak_hash/poseidon.rs index 9f8e087..e70049f 100644 --- a/src/symmetric/tweak_hash/poseidon.rs +++ b/src/symmetric/tweak_hash/poseidon.rs @@ -161,7 +161,7 @@ fn poseidon_safe_domain_separator( poseidon_compress::(perm, &input) } -/// Poseidon T-Sponge with "Replacement" Hash Function +/// Poseidon Sponge with "Replacement" Hash Function /// /// Absorbs an arbitrary-length input using the Poseidon sponge construction /// and outputs `OUT_LEN` field elements. Domain separation is achieved by @@ -185,9 +185,6 @@ fn poseidon_safe_domain_separator( /// - **Absorption**: inputs are written into the rate part of the state (`state[cap_len..]`). /// - **Squeezing**: outputs are read from the rate part of the state, permuted as needed. /// -/// ### "T-Sponge" -/// This means we use Poseidon in compresson mode (not a permutation), at each step. -/// /// ### "Replacement" /// This means we "replace" the rate elements of the state with the input chunk, instead /// of adding (in the sense of finite field addition). @@ -227,7 +224,7 @@ where for (s, &x) in state[cap_len..].iter_mut().zip(chunk) { *s = x; // 'replacement' sponge } - state = poseidon_compress::(perm, &state); // T-sponge + perm.permute_mut(&mut state); } // 2. Fill the remainder and pad with zeros. // NOTE: This zero-padding is secure for constant-size inputs but may be insecure elsewhere. @@ -239,7 +236,7 @@ where for s in &mut state[cap_len + num_remainder..] { *s = A::ZERO; } - state = poseidon_compress::(perm, &state); // T-sponge + perm.permute_mut(&mut state); } // 3. squeeze @@ -251,7 +248,7 @@ where out_index += chunk_size; if out_index < OUT_LEN { // no need to permute in last iteration, `state` is local variable - state = poseidon_compress::(perm, &state); // T-sponge + perm.permute_mut(&mut state); } } out From 678c7543aa42cbefc2a3e9befe928403772d07d8 Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 24 Mar 2026 16:49:08 +0100 Subject: [PATCH 3/5] naming --- src/symmetric/tweak_hash/poseidon.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/symmetric/tweak_hash/poseidon.rs b/src/symmetric/tweak_hash/poseidon.rs index e70049f..9b8ebc1 100644 --- a/src/symmetric/tweak_hash/poseidon.rs +++ b/src/symmetric/tweak_hash/poseidon.rs @@ -191,7 +191,7 @@ fn poseidon_safe_domain_separator( /// /// ### Panics /// - If `capacity_value.len() >= WIDTH` -fn poseidon_replacement_t_sponge( +fn poseidon_replacement_sponge( perm: &P, capacity_value: &[A], input: &[A], @@ -408,7 +408,7 @@ impl< HASH_LEN as u32, ]; let capacity_value = poseidon_safe_domain_separator::(&perm, &lengths); - FieldArray(poseidon_replacement_t_sponge::< + FieldArray(poseidon_replacement_sponge::< _, _, MERGE_COMPRESSION_WIDTH, @@ -690,7 +690,7 @@ impl< // Apply the sponge hash to produce the leaf. // This absorbs all chain ends and squeezes out the final hash. - poseidon_replacement_t_sponge::( + poseidon_replacement_sponge::( &sponge_perm, &capacity_val, packed_leaf_input, From cdd95241f5be2018548facb8f9e94b813c6f212d Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 24 Mar 2026 16:51:25 +0100 Subject: [PATCH 4/5] rm modifs --- MODIFS.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 MODIFS.md diff --git a/MODIFS.md b/MODIFS.md deleted file mode 100644 index 2706001..0000000 --- a/MODIFS.md +++ /dev/null @@ -1,5 +0,0 @@ -# Modifs - -- replacement sponge -- t-sponge -- data ordering in chains \ No newline at end of file From 6b03aaa6e17317428e6c2d5ad0f425c3290a785a Mon Sep 17 00:00:00 2001 From: Tom Wambsgans Date: Tue, 24 Mar 2026 18:08:48 +0100 Subject: [PATCH 5/5] clippy --- .../generalized_xmss/instantiations_aborting.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/signature/generalized_xmss/instantiations_aborting.rs b/src/signature/generalized_xmss/instantiations_aborting.rs index 9ac132a..e386055 100644 --- a/src/signature/generalized_xmss/instantiations_aborting.rs +++ b/src/signature/generalized_xmss/instantiations_aborting.rs @@ -4,7 +4,8 @@ pub mod lifetime_2_to_the_32 { use crate::{ inc_encoding::target_sum::TargetSumEncoding, signature::generalized_xmss::{ - GeneralizedXMSSPublicKey, GeneralizedXMSSSecretKey, GeneralizedXMSSSignature, GeneralizedXMSSSignatureScheme + GeneralizedXMSSPublicKey, GeneralizedXMSSSecretKey, GeneralizedXMSSSignature, + GeneralizedXMSSSignatureScheme, }, symmetric::{ message_hash::aborting::AbortingHypercubeMessageHash, prf::shake_to_field::ShakePRFtoF, @@ -46,14 +47,15 @@ pub mod lifetime_2_to_the_32 { pub type SchemeAbortingTargetSumLifetime32Dim64Base8 = GeneralizedXMSSSignatureScheme; pub type PubKeyAbortingTargetSumLifetime32Dim64Base8 = GeneralizedXMSSPublicKey; - pub type SecretKeyAbortingTargetSumLifetime32Dim64Base8 = GeneralizedXMSSSecretKey; + pub type SecretKeyAbortingTargetSumLifetime32Dim64Base8 = + GeneralizedXMSSSecretKey; pub type SigAbortingTargetSumLifetime32Dim64Base8 = GeneralizedXMSSSignature; #[cfg(test)] mod test { #[cfg(feature = "slow-tests")] - use super::*; + use super::SchemeAbortingTargetSumLifetime32Dim64Base8; #[cfg(feature = "slow-tests")] use crate::signature::SignatureScheme; @@ -63,15 +65,15 @@ pub mod lifetime_2_to_the_32 { #[test] #[cfg(feature = "slow-tests")] pub fn test_correctness() { - test_signature_scheme_correctness::( + test_signature_scheme_correctness::( 213, 0, - SIGAbortingTargetSumLifetime32Dim64Base8::LIFETIME as usize, + SchemeAbortingTargetSumLifetime32Dim64Base8::LIFETIME as usize, ); - test_signature_scheme_correctness::( + test_signature_scheme_correctness::( 4, 0, - SIGAbortingTargetSumLifetime32Dim64Base8::LIFETIME as usize, + SchemeAbortingTargetSumLifetime32Dim64Base8::LIFETIME as usize, ); } }