Skip to content

Commit 4854bcf

Browse files
authored
ml-dsa: optimize rejection sampling in rej_(ntt|bounded)_poly (#1291)
It seems like filling matrix is a bottleneck of decoding encapsulation key. With simple changes, it gets better performance.
1 parent 2399f8a commit 4854bcf

1 file changed

Lines changed: 64 additions & 18 deletions

File tree

ml-dsa/src/sampling.rs

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use crate::{
55
};
66
use hybrid_array::Array;
77
use module_lattice::{ArraySize, Field, Truncate};
8+
#[cfg(feature = "zeroize")]
9+
use zeroize::Zeroize;
810

911
// Algorithm 13 BytesToBits
1012
fn bit_set(z: &[u8], i: usize) -> bool {
@@ -95,46 +97,90 @@ pub(crate) fn sample_in_ball(rho: &[u8], tau: usize) -> Polynomial {
9597
fn rej_ntt_poly(rho: &[u8], r: u8, s: u8) -> NttPolynomial {
9698
let mut j = 0;
9799
let mut ctx = G::default().absorb(rho).absorb(&[s]).absorb(&[r]);
98-
99100
let mut a = NttPolynomial::default();
100-
let mut s = [0u8; 3];
101-
while j < 256 {
102-
ctx.squeeze(&mut s);
103-
if let Some(x) = coeff_from_three_bytes(s) {
101+
102+
// Squeeze 840 bytes (5 SHAKE128 blocks) in a single call rather than 3 bytes
103+
// at a time. The rejection probability is ~0.098%, so 280 candidates are
104+
// almost always sufficient while requiring the same 5 Keccak-f permutations.
105+
let mut buf = [0u8; 840];
106+
ctx.squeeze(&mut buf);
107+
108+
for chunk in buf.chunks_exact(3) {
109+
if let Some(x) = coeff_from_three_bytes([chunk[0], chunk[1], chunk[2]]) {
104110
a.0[j] = x;
105111
j += 1;
112+
if j == 256 {
113+
break;
114+
}
106115
}
107116
}
108117

118+
// Fallback: astronomically unlikely (~10^-44), but required for correctness.
119+
let mut tmp = [0u8; 3];
120+
while j < 256 {
121+
ctx.squeeze(&mut tmp);
122+
if let Some(x) = coeff_from_three_bytes(tmp) {
123+
a.0[j] = x;
124+
j += 1;
125+
}
126+
}
127+
#[cfg(feature = "zeroize")]
128+
{
129+
buf.zeroize();
130+
tmp.zeroize();
131+
}
109132
a
110133
}
111134

112135
// Algorithm 31 RejBoundedPoly
113136
fn rej_bounded_poly(rho: &[u8], eta: Eta, r: u16) -> Polynomial {
114137
let mut j = 0;
115138
let mut ctx = H::default().absorb(rho).absorb(&r.to_le_bytes());
116-
117139
let mut a = Polynomial::default();
118-
let mut z = [0u8];
119-
while j < 256 {
120-
ctx.squeeze(&mut z);
121-
let (z0, z1) = coeffs_from_byte(z[0], eta);
122140

123-
if let Some(z) = z0 {
124-
a.0[j] = z;
141+
// The reference implementation uses 136 bytes (1 SHAKE256 block) for eta=2 and 272 bytes (2 blocks) for eta=4.
142+
let mut buf = [0u8; 272];
143+
ctx.squeeze(&mut buf);
144+
145+
for &byte in &buf {
146+
let (z0, z1) = coeffs_from_byte(byte, eta);
147+
if let Some(x) = z0 {
148+
a.0[j] = x;
125149
j += 1;
150+
if j == 256 {
151+
break;
152+
}
126153
}
127-
128-
if j == 256 {
129-
break;
154+
if let Some(x) = z1 {
155+
a.0[j] = x;
156+
j += 1;
157+
if j == 256 {
158+
break;
159+
}
130160
}
161+
}
131162

132-
if let Some(z) = z1 {
133-
a.0[j] = z;
163+
// Fallback: astronomically unlikely, but required for correctness.
164+
let mut tmp = [0u8; 1];
165+
while j < 256 {
166+
ctx.squeeze(&mut tmp);
167+
let (z0, z1) = coeffs_from_byte(tmp[0], eta);
168+
if let Some(x) = z0 {
169+
a.0[j] = x;
134170
j += 1;
135171
}
172+
if j < 256 {
173+
if let Some(x) = z1 {
174+
a.0[j] = x;
175+
j += 1;
176+
}
177+
}
178+
}
179+
#[cfg(feature = "zeroize")]
180+
{
181+
buf.zeroize();
182+
tmp.zeroize();
136183
}
137-
138184
a
139185
}
140186

0 commit comments

Comments
 (0)