Skip to main content

fugue_evo/checkpoint/
rng.rs

1//! Serializable RNG snapshots for reproducible checkpoint resume (EV-02)
2//!
3//! Reproducible resume requires an RNG whose entire internal state can be
4//! captured and restored byte-for-byte. `rand`'s generic `Rng`/`RngCore`
5//! traits provide no serialization hook, so a resumed run seeded from a fresh
6//! (or differently-seeded) generator silently diverges from the trajectory a
7//! continuous run would have taken.
8//!
9//! The [`SnapshotRng`] trait fills that gap for the ChaCha family of counter
10//! based RNGs (`ChaCha8Rng`, `ChaCha12Rng`, `ChaCha20Rng`), whose state is
11//! fully serializable via `serde`. Capturing the RNG alongside the population
12//! in a [`Checkpoint`](crate::checkpoint::Checkpoint) makes resume
13//! bit-identical to a run that was never interrupted.
14//!
15//! # Reproducibility contract
16//!
17//! Bit-identical resume is only guaranteed when the algorithm draws all of its
18//! randomness from a `SnapshotRng` and that RNG is captured into the checkpoint
19//! (via [`Checkpoint::with_rng`](crate::checkpoint::Checkpoint::with_rng)) and
20//! restored on resume (via
21//! [`Checkpoint::restore_rng`](crate::checkpoint::Checkpoint::restore_rng)).
22//! Non-ChaCha generators (e.g. `StdRng`, `ThreadRng`) cannot be snapshotted and
23//! therefore cannot provide reproducible resume.
24
25use rand_chacha::{ChaCha12Rng, ChaCha20Rng, ChaCha8Rng};
26
27use crate::error::CheckpointError;
28
29/// An RNG whose complete internal state can be captured to bytes and restored,
30/// enabling bit-identical checkpoint resume.
31///
32/// Implemented for the ChaCha family (`ChaCha8Rng`, `ChaCha12Rng`,
33/// `ChaCha20Rng`), all of which expose fully serializable state.
34pub trait SnapshotRng: Sized {
35    /// Serialize the full RNG state to a byte buffer.
36    fn capture(&self) -> Result<Vec<u8>, CheckpointError>;
37
38    /// Reconstruct an RNG from bytes previously produced by [`capture`].
39    ///
40    /// [`capture`]: SnapshotRng::capture
41    fn restore(bytes: &[u8]) -> Result<Self, CheckpointError>;
42}
43
44macro_rules! impl_snapshot_rng {
45    ($rng:ty) => {
46        impl SnapshotRng for $rng {
47            fn capture(&self) -> Result<Vec<u8>, CheckpointError> {
48                bincode::serialize(self).map_err(|e| CheckpointError::SerializeError(Box::new(e)))
49            }
50
51            fn restore(bytes: &[u8]) -> Result<Self, CheckpointError> {
52                bincode::deserialize(bytes)
53                    .map_err(|e| CheckpointError::DeserializeError(Box::new(e)))
54            }
55        }
56    };
57}
58
59impl_snapshot_rng!(ChaCha8Rng);
60impl_snapshot_rng!(ChaCha12Rng);
61impl_snapshot_rng!(ChaCha20Rng);
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use rand::{Rng, SeedableRng};
67
68    #[test]
69    fn test_snapshot_rng_round_trip_is_bit_identical() {
70        // regression: EV-02 - a captured RNG must restore to the exact same
71        // state, producing an identical sequence of draws afterwards.
72        let mut rng = ChaCha8Rng::seed_from_u64(42);
73        // Advance the stream so we are not just testing the seeded state.
74        for _ in 0..37 {
75            let _: u64 = rng.gen();
76        }
77
78        let bytes = rng.capture().unwrap();
79        let mut restored = ChaCha8Rng::restore(&bytes).unwrap();
80
81        // Both generators must now produce an identical sequence.
82        for _ in 0..1000 {
83            let a: u64 = rng.gen();
84            let b: u64 = restored.gen();
85            assert_eq!(a, b, "restored RNG diverged from the original");
86        }
87    }
88
89    #[test]
90    fn test_snapshot_rng_variants() {
91        let mut c12 = ChaCha12Rng::seed_from_u64(7);
92        let _: u32 = c12.gen();
93        let bytes = c12.capture().unwrap();
94        let mut r12 = ChaCha12Rng::restore(&bytes).unwrap();
95        assert_eq!(c12.gen::<u64>(), r12.gen::<u64>());
96
97        let mut c20 = ChaCha20Rng::seed_from_u64(9);
98        let _: u32 = c20.gen();
99        let bytes = c20.capture().unwrap();
100        let mut r20 = ChaCha20Rng::restore(&bytes).unwrap();
101        assert_eq!(c20.gen::<u64>(), r20.gen::<u64>());
102    }
103}