Skip to content

Commit e30aedb

Browse files
carlaKCclaude
andcommitted
simln-lib: seed the latency interceptor's RNG
The latency interceptor sampled its forwarding delay from rand::thread_rng(), an unseeded global RNG. That made HTLC latencies non-reproducible even when the simulation was given a fixed seed. Hold a seedable StdRng on the interceptor (behind a mutex, since it is shared across concurrent HTLCs) and seed it from the new_poisson constructor. The CLI threads its fixed seed through when building the interceptor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0d163bb commit e30aedb

2 files changed

Lines changed: 39 additions & 6 deletions

File tree

sim-cli/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ async fn main() -> anyhow::Result<()> {
3939
} else {
4040
let latency = cli.latency_ms.unwrap_or(0);
4141
let interceptors = if latency > 0 {
42-
vec![Arc::new(LatencyIntercepor::new_poisson(latency as f32)?) as Arc<dyn Interceptor>]
42+
vec![Arc::new(LatencyIntercepor::new_poisson(
43+
latency as f32,
44+
cli.fix_seed,
45+
)?) as Arc<dyn Interceptor>]
4346
} else {
4447
vec![]
4548
};

simln-lib/src/latency_interceptor.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use crate::sim_node::{
33
};
44
use crate::SimulationError;
55
use async_trait::async_trait;
6+
use rand::{rngs::StdRng, SeedableRng};
67
use rand_distr::{Distribution, Poisson};
8+
use std::sync::Mutex;
79
use std::time::Duration;
810
use 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

1823
impl 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]
3147
impl<D> Interceptor for LatencyIntercepor<D>
3248
where
@@ -37,7 +53,13 @@ where
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

Comments
 (0)