helpers_codec/common/
sampling.rs1use core::f64::consts::TAU;
32
33use rand_core::CryptoRng;
34
35use nc_polynomial::{PolynomialError, RingContext, RingElem};
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum SamplingError {
40 InvalidUpperBound,
42 InvalidEta,
44 InvalidSigma,
46 Polynomial(PolynomialError),
48}
49
50impl core::fmt::Display for SamplingError {
51 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52 match self {
53 Self::InvalidUpperBound => write!(f, "invalid upper bound, expected > 0"),
54 Self::InvalidEta => write!(f, "invalid eta, expected eta >= 1"),
55 Self::InvalidSigma => write!(f, "invalid sigma, expected finite sigma > 0"),
56 Self::Polynomial(err) => write!(f, "polynomial operation failed: {err}"),
57 }
58 }
59}
60
61impl std::error::Error for SamplingError {}
62
63impl From<PolynomialError> for SamplingError {
64 fn from(value: PolynomialError) -> Self {
65 Self::Polynomial(value)
66 }
67}
68
69pub fn sample_rejection_u64_below_example<R: CryptoRng>(
71 upper_bound: u64,
72 rng: &mut R,
73) -> Result<u64, SamplingError> {
74 if upper_bound == 0 {
75 return Err(SamplingError::InvalidUpperBound);
76 }
77
78 let range = (u64::MAX as u128) + 1;
84 let ub = upper_bound as u128;
85 let zone = range - (range % ub);
86
87 loop {
88 let candidate = rng.next_u64() as u128;
90 if candidate < zone {
91 return Ok((candidate % ub) as u64);
92 }
93 }
94}
95
96pub fn sample_uniform_poly_example<R: CryptoRng>(
98 ctx: &RingContext,
99 rng: &mut R,
100) -> Result<RingElem, SamplingError> {
101 let mut coeffs = vec![0_u64; ctx.max_degree() + 1];
102 for coeff in &mut coeffs {
103 *coeff = sample_rejection_u64_below_example(ctx.modulus(), rng)?;
105 }
106 Ok(ctx.element(&coeffs)?)
108}
109
110pub fn sample_cbd_poly_example<R: CryptoRng>(
115 ctx: &RingContext,
116 eta: u8,
117 rng: &mut R,
118) -> Result<RingElem, SamplingError> {
119 if eta == 0 {
120 return Err(SamplingError::InvalidEta);
121 }
122
123 let mut coeffs = vec![0_u64; ctx.max_degree() + 1];
124 for coeff in &mut coeffs {
125 let mut a = 0_i64;
127 let mut b = 0_i64;
128 for _ in 0..eta {
129 a += (rng.next_u32() & 1) as i64;
130 b += (rng.next_u32() & 1) as i64;
131 }
132 *coeff = wrap_signed_to_modulus(a - b, ctx.modulus());
134 }
135
136 Ok(ctx.element(&coeffs)?)
137}
138
139pub fn sample_discrete_gaussian_poly_example<R: CryptoRng>(
143 ctx: &RingContext,
144 sigma: f64,
145 rng: &mut R,
146) -> Result<RingElem, SamplingError> {
147 if !(sigma.is_finite() && sigma > 0.0) {
148 return Err(SamplingError::InvalidSigma);
149 }
150
151 let mut coeffs = vec![0_u64; ctx.max_degree() + 1];
152 for coeff in &mut coeffs {
153 let sampled = sample_box_muller_integer(rng, sigma);
155 *coeff = wrap_signed_to_modulus(sampled, ctx.modulus());
156 }
157
158 Ok(ctx.element(&coeffs)?)
159}
160
161fn sample_box_muller_integer<R: CryptoRng>(rng: &mut R, sigma: f64) -> i64 {
162 let mut u1 = sample_unit_open_closed(rng);
166 while u1 <= 0.0 {
167 u1 = sample_unit_open_closed(rng);
168 }
169 let u2 = sample_unit_open_closed(rng);
170
171 let radius = (-2.0 * u1.ln()).sqrt();
172 let z0 = radius * (TAU * u2).cos();
173 (z0 * sigma).round() as i64
174}
175
176fn sample_unit_open_closed<R: CryptoRng>(rng: &mut R) -> f64 {
177 const SCALE: f64 = 1.0 / ((1_u64 << 53) as f64);
179 let raw = rng.next_u64() >> 11;
180 let mut out = (raw as f64) * SCALE;
181 if out <= 0.0 {
183 out = f64::from_bits(1);
184 }
185 if out >= 1.0 {
186 out = 1.0 - f64::EPSILON;
187 }
188 out
189}
190
191fn wrap_signed_to_modulus(value: i64, modulus: u64) -> u64 {
192 let m = modulus as i128;
194 let reduced = ((value as i128 % m) + m) % m;
195 reduced as u64
196}
197
198#[cfg(test)]
199mod tests {
200 use super::{
201 SamplingError, sample_cbd_poly_example, sample_discrete_gaussian_poly_example,
202 sample_rejection_u64_below_example, sample_uniform_poly_example,
203 };
204 use core::convert::Infallible;
205 use nc_polynomial::RingContext;
206 use rand_core::{TryCryptoRng, TryRng};
207
208 #[derive(Debug, Clone)]
209 struct TestRng {
210 state: u64,
211 }
212
213 impl TestRng {
214 fn new(seed: u64) -> Self {
215 Self { state: seed }
216 }
217
218 fn step(&mut self) -> u64 {
219 let mut x = self.state;
220 x ^= x >> 12;
221 x ^= x << 25;
222 x ^= x >> 27;
223 self.state = x;
224 x.wrapping_mul(0x2545_F491_4F6C_DD1D)
225 }
226 }
227
228 impl TryRng for TestRng {
229 type Error = Infallible;
230
231 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
232 Ok(self.step() as u32)
233 }
234
235 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
236 Ok(self.step())
237 }
238
239 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
240 let mut offset = 0;
241 while offset < dest.len() {
242 let word = self.step().to_le_bytes();
243 let chunk = (dest.len() - offset).min(8);
244 dest[offset..offset + chunk].copy_from_slice(&word[..chunk]);
245 offset += chunk;
246 }
247 Ok(())
248 }
249 }
250
251 impl TryCryptoRng for TestRng {}
252
253 fn ctx() -> RingContext {
254 RingContext::from_parts(8, 998_244_353, &[1, 0, 0, 0, 0, 0, 0, 0, 1], 3)
255 .expect("context should build")
256 }
257
258 #[test]
259 fn rejection_sampling_respects_bounds() {
260 let mut rng = TestRng::new(7);
261 for bound in [1_u64, 2, 3, 17, 1_000_000, 998_244_353] {
262 for _ in 0..200 {
263 let sampled = sample_rejection_u64_below_example(bound, &mut rng)
265 .expect("sampling should work");
266 assert!(sampled < bound);
267 }
268 }
269 }
270
271 #[test]
272 fn rejection_sampling_rejects_zero_bound() {
273 let mut rng = TestRng::new(11);
274 let err = sample_rejection_u64_below_example(0, &mut rng).expect_err("expected error");
275 assert_eq!(err, SamplingError::InvalidUpperBound);
276 }
277
278 #[test]
279 fn uniform_poly_sampling_outputs_valid_ring_element() {
280 let ctx = ctx();
281 let mut rng = TestRng::new(13);
282 let sampled = sample_uniform_poly_example(&ctx, &mut rng).expect("sampling should work");
283
284 assert_eq!(sampled.coefficients().len(), ctx.max_degree() + 1);
286 assert!(sampled.coefficients().iter().all(|&c| c < ctx.modulus()));
287 }
288
289 #[test]
290 fn cbd_poly_sampling_outputs_small_noise_mod_q() {
291 let ctx = ctx();
292 let mut rng = TestRng::new(17);
293 let sampled = sample_cbd_poly_example(&ctx, 3, &mut rng).expect("sampling should work");
294
295 let eta = 3_i64;
297 for &coeff in sampled.coefficients() {
298 let signed = if coeff > ctx.modulus() / 2 {
299 coeff as i64 - ctx.modulus() as i64
300 } else {
301 coeff as i64
302 };
303 assert!(signed >= -eta && signed <= eta);
304 }
305 }
306
307 #[test]
308 fn cbd_sampling_rejects_eta_zero() {
309 let ctx = ctx();
310 let mut rng = TestRng::new(19);
311 let err = sample_cbd_poly_example(&ctx, 0, &mut rng).expect_err("expected error");
312 assert_eq!(err, SamplingError::InvalidEta);
313 }
314
315 #[test]
316 fn gaussian_sampling_accepts_positive_sigma() {
317 let ctx = ctx();
318 let mut rng = TestRng::new(23);
319 let sampled = sample_discrete_gaussian_poly_example(&ctx, 2.5, &mut rng)
320 .expect("sampling should work");
321 assert_eq!(sampled.coefficients().len(), ctx.max_degree() + 1);
322 }
323
324 #[test]
325 fn gaussian_sampling_rejects_invalid_sigma() {
326 let ctx = ctx();
327 let mut rng = TestRng::new(29);
328 let err =
329 sample_discrete_gaussian_poly_example(&ctx, 0.0, &mut rng).expect_err("expected error");
330 assert_eq!(err, SamplingError::InvalidSigma);
331 }
332}