@@ -3,7 +3,9 @@ use crate::sim_node::{
33} ;
44use crate :: SimulationError ;
55use async_trait:: async_trait;
6+ use rand:: { rngs:: StdRng , SeedableRng } ;
67use rand_distr:: { Distribution , Poisson } ;
8+ use std:: sync:: Mutex ;
79use std:: time:: Duration ;
810use tokio:: { select, time} ;
911
@@ -13,20 +15,34 @@ where
1315 D : Distribution < f32 > + Send + Sync ,
1416{
1517 latency_dist : D ,
18+ /// Seedable RNG used to sample the latency distribution. Held behind a mutex because the interceptor is shared
19+ /// across concurrent HTLCs, and seeded (rather than `thread_rng`) so that simulation runs are reproducible.
20+ rng : Mutex < StdRng > ,
1621}
1722
1823impl LatencyIntercepor < Poisson < f32 > > {
19- pub fn new_poisson ( lambda_ms : f32 ) -> Result < Self , SimulationError > {
24+ /// Creates a latency interceptor that samples delays from a Poisson distribution. If `seed` is provided the
25+ /// sampled latencies are reproducible; otherwise the RNG is seeded from entropy.
26+ pub fn new_poisson ( lambda_ms : f32 , seed : Option < u64 > ) -> Result < Self , SimulationError > {
2027 let poisson_dist = Poisson :: new ( lambda_ms) . map_err ( |e| {
2128 SimulationError :: SimulatedNetworkError ( format ! ( "Could not create possion: {e}" ) )
2229 } ) ?;
2330
2431 Ok ( Self {
2532 latency_dist : poisson_dist,
33+ rng : Mutex :: new ( seeded_rng ( seed) ) ,
2634 } )
2735 }
2836}
2937
38+ /// Builds an RNG from an optional seed, falling back to entropy when no seed is provided.
39+ fn seeded_rng ( seed : Option < u64 > ) -> StdRng {
40+ match seed {
41+ Some ( seed) => StdRng :: seed_from_u64 ( seed) ,
42+ None => StdRng :: from_entropy ( ) ,
43+ }
44+ }
45+
3046#[ async_trait]
3147impl < D > Interceptor for LatencyIntercepor < D >
3248where
3753 & self ,
3854 req : InterceptRequest ,
3955 ) -> Result < Result < CustomRecords , ForwardingError > , CriticalError > {
40- let latency = self . latency_dist . sample ( & mut rand:: thread_rng ( ) ) ;
56+ let latency = {
57+ let mut rng = self
58+ . rng
59+ . lock ( )
60+ . expect ( "latency interceptor RNG lock poisoned" ) ;
61+ self . latency_dist . sample ( & mut * rng)
62+ } ;
4163
4264 select ! {
4365 _ = req. shutdown_listener => log:: debug!( "Latency interceptor exiting due to shutdown signal received." ) ,
@@ -62,7 +84,9 @@ mod tests {
6284 use lightning:: ln:: PaymentHash ;
6385 use ntest:: assert_true;
6486 use rand:: distributions:: Distribution ;
65- use rand:: Rng ;
87+ use rand:: rngs:: StdRng ;
88+ use rand:: { Rng , SeedableRng } ;
89+ use std:: sync:: Mutex ;
6690 use tokio:: time:: timeout;
6791 use triggered:: Trigger ;
6892
@@ -105,7 +129,10 @@ mod tests {
105129 async fn test_shutdown_signal ( ) {
106130 // Set fixed dist to a high value so that the test won't flake.
107131 let latency_dist = ConstantDistribution { value : 1000.0 } ;
108- let interceptor = LatencyIntercepor { latency_dist } ;
132+ let interceptor = LatencyIntercepor {
133+ latency_dist,
134+ rng : Mutex :: new ( StdRng :: seed_from_u64 ( 0 ) ) ,
135+ } ;
109136
110137 let ( request, trigger) = test_request ( ) ;
111138 trigger. trigger ( ) ;
@@ -121,7 +148,10 @@ mod tests {
121148 #[ tokio:: test]
122149 async fn test_latency_response ( ) {
123150 let latency_dist = ConstantDistribution { value : 0.0 } ;
124- let interceptor = LatencyIntercepor { latency_dist } ;
151+ let interceptor = LatencyIntercepor {
152+ latency_dist,
153+ rng : Mutex :: new ( StdRng :: seed_from_u64 ( 0 ) ) ,
154+ } ;
125155
126156 let ( request, _) = test_request ( ) ;
127157 // We should return immediately because timeout is zero.
0 commit comments