Skip to content

Commit 840d319

Browse files
authored
core: more compile time assertions and doc (#48)
* core: more compile time assertions and doc * clippy * fix comments
1 parent 9d37fa9 commit 840d319

File tree

5 files changed

+287
-55
lines changed

5 files changed

+287
-55
lines changed

src/inc_encoding/target_sum.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,32 @@ impl<MH: MessageHash, const TARGET_SUM: usize> IncomparableEncoding
6161
randomness: &Self::Randomness,
6262
epoch: u32,
6363
) -> Result<Vec<u8>, Self::Error> {
64+
// Compile-time parameter validation for Target Sum Encoding
65+
//
66+
// This encoding implements Construction 6 (IE for Target Sum Winternitz)
67+
// from DKKW25. It maps a message to a codeword x ∈ C ⊆ Z_w^v, where:
68+
//
69+
// C = { (x_1, ..., x_v) ∈ {0, ..., w-1}^v | Σ x_i = T }
70+
//
71+
// The code C enforces the *incomparability* property (Definition 13):
72+
// no two distinct codewords x, x' satisfy x_i ≥ x'_i for all i.
73+
// This is critical for the security of the XMSS signature scheme.
74+
//
75+
// DKKW25: https://eprint.iacr.org/2025/055
76+
// HHKTW26: https://eprint.iacr.org/2026/016
6477
const {
65-
// base and dimension must not be too large
78+
// Representation constraints
79+
//
80+
// In the Generalized XMSS construction (DKKW25),
81+
// each chain position and chain index is encoded as a single byte
82+
// in the tweak function:
83+
//
84+
// tweak(ep, i, k) = (0x00 || ep || i || k)
85+
// 8b ⌈log L⌉ ⌈log v⌉ w bits
86+
//
87+
// - Since chain_index `i` is stored as u8, we need v ≤ 256.
88+
// - Since pos_in_chain `k` is stored as u8, we need w ≤ 256.
89+
// - Codeword entries (chunks) are also stored as u8 in signatures.
6690
assert!(
6791
MH::BASE <= 1 << 8,
6892
"Target Sum Encoding: Base must be at most 2^8"
@@ -71,6 +95,36 @@ impl<MH: MessageHash, const TARGET_SUM: usize> IncomparableEncoding
7195
MH::DIMENSION <= 1 << 8,
7296
"Target Sum Encoding: Dimension must be at most 2^8"
7397
);
98+
99+
// Encoding well-formedness
100+
//
101+
// Definition 13 (DKKW25): an incomparable encoding maps messages
102+
// to codewords in {0, ..., w-1}^v. For the incomparability
103+
// property to be meaningful, we need w ≥ 2 (otherwise every
104+
// codeword is the zero vector, and distinct codewords cannot
105+
// exist).
106+
assert!(
107+
MH::BASE >= 2,
108+
"Target Sum Encoding: Base must be at least 2"
109+
);
110+
111+
// Target sum range
112+
//
113+
// Construction 6 (DKKW25) defines the code:
114+
//
115+
// C = { x ∈ {0,...,w-1}^v | Σ x_i = T }
116+
//
117+
// For C to be non-empty, T must be achievable: each x_i can
118+
// contribute at most w-1 to the sum, so T ≤ v*(w-1). The lower
119+
// bound T ≥ 0 is guaranteed by the usize type.
120+
//
121+
// Choosing T close to v*(w-1)/2 (the expected sum of a uniform
122+
// hash) maximizes |C| and minimizes the signing retry rate
123+
// (Lemma 7, DKKW25).
124+
assert!(
125+
TARGET_SUM <= MH::DIMENSION * (MH::BASE - 1),
126+
"Target Sum Encoding: TARGET_SUM must be at most DIMENSION * (BASE - 1)"
127+
);
74128
}
75129

76130
// apply the message hash first to get chunks

src/signature/generalized_xmss.rs

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -653,37 +653,77 @@ where
653653

654654
const LIFETIME: u64 = 1 << LOG_LIFETIME;
655655

656+
#[allow(clippy::too_many_lines)]
656657
fn key_gen<R: RngExt>(
657658
rng: &mut R,
658659
activation_epoch: usize,
659660
num_active_epochs: usize,
660661
) -> (Self::PublicKey, Self::SecretKey) {
661662
const {
662-
// assert BASE and DIMENSION are small enough to make sure that we can fit
663-
// pos_in_chain and chain_index in u8.
663+
// Encoding well-formedness
664+
//
665+
// Definition 13 (DKKW25): the incomparable encoding maps
666+
// messages to codewords x ∈ C ⊆ {0, ..., w-1}^v. For the
667+
// incomparability property to hold, we need:
668+
// - w >= 2: a single-element alphabet makes all codewords
669+
// identical, so incomparability is vacuous.
670+
// - v >= 1: codewords must have at least one coordinate.
671+
assert!(
672+
IE::BASE >= 2,
673+
"Generalized XMSS: Encoding base (w) must be at least 2"
674+
);
675+
assert!(
676+
IE::DIMENSION >= 1,
677+
"Generalized XMSS: Encoding dimension (v) must be at least 1"
678+
);
679+
680+
// Representation constraints
681+
//
682+
// The chain tweak function (DKKW25) encodes:
683+
//
684+
// tweak(ep, i, k) = (0x00 || ep || i || k)
685+
// 8 bits ceil(log L) ceil(log v) w bits
686+
//
687+
// chain_index `i` and pos_in_chain `k` are stored as u8, and
688+
// chunk values in signatures are also u8. Therefore:
689+
// - BASE (= w) <= 256 (chunk fits in u8)
690+
// - DIMENSION (= v) <= 256 (chain_index fits in u8)
664691
assert!(
665692
IE::BASE <= 1 << 8,
666-
"Generalized XMSS: Encoding base too large, must be at most 2^8"
693+
"Generalized XMSS: Encoding base (w) must fit in u8 (<= 256)"
667694
);
668695
assert!(
669696
IE::DIMENSION <= 1 << 8,
670-
"Generalized XMSS: Encoding dimension too large, must be at most 2^8"
697+
"Generalized XMSS: Encoding dimension (v) must fit in u8 (<= 256)"
671698
);
672699

673-
// LOG_LIFETIME needs to be even, so that we can use the top-bottom tree approach
700+
// Merkle tree structure
701+
//
702+
// The key lifetime is L = 2^LOG_LIFETIME epochs. The Merkle tree
703+
// has depth h = LOG_LIFETIME, with L leaves (one per epoch).
704+
//
705+
// The top-bottom optimization splits the tree at depth h/2,
706+
// creating one top tree of depth h/2 and sqrt(L) bottom trees of
707+
// depth h/2. This requires h to be even.
674708
assert!(
675709
LOG_LIFETIME.is_multiple_of(2),
676-
"Generalized XMSS: LOG_LIFETIME must be multiple of two"
710+
"Generalized XMSS: LOG_LIFETIME must be even (top-bottom tree split)"
677711
);
678712

679-
// sign() and verify() take epoch as u32, so LOG_LIFETIME > 32 would create
680-
// epochs unreachable by the signing/verification API.
681-
const {
682-
assert!(
683-
LOG_LIFETIME <= 32,
684-
"Generalized XMSS: LOG_LIFETIME must be at most 32 (epoch type is u32)"
685-
);
686-
}
713+
// The smallest valid even LOG_LIFETIME is 2, giving L = 4 epochs,
714+
// a top tree of depth 1, and 2 bottom trees of depth 1.
715+
// LOG_LIFETIME = 0 would mean L = 1 (no internal Merkle nodes).
716+
assert!(
717+
LOG_LIFETIME >= 2,
718+
"Generalized XMSS: LOG_LIFETIME must be at least 2"
719+
);
720+
721+
// The sign() and verify() APIs take the epoch as u32, so
722+
// LOG_LIFETIME > 32 would create epochs that cannot be addressed.
723+
assert!(
724+
LOG_LIFETIME <= 32,
725+
"Generalized XMSS: LOG_LIFETIME must be at most 32 (epoch is u32)"
726+
);
687727
}
688728

689729
// Overflow-safe validation of the requested activation interval.

src/symmetric/message_hash/aborting.rs

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -77,50 +77,108 @@ where
7777
randomness: &Self::Randomness,
7878
message: &[u8; MESSAGE_LENGTH],
7979
) -> Result<Vec<u8>, HypercubeHashError> {
80+
// Compile-time parameter validation for AbortingHypercubeMessageHash
81+
//
82+
// This hash implements H^hc_{w,v,z,Q} from §6.1 of HHKTW26. It uses
83+
// rejection sampling to uniformly map Poseidon field elements into
84+
// the hypercube Z_w^v, avoiding big-integer arithmetic entirely:
85+
//
86+
// 1. Compute (A_1, ..., A_ℓ) := Poseidon(R || P || T || M)
87+
// 2. For each A_i: reject if A_i ≥ Q·w^z (ensures uniformity)
88+
// 3. Decompose d_i = ⌊A_i / Q⌋ into z base-w digits
89+
// 4. Collect the first v digits as the output
90+
//
91+
// The field prime decomposes as p = Q·w^z + α (α ≥ 0).
92+
// Rejection happens with per-element probability α/p, and the
93+
// overall abort probability is θ = 1 - ((Q·w^z)/p)^ℓ (Lemma 8).
94+
//
95+
// By Theorem 4 of HHKTW26, this construction is indifferentiable
96+
// from a θ-aborting random oracle when Poseidon is modeled as a
97+
// standard random oracle.
98+
//
99+
// DKKW25: https://eprint.iacr.org/2025/055
100+
// HHKTW26: https://eprint.iacr.org/2026/016
80101
const {
81-
// Check that Poseidon of width 24 is enough
102+
// Poseidon capacity constraints
103+
//
104+
// We use Poseidon in compression mode with a width-24 permutation.
105+
// All inputs must fit in one call, and the output is extracted
106+
// from the same state.
82107
assert!(
83108
PARAMETER_LEN + RAND_LEN_FE + TWEAK_LEN_FE + MSG_LEN_FE <= 24,
84-
"Poseidon of width 24 is not enough"
109+
"Poseidon of width 24 is not enough for the input"
85110
);
86-
assert!(HASH_LEN_FE <= 24, "Poseidon of width 24 is not enough");
87-
88-
// Check that we have enough hash output field elements
89111
assert!(
90-
HASH_LEN_FE >= DIMENSION.div_ceil(Z),
91-
"Not enough hash output field elements for the requested dimension"
112+
HASH_LEN_FE <= 24,
113+
"Poseidon of width 24 is not enough for the output"
92114
);
115+
116+
// Poseidon compression mode can only produce as many output
117+
// field elements as there are input elements.
93118
assert!(
94119
PARAMETER_LEN + RAND_LEN_FE + TWEAK_LEN_FE + MSG_LEN_FE >= HASH_LEN_FE,
95120
"Input shorter than requested output"
96121
);
97122

98-
// Base check
123+
// Hypercube decomposition parameters
124+
//
125+
// Each good field element A_i < Q·w^z is decomposed into z
126+
// base-w digits, so we need ℓ = ⌈v/z⌉ field elements to get
127+
// at least v digits. HASH_LEN_FE must supply enough elements.
99128
assert!(
100-
BASE <= 1 << 8,
101-
"Aborting Hypercube Message Hash: Base must be at most 2^8"
129+
DIMENSION >= 1,
130+
"AbortingHypercubeMessageHash: DIMENSION (v) must be at least 1"
131+
);
132+
assert!(
133+
Z >= 1,
134+
"AbortingHypercubeMessageHash: Z (digits per field element) must be at least 1"
135+
);
136+
assert!(
137+
HASH_LEN_FE >= DIMENSION.div_ceil(Z),
138+
"Not enough hash output field elements: need ceil(v/z)"
102139
);
103140

104-
// Check that Q * w^z fits within the field
141+
// Q is the quotient in the decomposition A_i = Q·d_i + c_i,
142+
// where c_i ∈ {0, ..., Q-1} is discarded and d_i ∈ {0, ..., w^z-1}
143+
// carries the uniform digits. Q must be positive for a valid range.
144+
assert!(Q >= 1, "AbortingHypercubeMessageHash: Q must be at least 1");
145+
146+
// The rejection threshold Q·w^z must not exceed the field order p,
147+
// since field elements A_i live in {0, ..., p-1}. The remainder
148+
// α = p - Q·w^z determines the per-element abort probability α/p.
149+
//
150+
// Example (KoalaBear): p = 2^31 - 2^24 + 1 = 127·8^8 + 1
151+
// ⟹ Q=127, w=8, z=8, α=1, abort prob ≈ 4.7e-10 per element.
105152
assert!(
106153
Q as u64 * (BASE as u64).pow(Z as u32) <= F::ORDER_U64,
107-
"Q * w^z exceeds field order"
154+
"Q * w^z exceeds field order p"
108155
);
109156

110-
// floor(log2(ORDER))
111-
let bits_per_fe = F::ORDER_U64.ilog2() as usize;
157+
// Representation constraints
158+
//
159+
// Same as the Poseidon message hash: chunks and chain indices
160+
// are stored as u8 in signatures and tweak encodings.
161+
assert!(
162+
BASE >= 2,
163+
"AbortingHypercubeMessageHash: BASE (w) must be at least 2 (Definition 13, DKKW25)"
164+
);
165+
assert!(
166+
BASE <= 1 << 8,
167+
"AbortingHypercubeMessageHash: BASE (w) must fit in u8"
168+
);
112169

113-
// Check that we have enough bits to encode message
170+
// Injective encoding of inputs
171+
//
172+
// Same requirements as the standard Poseidon message hash:
173+
// message and epoch must be losslessly encodable as field elements.
174+
let bits_per_fe = F::ORDER_U64.ilog2() as usize;
114175
assert!(
115176
bits_per_fe * MSG_LEN_FE >= 8 * MESSAGE_LENGTH,
116-
"Aborting Hypercube Message Hash: not enough field elements to encode the message"
177+
"AbortingHypercubeMessageHash: not enough field elements to encode the message"
117178
);
118-
119-
// Check that we have enough bits to encode tweak
120-
// Epoch is a u32, and we have one domain separator byte
121179
assert!(
122180
bits_per_fe * TWEAK_LEN_FE >= 40,
123-
"Aborting Hypercube Message Hash: not enough field elements to encode the epoch tweak"
181+
"AbortingHypercubeMessageHash: not enough field elements to encode the epoch tweak"
124182
);
125183
}
126184

0 commit comments

Comments
 (0)