1use alloc::string::ToString;
2
3use rand::{
4 Rng,
5 rand_core::{Infallible, TryRng, utils},
6};
7
8use super::{Felt, FeltRng};
9use crate::{
10 Word,
11 field::ExtensionField,
12 hash::eidos::{
13 Eidos,
14 domains::{RANDOM_COIN_OUTPUT, RANDOM_COIN_STATE},
15 },
16 utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
17};
18
19const OUTPUT_FELTS: usize = Word::NUM_ELEMENTS;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct EidosRandomCoin {
35 state: Word,
36 output: Word,
37 counter: u64,
38 current: usize,
39}
40
41impl EidosRandomCoin {
42 pub fn new(seed: Word) -> Self {
44 let state = Eidos::hash_elements_in_domain(seed.as_elements(), RANDOM_COIN_STATE);
45 Self {
46 state,
47 output: Word::default(),
48 counter: 0,
49 current: OUTPUT_FELTS,
50 }
51 }
52
53 pub fn from_parts(state: Word, output: Word, counter: u64, current: usize) -> Self {
66 assert!(current <= OUTPUT_FELTS, "current output index is out of range");
67 assert!(
68 current == OUTPUT_FELTS || counter != 0,
69 "buffered output requires a generated block"
70 );
71 Self { state, output, counter, current }
72 }
73
74 pub fn into_parts(self) -> (Word, Word, u64, usize) {
79 (self.state, self.output, self.counter, self.current)
80 }
81
82 pub fn fill_bytes(&mut self, dest: &mut [u8]) {
89 <Self as Rng>::fill_bytes(self, dest)
90 }
91
92 pub fn draw_basefield(&mut self) -> Felt {
99 loop {
100 let candidate =
101 self.try_next_u64().expect("Eidos random-coin generation is infallible");
102 if let Ok(value) = Felt::new(candidate) {
103 return value;
104 }
105 }
106 }
107
108 pub fn draw(&mut self) -> Felt {
114 self.draw_basefield()
115 }
116
117 pub fn draw_ext_field<E: ExtensionField<Felt>>(&mut self) -> E {
123 E::from_basis_coefficients_fn(|_| self.draw_basefield())
124 }
125
126 pub fn reseed(&mut self, data: Word) {
131 let input = [
132 self.state[0],
133 self.state[1],
134 self.state[2],
135 self.state[3],
136 Felt::from_u32(self.counter as u32),
137 Felt::from_u32((self.counter >> 32) as u32),
138 data[0],
139 data[1],
140 data[2],
141 data[3],
142 ];
143 self.state = Eidos::hash_elements_in_domain(&input, RANDOM_COIN_STATE);
144 self.output = Word::default();
145 self.counter = 0;
146 self.current = OUTPUT_FELTS;
147 }
148
149 fn next_output_u32(&mut self) -> u32 {
150 loop {
151 if self.current == OUTPUT_FELTS {
152 self.refill_output();
153 }
154
155 let value = self.output[self.current].as_canonical_u64();
156 self.current += 1;
157
158 if value != Felt::ORDER - 1 {
162 return value as u32;
163 }
164 }
165 }
166
167 fn refill_output(&mut self) {
168 let counter = self.counter;
169 self.counter = counter.checked_add(1).expect("Eidos random-coin counter exhausted");
170
171 let input = [
172 self.state[0],
173 self.state[1],
174 self.state[2],
175 self.state[3],
176 Felt::from_u32(counter as u32),
177 Felt::from_u32((counter >> 32) as u32),
178 ];
179 self.output = Eidos::hash_elements_in_domain(&input, RANDOM_COIN_OUTPUT);
180 self.current = 0;
181 }
182}
183
184impl FeltRng for EidosRandomCoin {
185 fn draw_element(&mut self) -> Felt {
186 self.draw_basefield()
187 }
188
189 fn draw_word(&mut self) -> Word {
190 Word::new(core::array::from_fn(|_| self.draw_basefield()))
191 }
192}
193
194impl TryRng for EidosRandomCoin {
195 type Error = Infallible;
196
197 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
198 Ok(self.next_output_u32())
199 }
200
201 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
202 utils::next_u64_via_u32(self)
203 }
204
205 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
206 utils::fill_bytes_via_next_word(dest, || self.try_next_u32())
207 }
208}
209
210impl Serializable for EidosRandomCoin {
211 fn write_into<W: ByteWriter>(&self, target: &mut W) {
212 self.state.write_into(target);
213 self.output.write_into(target);
214 target.write_u64(self.counter);
215 target.write_u8(self.current as u8);
216 }
217}
218
219impl Deserializable for EidosRandomCoin {
220 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
221 let state = Word::read_from(source)?;
222 let output = Word::read_from(source)?;
223 let counter = source.read_u64()?;
224 let current = source.read_u8()? as usize;
225 if current > OUTPUT_FELTS {
226 return Err(DeserializationError::InvalidValue(
227 "current output index is out of range".to_string(),
228 ));
229 }
230 if current != OUTPUT_FELTS && counter == 0 {
231 return Err(DeserializationError::InvalidValue(
232 "buffered output requires a generated block".to_string(),
233 ));
234 }
235
236 Ok(Self { state, output, counter, current })
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use rand::RngExt;
243
244 use super::*;
245 use crate::{ONE, ZERO, field::PrimeCharacteristicRing};
246
247 fn seed() -> Word {
248 Word::new([Felt::ONE, Felt::TWO, Felt::from_u8(3), Felt::from_u8(4)])
249 }
250
251 #[test]
252 fn seed_is_framed_under_the_state_domain() {
253 let coin = EidosRandomCoin::new(seed());
254 let expected = Eidos::hash_elements_in_domain(seed().as_elements(), RANDOM_COIN_STATE);
255
256 assert_eq!(coin.state, expected);
257 assert_eq!(coin.counter, 0);
258 assert_eq!(coin.current, OUTPUT_FELTS);
259 }
260
261 #[test]
262 fn output_matches_the_registered_counter_mode_construction() {
263 let mut coin = EidosRandomCoin::new(seed());
264 let initial_state = coin.state;
265 let input = [
266 initial_state[0],
267 initial_state[1],
268 initial_state[2],
269 initial_state[3],
270 ZERO,
271 ZERO,
272 ];
273 let expected = Eidos::hash_elements_in_domain(&input, RANDOM_COIN_OUTPUT);
274
275 let actual: [u32; OUTPUT_FELTS] = core::array::from_fn(|_| coin.random::<u32>());
276
277 assert_eq!(actual, expected.into_elements().map(|value| value.as_canonical_u64() as u32));
278 assert_eq!(coin.state, initial_state);
279 assert_eq!(coin.output, expected);
280 assert_eq!(coin.counter, 1);
281 assert_eq!(actual, [3_343_138_332, 3_182_666_834, 3_956_264_476, 4_003_292_457]);
282 }
283
284 #[test]
285 fn base_field_sampling_rejects_noncanonical_candidates() {
286 let modulus = Felt::ORDER;
287 let output = Word::new([
288 Felt::from_u32(modulus as u32),
289 Felt::from_u32((modulus >> 32) as u32),
290 Felt::from_u32(42),
291 ZERO,
292 ]);
293 let mut coin = EidosRandomCoin::from_parts(Word::default(), output, 1, 0);
294
295 assert_eq!(coin.draw_basefield(), Felt::from_u64(42));
296 assert_eq!(coin.current, 4);
297 }
298
299 #[test]
300 fn u32_sampling_rejects_the_extra_low_limb_preimage() {
301 let output =
302 Word::new([Felt::new_unchecked(Felt::ORDER - 1), Felt::from_u32(42), ZERO, ZERO]);
303 let mut coin = EidosRandomCoin::from_parts(Word::default(), output, 1, 0);
304
305 assert_eq!(coin.random::<u32>(), 42);
306 assert_eq!(coin.current, 2);
307 }
308
309 #[test]
310 fn reseed_uses_the_complete_state_and_input() {
311 let data =
312 Word::new([Felt::from_u8(5), Felt::from_u8(6), Felt::from_u8(7), Felt::from_u8(8)]);
313 let mut coin = EidosRandomCoin::new(seed());
314 let old_state = coin.state;
315 let _ = coin.random::<u32>();
316 assert_eq!(coin.state, old_state);
317 assert_eq!(coin.counter, 1);
318 coin.reseed(data);
319
320 let input = [
321 old_state[0],
322 old_state[1],
323 old_state[2],
324 old_state[3],
325 Felt::ONE,
326 ZERO,
327 data[0],
328 data[1],
329 data[2],
330 data[3],
331 ];
332 let expected = Eidos::hash_elements_in_domain(&input, RANDOM_COIN_STATE);
333 assert_eq!(coin.state, expected);
334 assert_eq!(coin.output, Word::default());
335 assert_eq!(coin.counter, 0);
336 assert_eq!(coin.current, OUTPUT_FELTS);
337 }
338
339 #[test]
340 fn reseed_binds_the_generated_block_count() {
341 let data =
342 Word::new([Felt::from_u8(5), Felt::from_u8(6), Felt::from_u8(7), Felt::from_u8(8)]);
343 let mut first = EidosRandomCoin::new(seed());
344 let mut second = first;
345
346 let _: [u32; OUTPUT_FELTS] = core::array::from_fn(|_| second.random());
347 first.reseed(data);
348 second.reseed(data);
349
350 assert_ne!(first.state, second.state);
351 assert_ne!(first.random::<[u8; 32]>(), second.random::<[u8; 32]>());
352 }
353
354 #[test]
355 fn reseeded_stream_matches_the_frozen_vector() {
356 let data =
357 Word::new([Felt::from_u8(5), Felt::from_u8(6), Felt::from_u8(7), Felt::from_u8(8)]);
358 let mut coin = EidosRandomCoin::new(seed());
359 let _: [u32; OUTPUT_FELTS] = core::array::from_fn(|_| coin.random());
360 coin.reseed(data);
361
362 let actual: [u32; OUTPUT_FELTS] = core::array::from_fn(|_| coin.random());
363 assert_eq!(actual, [3_145_282_389, 2_211_610_295, 3_232_936_185, 3_064_838_016]);
364 }
365
366 #[test]
367 fn felt_rng_methods_follow_the_base_field_stream() {
368 let mut actual = EidosRandomCoin::new(seed());
369 let mut expected = actual;
370
371 assert_eq!(actual.draw_element(), expected.draw_basefield());
372 assert_eq!(
373 actual.draw_word(),
374 Word::new(core::array::from_fn(|_| expected.draw_basefield()))
375 );
376 }
377
378 #[test]
379 fn serialization_preserves_partially_consumed_output() {
380 let mut coin = EidosRandomCoin::new(seed());
381 let _: [u8; 13] = coin.random();
382
383 let bytes = coin.to_bytes();
384 let decoded = EidosRandomCoin::read_from_bytes(&bytes).unwrap();
385 assert_eq!(decoded, coin);
386
387 let mut first = coin;
388 let mut second = decoded;
389 assert_eq!(first.random::<[u8; 64]>(), second.random::<[u8; 64]>());
390 }
391
392 #[test]
393 fn deserialization_rejects_an_invalid_output_index() {
394 let mut bytes = EidosRandomCoin::new(seed()).to_bytes();
395 *bytes.last_mut().unwrap() = (OUTPUT_FELTS + 1) as u8;
396
397 assert!(EidosRandomCoin::read_from_bytes(&bytes).is_err());
398 }
399
400 #[test]
401 fn deserialization_rejects_buffered_output_without_a_generated_block() {
402 let mut bytes = EidosRandomCoin::new(seed()).to_bytes();
403 *bytes.last_mut().unwrap() = 0;
404
405 assert!(EidosRandomCoin::read_from_bytes(&bytes).is_err());
406 }
407
408 #[test]
409 #[should_panic(expected = "current output index is out of range")]
410 fn from_parts_rejects_an_invalid_output_index() {
411 EidosRandomCoin::from_parts(Word::default(), Word::default(), 0, OUTPUT_FELTS + 1);
412 }
413
414 #[test]
415 #[should_panic(expected = "buffered output requires a generated block")]
416 fn from_parts_rejects_buffered_output_without_a_generated_block() {
417 EidosRandomCoin::from_parts(Word::default(), Word::default(), 0, 0);
418 }
419
420 #[test]
421 fn successive_counters_produce_distinct_output_blocks() {
422 let mut coin = EidosRandomCoin::new(seed());
423 let first: [u32; OUTPUT_FELTS] = core::array::from_fn(|_| coin.random());
424 let second: [u32; OUTPUT_FELTS] = core::array::from_fn(|_| coin.random());
425
426 assert_ne!(first, second);
427 assert_eq!(coin.counter, 2);
428 }
429
430 #[test]
431 fn output_binds_both_counter_limbs() {
432 let state = EidosRandomCoin::new(seed()).state;
433 let mut low = EidosRandomCoin::from_parts(state, Word::default(), 0, OUTPUT_FELTS);
434 let mut high =
435 EidosRandomCoin::from_parts(state, Word::default(), 1u64 << 32, OUTPUT_FELTS);
436
437 assert_ne!(low.random::<[u8; 16]>(), high.random::<[u8; 16]>());
438 }
439
440 #[test]
441 #[should_panic(expected = "Eidos random-coin counter exhausted")]
442 fn counter_exhaustion_is_detected() {
443 let mut coin =
444 EidosRandomCoin::from_parts(Word::default(), Word::default(), u64::MAX, OUTPUT_FELTS);
445 let _ = coin.random::<u32>();
446 }
447
448 #[test]
449 fn different_seeds_produce_different_streams() {
450 let mut first = EidosRandomCoin::new(seed());
451 let mut second = EidosRandomCoin::new(Word::new([ONE; Word::NUM_ELEMENTS]));
452
453 assert_ne!(first.random::<[u8; 64]>(), second.random::<[u8; 64]>());
454 }
455}