From d54999d95893fe3e8e53f55e727bf19fb0482706 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Sat, 4 Apr 2026 11:44:25 +0200 Subject: [PATCH 1/3] core: more compile time assertions and doc --- src/inc_encoding/target_sum.rs | 57 ++++++++++++++- src/signature/generalized_xmss.rs | 87 ++++++++++++++++++---- src/symmetric/message_hash/aborting.rs | 99 ++++++++++++++++++++------ src/symmetric/message_hash/poseidon.rs | 88 ++++++++++++++++++----- src/symmetric/tweak_hash/poseidon.rs | 78 +++++++++++++++++--- 5 files changed, 349 insertions(+), 60 deletions(-) diff --git a/src/inc_encoding/target_sum.rs b/src/inc_encoding/target_sum.rs index 7360719..d8f4188 100644 --- a/src/inc_encoding/target_sum.rs +++ b/src/inc_encoding/target_sum.rs @@ -61,8 +61,33 @@ impl IncomparableEncoding randomness: &Self::Randomness, epoch: u32, ) -> Result, Self::Error> { + // Compile-time parameter validation for Target Sum Encoding + // + // This encoding implements Construction 6 (IE for Target Sum Winternitz) + // from DKKW25. It maps a message to a codeword x ∈ C ⊆ Z_w^v, where: + // + // C = { (x_1, ..., x_v) ∈ {0, ..., w-1}^v | Σ x_i = T } + // + // The code C enforces the *incomparability* property (Definition 13): + // no two distinct codewords x, x' satisfy x_i ≥ x'_i for all i. + // This is critical for the security of the XMSS signature scheme. + // + // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" + // (DKKW25, IACR CiC 2(1), 2025) + // HHKTW26: "Aborting Random Oracles" (ePrint 2026/016) const { - // base and dimension must not be too large + // Representation constraints + // + // In the Generalized XMSS construction (Construction 3, DKKW25), + // each chain position and chain index is encoded as a single byte + // in the tweak function (Eq. 17): + // + // tweak(ep, i, k) = (0x00 || ep || i || k) + // 8b ⌈log L⌉ ⌈log v⌉ w bits + // + // - Since chain_index `i` is stored as u8, we need v ≤ 256. + // - Since pos_in_chain `k` is stored as u8, we need w ≤ 256. + // - Codeword entries (chunks) are also stored as u8 in signatures. assert!( MH::BASE <= 1 << 8, "Target Sum Encoding: Base must be at most 2^8" @@ -71,6 +96,36 @@ impl IncomparableEncoding MH::DIMENSION <= 1 << 8, "Target Sum Encoding: Dimension must be at most 2^8" ); + + // Encoding well-formedness + // + // Definition 13 (DKKW25): an incomparable encoding maps messages + // to codewords in {0, ..., w-1}^v. For the incomparability + // property to be meaningful, we need w ≥ 2 (otherwise every + // codeword is the zero vector, and distinct codewords cannot + // exist). + assert!( + MH::BASE >= 2, + "Target Sum Encoding: Base must be at least 2" + ); + + // Target sum range + // + // Construction 6 (DKKW25) defines the code: + // + // C = { x ∈ {0,...,w-1}^v | Σ x_i = T } + // + // For C to be non-empty, T must be achievable: each x_i can + // contribute at most w-1 to the sum, so T ≤ v*(w-1). The lower + // bound T ≥ 0 is guaranteed by the usize type. + // + // Choosing T close to v*(w-1)/2 (the expected sum of a uniform + // hash) maximizes |C| and minimizes the signing retry rate + // (Lemma 7, DKKW25). + assert!( + TARGET_SUM <= MH::DIMENSION * (MH::BASE - 1), + "Target Sum Encoding: TARGET_SUM must be at most DIMENSION * (BASE - 1)" + ); } // apply the message hash first to get chunks diff --git a/src/signature/generalized_xmss.rs b/src/signature/generalized_xmss.rs index 6524470..43d6914 100644 --- a/src/signature/generalized_xmss.rs +++ b/src/signature/generalized_xmss.rs @@ -658,32 +658,91 @@ where activation_epoch: usize, num_active_epochs: usize, ) -> (Self::PublicKey, Self::SecretKey) { + // Compile-time parameter validation for Generalized XMSS + // + // This implements Construction 3 (Generalized XMSS) from DKKW25. + // The scheme structure is: + // + // Public key: pk = (root, P) where root is a Merkle root over + // L = 2^h one-time public keys. + // + // Signature: σ = (ρ, σ_OTS, path_ep) + // - ρ: randomness for the incomparable encoding + // - σ_OTS: v chain hashes (one per chain) + // - path_ep: Merkle authentication path of length h + // + // Key generation builds the Merkle tree using the "top-bottom" + // approach: the full tree of depth h is split at h/2 into one + // top tree and sqrt(L) bottom trees, enabling a sliding window of + // two bottom trees in the secret key. + // + // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" + // (DKKW25, IACR CiC 2(1), 2025) const { - // assert BASE and DIMENSION are small enough to make sure that we can fit - // pos_in_chain and chain_index in u8. + // Encoding well-formedness + // + // Definition 13 (DKKW25): the incomparable encoding maps + // messages to codewords x ∈ C ⊆ {0, ..., w-1}^v. For the + // incomparability property to hold, we need: + // - w >= 2: a single-element alphabet makes all codewords + // identical, so incomparability is vacuous. + // - v >= 1: codewords must have at least one coordinate. + assert!( + IE::BASE >= 2, + "Generalized XMSS: Encoding base (w) must be at least 2" + ); + assert!( + IE::DIMENSION >= 1, + "Generalized XMSS: Encoding dimension (v) must be at least 1" + ); + + // Representation constraints + // + // The chain tweak function (Eq. 17, DKKW25) encodes: + // + // tweak(ep, i, k) = (0x00 || ep || i || k) + // 8 bits ceil(log L) ceil(log v) w bits + // + // chain_index `i` and pos_in_chain `k` are stored as u8, and + // chunk values in signatures are also u8. Therefore: + // - BASE (= w) <= 256 (chunk fits in u8) + // - DIMENSION (= v) <= 256 (chain_index fits in u8) assert!( IE::BASE <= 1 << 8, - "Generalized XMSS: Encoding base too large, must be at most 2^8" + "Generalized XMSS: Encoding base (w) must fit in u8 (<= 256)" ); assert!( IE::DIMENSION <= 1 << 8, - "Generalized XMSS: Encoding dimension too large, must be at most 2^8" + "Generalized XMSS: Encoding dimension (v) must fit in u8 (<= 256)" ); - // LOG_LIFETIME needs to be even, so that we can use the top-bottom tree approach + // Merkle tree structure + // + // The key lifetime is L = 2^LOG_LIFETIME epochs. The Merkle tree + // has depth h = LOG_LIFETIME, with L leaves (one per epoch). + // + // The top-bottom optimization splits the tree at depth h/2, + // creating one top tree of depth h/2 and sqrt(L) bottom trees of + // depth h/2. This requires h to be even. assert!( LOG_LIFETIME.is_multiple_of(2), - "Generalized XMSS: LOG_LIFETIME must be multiple of two" + "Generalized XMSS: LOG_LIFETIME must be even (top-bottom tree split)" ); - // sign() and verify() take epoch as u32, so LOG_LIFETIME > 32 would create - // epochs unreachable by the signing/verification API. - const { - assert!( - LOG_LIFETIME <= 32, - "Generalized XMSS: LOG_LIFETIME must be at most 32 (epoch type is u32)" - ); - } + // The smallest valid even LOG_LIFETIME is 2, giving L = 4 epochs, + // a top tree of depth 1, and 2 bottom trees of depth 1. + // LOG_LIFETIME = 0 would mean L = 1 (no internal Merkle nodes). + assert!( + LOG_LIFETIME >= 2, + "Generalized XMSS: LOG_LIFETIME must be at least 2" + ); + + // The sign() and verify() APIs take the epoch as u32, so + // LOG_LIFETIME > 32 would create epochs that cannot be addressed. + assert!( + LOG_LIFETIME <= 32, + "Generalized XMSS: LOG_LIFETIME must be at most 32 (epoch is u32)" + ); } // Overflow-safe validation of the requested activation interval. diff --git a/src/symmetric/message_hash/aborting.rs b/src/symmetric/message_hash/aborting.rs index 90818ff..321a61a 100644 --- a/src/symmetric/message_hash/aborting.rs +++ b/src/symmetric/message_hash/aborting.rs @@ -77,50 +77,109 @@ where randomness: &Self::Randomness, message: &[u8; MESSAGE_LENGTH], ) -> Result, HypercubeHashError> { + // Compile-time parameter validation for AbortingHypercubeMessageHash + // + // This hash implements H^hc_{w,v,z,Q} from §6.1 of HHKTW26. It uses + // rejection sampling to uniformly map Poseidon field elements into + // the hypercube Z_w^v, avoiding big-integer arithmetic entirely: + // + // 1. Compute (A_1, ..., A_ℓ) := Poseidon(R || P || T || M) + // 2. For each A_i: reject if A_i ≥ Q·w^z (ensures uniformity) + // 3. Decompose d_i = ⌊A_i / Q⌋ into z base-w digits + // 4. Collect the first v digits as the output + // + // The field prime decomposes as p = Q·w^z + α (α ≥ 0). + // Rejection happens with per-element probability α/p, and the + // overall abort probability is θ = 1 - ((Q·w^z)/p)^ℓ (Lemma 8). + // + // By Theorem 4 of HHKTW26, this construction is indifferentiable + // from a θ-aborting random oracle when Poseidon is modeled as a + // standard random oracle. + // + // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" + // (DKKW25, IACR CiC 2(1), 2025) + // HHKTW26: "Aborting Random Oracles" (ePrint 2026/016) const { - // Check that Poseidon of width 24 is enough + // Poseidon capacity constraints + // + // We use Poseidon in compression mode with a width-24 permutation. + // All inputs must fit in one call, and the output is extracted + // from the same state. assert!( PARAMETER_LEN + RAND_LEN_FE + TWEAK_LEN_FE + MSG_LEN_FE <= 24, - "Poseidon of width 24 is not enough" + "Poseidon of width 24 is not enough for the input" ); - assert!(HASH_LEN_FE <= 24, "Poseidon of width 24 is not enough"); - - // Check that we have enough hash output field elements assert!( - HASH_LEN_FE >= DIMENSION.div_ceil(Z), - "Not enough hash output field elements for the requested dimension" + HASH_LEN_FE <= 24, + "Poseidon of width 24 is not enough for the output" ); + + // Poseidon compression mode can only produce as many output + // field elements as there are input elements. assert!( PARAMETER_LEN + RAND_LEN_FE + TWEAK_LEN_FE + MSG_LEN_FE >= HASH_LEN_FE, "Input shorter than requested output" ); - // Base check + // Hypercube decomposition parameters + // + // Each good field element A_i < Q·w^z is decomposed into z + // base-w digits, so we need ℓ = ⌈v/z⌉ field elements to get + // at least v digits. HASH_LEN_FE must supply enough elements. assert!( - BASE <= 1 << 8, - "Aborting Hypercube Message Hash: Base must be at most 2^8" + DIMENSION >= 1, + "Aborting Hypercube: DIMENSION (v) must be at least 1" + ); + assert!( + Z >= 1, + "Aborting Hypercube: Z (digits per field element) must be at least 1" + ); + assert!( + HASH_LEN_FE >= DIMENSION.div_ceil(Z), + "Not enough hash output field elements: need ceil(v/z)" ); - // Check that Q * w^z fits within the field + // Q is the quotient in the decomposition A_i = Q·d_i + c_i, + // where c_i ∈ {0, ..., Q-1} is discarded and d_i ∈ {0, ..., w^z-1} + // carries the uniform digits. Q must be positive for a valid range. + assert!(Q >= 1, "Aborting Hypercube: Q must be at least 1"); + + // The rejection threshold Q·w^z must not exceed the field order p, + // since field elements A_i live in {0, ..., p-1}. The remainder + // α = p - Q·w^z determines the per-element abort probability α/p. + // + // Example (KoalaBear): p = 2^31 - 2^24 + 1 = 127·8^8 + 1 + // ⟹ Q=127, w=8, z=8, α=1, abort prob ≈ 4.7e-10 per element. assert!( Q as u64 * (BASE as u64).pow(Z as u32) <= F::ORDER_U64, - "Q * w^z exceeds field order" + "Q * w^z exceeds field order p" ); - // floor(log2(ORDER)) - let bits_per_fe = F::ORDER_U64.ilog2() as usize; + // Representation constraints + // + // Same as the Poseidon message hash: chunks and chain indices + // are stored as u8 in signatures and tweak encodings. + assert!( + BASE >= 2, + "Aborting Hypercube: BASE (w) must be at least 2 (Definition 13, DKKW25)" + ); + assert!( + BASE <= 1 << 8, + "Aborting Hypercube: BASE (w) must fit in u8" + ); - // Check that we have enough bits to encode message + // Injective encoding of inputs + // + // Same requirements as the standard Poseidon message hash: + // message and epoch must be losslessly encodable as field elements. + let bits_per_fe = F::ORDER_U64.ilog2() as usize; assert!( bits_per_fe * MSG_LEN_FE >= 8 * MESSAGE_LENGTH, - "Aborting Hypercube Message Hash: not enough field elements to encode the message" + "Aborting Hypercube: not enough field elements to encode the message" ); - - // Check that we have enough bits to encode tweak - // Epoch is a u32, and we have one domain separator byte assert!( bits_per_fe * TWEAK_LEN_FE >= 40, - "Aborting Hypercube Message Hash: not enough field elements to encode the epoch tweak" + "Aborting Hypercube: not enough field elements to encode the epoch tweak" ); } diff --git a/src/symmetric/message_hash/poseidon.rs b/src/symmetric/message_hash/poseidon.rs index 5605cbf..13e53ea 100644 --- a/src/symmetric/message_hash/poseidon.rs +++ b/src/symmetric/message_hash/poseidon.rs @@ -186,15 +186,48 @@ where randomness: &Self::Randomness, message: &[u8; MESSAGE_LENGTH], ) -> Result, Infallible> { + // Compile-time parameter validation for Poseidon Message Hash + // + // This hash implements Th_msg from §7.3.1 of DKKW25. It takes: + // + // Th_msg(P, T, M, R) = Decode_{p,η',w}( + // PoseidonCompress_{p, t_msg, η'}(R || P || EncT(T) || EncM(M)) + // ) + // + // The output is a vector of DIMENSION chunks, each in {0, ..., BASE-1}. + // This is used inside the incomparable encoding to map (message, seed) + // to a hypercube vertex x ∈ Z_w^v before the target sum filter. + // + // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" + // (DKKW25, IACR CiC 2(1), 2025) const { - // Check that Poseidon of width 24 is enough + // Poseidon capacity constraints + // + // We use Poseidon in compression mode with a width-24 permutation. + // The entire input (R || P || T || M) must fit in one call, and + // the output η' field elements are extracted from the same state. assert!( PARAMETER_LEN + TWEAK_LEN_FE + RAND_LEN_FE + MSG_LEN_FE <= 24, - "Poseidon of width 24 is not enough" + "Poseidon of width 24 is not enough for the input" + ); + assert!( + HASH_LEN_FE <= 24, + "Poseidon of width 24 is not enough for the output" ); - assert!(HASH_LEN_FE <= 24, "Poseidon of width 24 is not enough"); - // Base and dimension check + // Non-trivial output + // + // The hash must produce at least one field element to be useful. + assert!( + HASH_LEN_FE >= 1, + "Poseidon Message Hash: HASH_LEN_FE must be at least 1" + ); + + // Representation constraints + // + // Construction 3 (DKKW25) stores chunk values and chain indices + // as u8 in the signature and tweak encodings (Eq. 17). + // BASE (= w) and DIMENSION (= v) must each fit in one byte. assert!( BASE <= 1 << 8, "Poseidon Message Hash: Base must be at most 2^8" @@ -204,30 +237,53 @@ where "Poseidon Message Hash: Dimension must be at most 2^8" ); - // how many bits can be represented by one field element: floor(log2(ORDER)) + // Encoding well-formedness + // + // Definition 13 (DKKW25): the incomparable encoding maps into + // {0, ..., w-1}^v with w ≥ 2. A single-element alphabet makes + // the code trivial and incomparability vacuous. + assert!(BASE >= 2, "Poseidon Message Hash: BASE must be at least 2"); + + // Injective encoding of inputs + // + // The message (32 bytes = 256 bits) is encoded into MSG_LEN_FE + // field elements via base-p decomposition. For this to be + // injective, the output space must be at least as large: + // + // p^MSG_LEN_FE ≥ 2^(8 * MESSAGE_LENGTH) + // ⟹ ⌊log2(p)⌋ * MSG_LEN_FE ≥ 8 * MESSAGE_LENGTH let bits_per_fe = F::ORDER_U64.ilog2() as usize; - - // Check that we have enough bits to encode message assert!( bits_per_fe * MSG_LEN_FE >= 8 * MESSAGE_LENGTH, - "Poseidon Message Hash: Parameter mismatch: not enough field elements to encode the message" + "Poseidon Message Hash: not enough field elements to encode the message" ); - // Check that we have enough bits to encode tweak - // Epoch is a u32, and we have one domain separator byte + // The epoch tweak (Eq. 19, DKKW25) packs a u32 epoch and an + // 8-bit domain separator into one value, requiring 40 bits total: + // + // tweakm(ep) = (0x02 || ep) ← 8 + 32 = 40 bits + // sep epoch + // + // The field element representation needs enough capacity: + // ⌊log2(p)⌋ * TWEAK_LEN_FE ≥ 40 assert!( bits_per_fe * TWEAK_LEN_FE >= 40, - "Poseidon Message Hash: Parameter mismatch: not enough field elements to encode the epoch tweak" + "Poseidon Message Hash: not enough field elements to encode the epoch tweak" ); - // Check that decoding from field elements to chunks can be done - // injectively, i.e., we have enough chunks - // chunk_size = ceil(log2(BASE)) - assert!(BASE > 1, "Poseidon Message Hash: BASE must be > 1"); + // Injective decoding to chunks + // + // The hash output (η' field elements) is decoded via Decode_{p,η',w} + // (§7.3.1, DKKW25): interpret the η' elements as one big integer + // and express it in base w to get DIMENSION chunks. + // + // For injectivity, the big integer must have enough room: + // p^η' ≤ w^DIMENSION + // ⟹ ⌊log2(p)⌋ * HASH_LEN_FE ≤ DIMENSION * ⌈log2(w)⌉ let chunk_size = (usize::BITS - (BASE - 1).leading_zeros()) as usize; assert!( bits_per_fe * HASH_LEN_FE <= DIMENSION * chunk_size, - "Poseidon Message Hash: Parameter mismatch: not enough bits to decode the hash" + "Poseidon Message Hash: not enough chunks to decode the hash output" ); } diff --git a/src/symmetric/tweak_hash/poseidon.rs b/src/symmetric/tweak_hash/poseidon.rs index 286d09e..2f4c5a9 100644 --- a/src/symmetric/tweak_hash/poseidon.rs +++ b/src/symmetric/tweak_hash/poseidon.rs @@ -300,37 +300,97 @@ impl< tweak: &Self::Tweak, message: &[Self::Domain], ) -> Self::Domain { + // Compile-time parameter validation for PoseidonTweakHash + // + // This implements the tweakable hash function Th from §7.3.2 of + // DKKW25. It is used in three distinct contexts: + // + // (1) Chain hashing: Th(P, T, M) for M ∈ H (width 16) + // (2) Tree hashing: Th(P, T, M) for M ∈ H² (width 24) + // (3) Leaf hashing: Th(P, T, M) for M ∈ H^v (sponge) + // + // The tweak T provides domain separation, ensuring that each + // hash invocation in the scheme uses a unique (ep, i, k) or + // (level, pos) address. This is essential for the multi-target + // security reductions in Theorem 1 (DKKW25). + // + // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" + // (DKKW25, IACR CiC 2(1), 2025) const { + // The hash output space H = F_p^η (Parameter Req. 3, DKKW25) + // and public parameter space P = F_p^l_p must both be non-empty. + assert!( + HASH_LEN >= 1, + "Poseidon Tweak Hash: HASH_LEN (η) must be at least 1" + ); + assert!( + PARAMETER_LEN >= 1, + "Poseidon Tweak Hash: PARAMETER_LEN (l_p) must be at least 1" + ); + + // Poseidon capacity for sponge mode + // + // Leaf hashing (case 3) uses a sponge construction with + // rate = 24 - CAPACITY. The capacity must leave room for at + // least one rate element, so CAPACITY < 24. + // + // The capacity also controls the sponge security level: + // classical security ≤ c·log(p)/2 bits (§7.3, DKKW25, Eq. 21). assert!( CAPACITY < 24, - "Poseidon Tweak Chain Hash: Capacity must be less than 24" + "Poseidon Tweak Hash: Capacity must be less than 24" ); + + // Compression mode width constraints + // + // Case (1) — Chain hashing: uses PoseidonCompress with width 16. + // Input layout: [P || T || M] must fit in 16 field elements. assert!( PARAMETER_LEN + TWEAK_LEN + HASH_LEN <= 16, - "Poseidon Tweak Chain Hash: Input lengths too large for Poseidon instance" + "Poseidon Tweak Chain Hash: P+T+M exceeds width-16 capacity" ); + + // Case (2) — Tree hashing: uses PoseidonCompress with width 24. + // Input layout: [P || T || left || right] must fit in 24 elements. assert!( PARAMETER_LEN + TWEAK_LEN + 2 * HASH_LEN <= 24, - "Poseidon Tweak Tree Hash: Input lengths too large for Poseidon instance" + "Poseidon Tweak Tree Hash: P+T+2H exceeds width-24 capacity" ); - // floor(log2(ORDER)) + // Leaf hashing domain separator + // + // Case (3) — Leaf hashing: the sponge capacity value V_c is + // derived from 4 domain parameters (each 32 bits = 128 bits + // total) via a width-24 Poseidon compression. The full 24 + // field elements must have enough bit capacity. let bits_per_fe = F::ORDER_U64.ilog2() as usize; assert!( bits_per_fe * 24 >= DOMAIN_PARAMETERS_LENGTH * 32, - "Poseidon Tweak Leaf Hash: not enough field elements to hash the domain separator" + "Poseidon Tweak Leaf Hash: width-24 cannot hold domain separator" ); - // tree tweak: 32 (pos_in_level) + 8 (level) = 40 bits - // chain tweak: 32 (epoch) + 8 (chain_index) + 8 (pos_in_chain) + 8 (separator) = 56 bits + // Tweak encoding capacity + // + // The tweak functions (Eqs. 17-18, DKKW25) pack structured + // addresses into TWEAK_LEN field elements. The two formats are: + // + // Tree tweak (Eq. 18): (0x01 || level || pos_in_level) + // separator: 8 bits, level: ceil(log(h)) bits, pos: ceil(log L) bits + // Minimum: 8 + 32 = 40 bits (level fits in remaining bits) + // + // Chain tweak (Eq. 17): (0x00 || epoch || chain_index || pos_in_chain) + // separator: 8 bits, epoch: 32 bits, chain_index: 8 bits, pos: 8 bits + // Total: 56 bits + // + // The field element capacity must accommodate the wider format. let tweak_fe_bits = bits_per_fe * TWEAK_LEN; assert!( tweak_fe_bits >= 40, - "Poseidon Tweak Hash: not enough field elements to encode the tree tweak" + "Poseidon Tweak Hash: TWEAK_LEN too small for tree tweak (need >= 40 bits)" ); assert!( tweak_fe_bits >= 56, - "Poseidon Tweak Hash: not enough field elements to encode the chain tweak" + "Poseidon Tweak Hash: TWEAK_LEN too small for chain tweak (need >= 56 bits)" ); } From d3939712f6ab7c98c42c7960b89709d51d66693e Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Sat, 4 Apr 2026 11:49:38 +0200 Subject: [PATCH 2/3] clippy --- src/signature/generalized_xmss.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/signature/generalized_xmss.rs b/src/signature/generalized_xmss.rs index 43d6914..ea44789 100644 --- a/src/signature/generalized_xmss.rs +++ b/src/signature/generalized_xmss.rs @@ -653,6 +653,7 @@ where const LIFETIME: u64 = 1 << LOG_LIFETIME; + #[allow(clippy::too_many_lines)] fn key_gen( rng: &mut R, activation_epoch: usize, From 021d6fcb6393deab853c20aeaab22b6c3fe0e21e Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Wed, 8 Apr 2026 22:40:24 +0200 Subject: [PATCH 3/3] fix comments --- src/inc_encoding/target_sum.rs | 9 ++-- src/signature/generalized_xmss.rs | 22 +------- src/symmetric/message_hash/aborting.rs | 19 ++++--- src/symmetric/message_hash/poseidon.rs | 13 +++-- src/symmetric/tweak_hash/poseidon.rs | 75 +++++++------------------- 5 files changed, 40 insertions(+), 98 deletions(-) diff --git a/src/inc_encoding/target_sum.rs b/src/inc_encoding/target_sum.rs index d8f4188..50b1930 100644 --- a/src/inc_encoding/target_sum.rs +++ b/src/inc_encoding/target_sum.rs @@ -72,15 +72,14 @@ impl IncomparableEncoding // no two distinct codewords x, x' satisfy x_i ≥ x'_i for all i. // This is critical for the security of the XMSS signature scheme. // - // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" - // (DKKW25, IACR CiC 2(1), 2025) - // HHKTW26: "Aborting Random Oracles" (ePrint 2026/016) + // DKKW25: https://eprint.iacr.org/2025/055 + // HHKTW26: https://eprint.iacr.org/2026/016 const { // Representation constraints // - // In the Generalized XMSS construction (Construction 3, DKKW25), + // In the Generalized XMSS construction (DKKW25), // each chain position and chain index is encoded as a single byte - // in the tweak function (Eq. 17): + // in the tweak function: // // tweak(ep, i, k) = (0x00 || ep || i || k) // 8b ⌈log L⌉ ⌈log v⌉ w bits diff --git a/src/signature/generalized_xmss.rs b/src/signature/generalized_xmss.rs index ea44789..69f1756 100644 --- a/src/signature/generalized_xmss.rs +++ b/src/signature/generalized_xmss.rs @@ -659,26 +659,6 @@ where activation_epoch: usize, num_active_epochs: usize, ) -> (Self::PublicKey, Self::SecretKey) { - // Compile-time parameter validation for Generalized XMSS - // - // This implements Construction 3 (Generalized XMSS) from DKKW25. - // The scheme structure is: - // - // Public key: pk = (root, P) where root is a Merkle root over - // L = 2^h one-time public keys. - // - // Signature: σ = (ρ, σ_OTS, path_ep) - // - ρ: randomness for the incomparable encoding - // - σ_OTS: v chain hashes (one per chain) - // - path_ep: Merkle authentication path of length h - // - // Key generation builds the Merkle tree using the "top-bottom" - // approach: the full tree of depth h is split at h/2 into one - // top tree and sqrt(L) bottom trees, enabling a sliding window of - // two bottom trees in the secret key. - // - // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" - // (DKKW25, IACR CiC 2(1), 2025) const { // Encoding well-formedness // @@ -699,7 +679,7 @@ where // Representation constraints // - // The chain tweak function (Eq. 17, DKKW25) encodes: + // The chain tweak function (DKKW25) encodes: // // tweak(ep, i, k) = (0x00 || ep || i || k) // 8 bits ceil(log L) ceil(log v) w bits diff --git a/src/symmetric/message_hash/aborting.rs b/src/symmetric/message_hash/aborting.rs index 321a61a..54519c9 100644 --- a/src/symmetric/message_hash/aborting.rs +++ b/src/symmetric/message_hash/aborting.rs @@ -96,9 +96,8 @@ where // from a θ-aborting random oracle when Poseidon is modeled as a // standard random oracle. // - // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" - // (DKKW25, IACR CiC 2(1), 2025) - // HHKTW26: "Aborting Random Oracles" (ePrint 2026/016) + // DKKW25: https://eprint.iacr.org/2025/055 + // HHKTW26: https://eprint.iacr.org/2026/016 const { // Poseidon capacity constraints // @@ -128,11 +127,11 @@ where // at least v digits. HASH_LEN_FE must supply enough elements. assert!( DIMENSION >= 1, - "Aborting Hypercube: DIMENSION (v) must be at least 1" + "AbortingHypercubeMessageHash: DIMENSION (v) must be at least 1" ); assert!( Z >= 1, - "Aborting Hypercube: Z (digits per field element) must be at least 1" + "AbortingHypercubeMessageHash: Z (digits per field element) must be at least 1" ); assert!( HASH_LEN_FE >= DIMENSION.div_ceil(Z), @@ -142,7 +141,7 @@ where // Q is the quotient in the decomposition A_i = Q·d_i + c_i, // where c_i ∈ {0, ..., Q-1} is discarded and d_i ∈ {0, ..., w^z-1} // carries the uniform digits. Q must be positive for a valid range. - assert!(Q >= 1, "Aborting Hypercube: Q must be at least 1"); + assert!(Q >= 1, "AbortingHypercubeMessageHash: Q must be at least 1"); // The rejection threshold Q·w^z must not exceed the field order p, // since field elements A_i live in {0, ..., p-1}. The remainder @@ -161,11 +160,11 @@ where // are stored as u8 in signatures and tweak encodings. assert!( BASE >= 2, - "Aborting Hypercube: BASE (w) must be at least 2 (Definition 13, DKKW25)" + "AbortingHypercubeMessageHash: BASE (w) must be at least 2 (Definition 13, DKKW25)" ); assert!( BASE <= 1 << 8, - "Aborting Hypercube: BASE (w) must fit in u8" + "AbortingHypercubeMessageHash: BASE (w) must fit in u8" ); // Injective encoding of inputs @@ -175,11 +174,11 @@ where let bits_per_fe = F::ORDER_U64.ilog2() as usize; assert!( bits_per_fe * MSG_LEN_FE >= 8 * MESSAGE_LENGTH, - "Aborting Hypercube: not enough field elements to encode the message" + "AbortingHypercubeMessageHash: not enough field elements to encode the message" ); assert!( bits_per_fe * TWEAK_LEN_FE >= 40, - "Aborting Hypercube: not enough field elements to encode the epoch tweak" + "AbortingHypercubeMessageHash: not enough field elements to encode the epoch tweak" ); } diff --git a/src/symmetric/message_hash/poseidon.rs b/src/symmetric/message_hash/poseidon.rs index 13e53ea..c86791d 100644 --- a/src/symmetric/message_hash/poseidon.rs +++ b/src/symmetric/message_hash/poseidon.rs @@ -188,7 +188,7 @@ where ) -> Result, Infallible> { // Compile-time parameter validation for Poseidon Message Hash // - // This hash implements Th_msg from §7.3.1 of DKKW25. It takes: + // This hash implements Th_msg from DKKW25. It takes: // // Th_msg(P, T, M, R) = Decode_{p,η',w}( // PoseidonCompress_{p, t_msg, η'}(R || P || EncT(T) || EncM(M)) @@ -198,8 +198,7 @@ where // This is used inside the incomparable encoding to map (message, seed) // to a hypercube vertex x ∈ Z_w^v before the target sum filter. // - // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" - // (DKKW25, IACR CiC 2(1), 2025) + // DKKW25: https://eprint.iacr.org/2025/055 const { // Poseidon capacity constraints // @@ -225,8 +224,8 @@ where // Representation constraints // - // Construction 3 (DKKW25) stores chunk values and chain indices - // as u8 in the signature and tweak encodings (Eq. 17). + // The Generalized XMSS construction (DKKW25) stores chunk values + // and chain indices as u8 in the signature and tweak encodings. // BASE (= w) and DIMENSION (= v) must each fit in one byte. assert!( BASE <= 1 << 8, @@ -258,7 +257,7 @@ where "Poseidon Message Hash: not enough field elements to encode the message" ); - // The epoch tweak (Eq. 19, DKKW25) packs a u32 epoch and an + // The epoch tweak (DKKW25) packs a u32 epoch and an // 8-bit domain separator into one value, requiring 40 bits total: // // tweakm(ep) = (0x02 || ep) ← 8 + 32 = 40 bits @@ -274,7 +273,7 @@ where // Injective decoding to chunks // // The hash output (η' field elements) is decoded via Decode_{p,η',w} - // (§7.3.1, DKKW25): interpret the η' elements as one big integer + // (DKKW25): interpret the η' elements as one big integer // and express it in base w to get DIMENSION chunks. // // For injectivity, the big integer must have enough room: diff --git a/src/symmetric/tweak_hash/poseidon.rs b/src/symmetric/tweak_hash/poseidon.rs index 2f4c5a9..cb35819 100644 --- a/src/symmetric/tweak_hash/poseidon.rs +++ b/src/symmetric/tweak_hash/poseidon.rs @@ -302,23 +302,10 @@ impl< ) -> Self::Domain { // Compile-time parameter validation for PoseidonTweakHash // - // This implements the tweakable hash function Th from §7.3.2 of - // DKKW25. It is used in three distinct contexts: - // - // (1) Chain hashing: Th(P, T, M) for M ∈ H (width 16) - // (2) Tree hashing: Th(P, T, M) for M ∈ H² (width 24) - // (3) Leaf hashing: Th(P, T, M) for M ∈ H^v (sponge) - // - // The tweak T provides domain separation, ensuring that each - // hash invocation in the scheme uses a unique (ep, i, k) or - // (level, pos) address. This is essential for the multi-target - // security reductions in Theorem 1 (DKKW25). - // - // DKKW25: "Hash-Based Multi-Signatures for Post-Quantum Ethereum" - // (DKKW25, IACR CiC 2(1), 2025) + // DKKW25: https://eprint.iacr.org/2025/055 const { - // The hash output space H = F_p^η (Parameter Req. 3, DKKW25) - // and public parameter space P = F_p^l_p must both be non-empty. + // The hash output space H = F_p^η and public parameter space + // P = F_p^l_p must both be non-empty. assert!( HASH_LEN >= 1, "Poseidon Tweak Hash: HASH_LEN (η) must be at least 1" @@ -328,69 +315,47 @@ impl< "Poseidon Tweak Hash: PARAMETER_LEN (l_p) must be at least 1" ); - // Poseidon capacity for sponge mode - // - // Leaf hashing (case 3) uses a sponge construction with - // rate = 24 - CAPACITY. The capacity must leave room for at - // least one rate element, so CAPACITY < 24. - // - // The capacity also controls the sponge security level: - // classical security ≤ c·log(p)/2 bits (§7.3, DKKW25, Eq. 21). + // Leaf hashing uses a sponge construction with rate = 24 - CAPACITY. + // The capacity must leave room for at least one rate element. assert!( CAPACITY < 24, "Poseidon Tweak Hash: Capacity must be less than 24" ); - // Compression mode width constraints - // - // Case (1) — Chain hashing: uses PoseidonCompress with width 16. - // Input layout: [P || T || M] must fit in 16 field elements. + // Chain hashing: Th(P, T, M) for M ∈ H uses compression mode + // with width 16. Input = [P || T || M]. assert!( PARAMETER_LEN + TWEAK_LEN + HASH_LEN <= 16, - "Poseidon Tweak Chain Hash: P+T+M exceeds width-16 capacity" + "Poseidon Tweak Chain Hash: Input lengths too large for Poseidon instance" ); - // Case (2) — Tree hashing: uses PoseidonCompress with width 24. - // Input layout: [P || T || left || right] must fit in 24 elements. + // Tree hashing: Th(P, T, M) for M ∈ H² uses compression mode + // with width 24. Input = [P || T || left || right]. assert!( PARAMETER_LEN + TWEAK_LEN + 2 * HASH_LEN <= 24, - "Poseidon Tweak Tree Hash: P+T+2H exceeds width-24 capacity" + "Poseidon Tweak Tree Hash: Input lengths too large for Poseidon instance" ); - // Leaf hashing domain separator - // - // Case (3) — Leaf hashing: the sponge capacity value V_c is - // derived from 4 domain parameters (each 32 bits = 128 bits - // total) via a width-24 Poseidon compression. The full 24 - // field elements must have enough bit capacity. + // Leaf hashing: the sponge capacity value V_c is derived from + // 4 domain parameters (each 32 bits = 128 bits total) via a + // width-24 Poseidon compression. let bits_per_fe = F::ORDER_U64.ilog2() as usize; assert!( bits_per_fe * 24 >= DOMAIN_PARAMETERS_LENGTH * 32, - "Poseidon Tweak Leaf Hash: width-24 cannot hold domain separator" + "Poseidon Tweak Leaf Hash: not enough field elements to hash the domain separator" ); - // Tweak encoding capacity - // - // The tweak functions (Eqs. 17-18, DKKW25) pack structured - // addresses into TWEAK_LEN field elements. The two formats are: - // - // Tree tweak (Eq. 18): (0x01 || level || pos_in_level) - // separator: 8 bits, level: ceil(log(h)) bits, pos: ceil(log L) bits - // Minimum: 8 + 32 = 40 bits (level fits in remaining bits) - // - // Chain tweak (Eq. 17): (0x00 || epoch || chain_index || pos_in_chain) - // separator: 8 bits, epoch: 32 bits, chain_index: 8 bits, pos: 8 bits - // Total: 56 bits - // - // The field element capacity must accommodate the wider format. + // Tweak encoding: the tree tweak packs 32 (pos_in_level) + 8 + // (level) = 40 bits; the chain tweak packs 32 (epoch) + 8 + // (chain_index) + 8 (pos_in_chain) + 8 (separator) = 56 bits. let tweak_fe_bits = bits_per_fe * TWEAK_LEN; assert!( tweak_fe_bits >= 40, - "Poseidon Tweak Hash: TWEAK_LEN too small for tree tweak (need >= 40 bits)" + "Poseidon Tweak Hash: not enough field elements to encode the tree tweak" ); assert!( tweak_fe_bits >= 56, - "Poseidon Tweak Hash: TWEAK_LEN too small for chain tweak (need >= 56 bits)" + "Poseidon Tweak Hash: not enough field elements to encode the chain tweak" ); }