1212use crate :: PPVMModule ;
1313use crate :: composite:: PPVM ;
1414use crate :: measurements:: MeasurementResult ;
15+ use rand:: { RngExt , SeedableRng , rngs:: SmallRng } ;
1516
1617/// One shot's full output: the measurement record and the trace-instruction
1718/// record. Either may be empty depending on what the program emits.
@@ -27,10 +28,15 @@ pub const PARALLEL_SHOT_THRESHOLD: usize = 128;
2728
2829/// Per-shot seed derived from the base seed and the shot index, so every shot
2930/// gets a distinct RNG stream (a shared seed would make all shots identical).
30- /// Depends only on `(base, index)`, so serial and parallel runs are bit-for-bit
31- /// identical for a given seed regardless of thread count.
31+ /// The base is scrambled first so nearby seeds give unrelated shots. Depends
32+ /// only on `(base, index)`, so serial and parallel runs are identical.
33+ #[ inline]
3234fn shot_seed ( base : Option < u64 > , index : usize ) -> Option < u64 > {
33- base. map ( |b| b. wrapping_add ( index as u64 ) )
35+ base. map ( |b| {
36+ SmallRng :: seed_from_u64 ( b)
37+ . random :: < u64 > ( )
38+ . wrapping_add ( index as u64 )
39+ } )
3440}
3541
3642/// Run a single shot on a fresh machine and return both records.
@@ -178,6 +184,19 @@ mod tests {
178184 ) ;
179185 }
180186
187+ #[ test]
188+ fn nearby_seeds_do_not_share_shots ( ) {
189+ // With `seed + i`, seed 8 was seed 7 shifted by one shot.
190+ let m = module ( RANDOM ) ;
191+ let a = run_shots_serial ( & m, 128 , Some ( 7 ) ) . unwrap ( ) ;
192+ let b = run_shots_serial ( & m, 128 , Some ( 8 ) ) . unwrap ( ) ;
193+ assert_ne ! (
194+ a[ 1 ..] ,
195+ b[ ..127 ] ,
196+ "seed 8 reproduced seed 7's shots shifted by one"
197+ ) ;
198+ }
199+
181200 #[ cfg( feature = "rayon" ) ]
182201 #[ test]
183202 fn serial_and_parallel_match_for_same_seed ( ) {
0 commit comments