Skip to content

Commit f5c8430

Browse files
authored
Merge pull request #178 from enigbe/feat-run-simulator-with-fixed-seed
feat: run simulator with fixed seed
2 parents e369d61 + ce67b98 commit f5c8430

File tree

8 files changed

+201
-50
lines changed

8 files changed

+201
-50
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ of the random activity that is generated:
132132
capacity in a month.
133133
* `capacity-multiplier=0.5` means that each node sends half their
134134
capacity in a month.
135+
* `--fix-seed`: a `u64` value that allows you to generate random activities deterministically from the provided seed, albeit with some limitations. The simulations are not guaranteed to be perfectly deterministic because tasks complete in slightly different orders on each run of the simulator. With a fixed seed, we can guarantee that the order in which activities are dispatched will be deterministic.
135136

136137
### Setup - Defined Activity
137138
If you would like SimLN to generate a specific payments between source

sim-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ sim-lib = { path = "../sim-lib" }
2020
tokio = { version = "1.26.0", features = ["full"] }
2121
bitcoin = { version = "0.30.1" }
2222
ctrlc = "3.4.0"
23+
rand = "0.8.5"
24+
hex = {version = "0.4.3"}

sim-cli/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ struct Cli {
7474
/// Do not create an output file containing the simulations results
7575
#[clap(long, default_value_t = false)]
7676
no_results: bool,
77+
/// Seed to run random activity generator deterministically
78+
#[clap(long, short)]
79+
fix_seed: Option<u64>,
7780
}
7881

7982
#[tokio::main]
@@ -208,6 +211,7 @@ async fn main() -> anyhow::Result<()> {
208211
cli.expected_pmt_amt,
209212
cli.capacity_multiplier,
210213
write_results,
214+
cli.fix_seed,
211215
);
212216
let sim2 = sim.clone();
213217

sim-lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ csv = "1.2.2"
3030
serde_millis = "0.1.1"
3131
rand_distr = "0.4.3"
3232
mockall = "0.12.1"
33+
rand_chacha = "0.3.1"
3334

3435
[dev-dependencies]
3536
ntest = "0.9.0"

sim-lib/src/defined_activity.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{
2-
DestinationGenerator, NodeInfo, PaymentGenerationError, PaymentGenerator, ValueOrRange,
2+
DestinationGenerationError, DestinationGenerator, NodeInfo, PaymentGenerationError,
3+
PaymentGenerator, ValueOrRange,
34
};
45
use std::fmt;
56
use tokio::time::Duration;
@@ -42,8 +43,11 @@ impl fmt::Display for DefinedPaymentActivity {
4243
}
4344

4445
impl DestinationGenerator for DefinedPaymentActivity {
45-
fn choose_destination(&self, _: bitcoin::secp256k1::PublicKey) -> (NodeInfo, Option<u64>) {
46-
(self.destination.clone(), None)
46+
fn choose_destination(
47+
&self,
48+
_: bitcoin::secp256k1::PublicKey,
49+
) -> Result<(NodeInfo, Option<u64>), DestinationGenerationError> {
50+
Ok((self.destination.clone(), None))
4751
}
4852
}
4953

@@ -56,8 +60,8 @@ impl PaymentGenerator for DefinedPaymentActivity {
5660
self.count
5761
}
5862

59-
fn next_payment_wait(&self) -> Duration {
60-
Duration::from_secs(self.wait.value() as u64)
63+
fn next_payment_wait(&self) -> Result<Duration, PaymentGenerationError> {
64+
Ok(Duration::from_secs(self.wait.value() as u64))
6165
}
6266

6367
fn payment_amount(
@@ -97,7 +101,7 @@ mod tests {
97101
crate::ValueOrRange::Value(payment_amt),
98102
);
99103

100-
let (dest, dest_capacity) = generator.choose_destination(source.1);
104+
let (dest, dest_capacity) = generator.choose_destination(source.1).unwrap();
101105
assert_eq!(node.pubkey, dest.pubkey);
102106
assert!(dest_capacity.is_none());
103107

sim-lib/src/lib.rs

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ use bitcoin::Network;
44
use csv::WriterBuilder;
55
use lightning::ln::features::NodeFeatures;
66
use lightning::ln::PaymentHash;
7-
use rand::Rng;
7+
use rand::rngs::StdRng;
8+
use rand::{Rng, RngCore, SeedableRng};
9+
use rand_chacha::ChaCha8Rng;
810
use random_activity::RandomActivityError;
911
use serde::{Deserialize, Serialize};
1012
use std::collections::HashSet;
1113
use std::fmt::{Display, Formatter};
1214
use std::marker::Send;
1315
use std::path::PathBuf;
16+
use std::sync::Mutex as StdMutex;
1417
use std::time::{SystemTimeError, UNIX_EPOCH};
1518
use std::{collections::HashMap, sync::Arc, time::SystemTime};
1619
use thiserror::Error;
@@ -235,6 +238,8 @@ pub enum SimulationError {
235238
MpscChannelError(String),
236239
#[error("Payment Generation Error: {0}")]
237240
PaymentGenerationError(PaymentGenerationError),
241+
#[error("Destination Generation Error: {0}")]
242+
DestinationGenerationError(DestinationGenerationError),
238243
}
239244

240245
#[derive(Debug, Error)]
@@ -304,10 +309,17 @@ pub trait LightningNode: Send {
304309
async fn list_channels(&mut self) -> Result<Vec<u64>, LightningError>;
305310
}
306311

312+
#[derive(Debug, Error)]
313+
#[error("Destination generation error: {0}")]
314+
pub struct DestinationGenerationError(String);
315+
307316
pub trait DestinationGenerator: Send {
308317
/// choose_destination picks a destination node within the network, returning the node's information and its
309318
/// capacity (if available).
310-
fn choose_destination(&self, source: PublicKey) -> (NodeInfo, Option<u64>);
319+
fn choose_destination(
320+
&self,
321+
source: PublicKey,
322+
) -> Result<(NodeInfo, Option<u64>), DestinationGenerationError>;
311323
}
312324

313325
#[derive(Debug, Error)]
@@ -322,7 +334,7 @@ pub trait PaymentGenerator: Display + Send {
322334
fn payment_count(&self) -> Option<u64>;
323335

324336
/// Returns the number of seconds that a node should wait until firing its next payment.
325-
fn next_payment_wait(&self) -> time::Duration;
337+
fn next_payment_wait(&self) -> Result<time::Duration, PaymentGenerationError>;
326338

327339
/// Returns a payment amount based, with a destination capacity optionally provided to inform the amount picked.
328340
fn payment_amount(
@@ -435,6 +447,36 @@ enum SimulationOutput {
435447
SendPaymentFailure(Payment, PaymentResult),
436448
}
437449

450+
/// MutRngType is a convenient type alias for any random number generator (RNG) type that
451+
/// allows shared and exclusive access. This is necessary because a single RNG
452+
/// is to be shared across multiple `DestinationGenerator`s and `PaymentGenerator`s
453+
/// for deterministic outcomes.
454+
///
455+
/// **Note**: `StdMutex`, i.e. (`std::sync::Mutex`), is used here to avoid making the traits
456+
/// `DestinationGenerator` and `PaymentGenerator` async.
457+
type MutRngType = Arc<StdMutex<Box<dyn RngCore + Send>>>;
458+
459+
/// Newtype for `MutRngType` to encapsulate and hide implementation details for
460+
/// creating new `MutRngType` types. Provides convenient API for the same purpose.
461+
#[derive(Clone)]
462+
struct MutRng(pub MutRngType);
463+
464+
impl MutRng {
465+
/// Creates a new MutRng given an optional `u64` argument. If `seed_opt` is `Some`,
466+
/// random activity generation in the simulator occurs near-deterministically.
467+
/// If it is `None`, activity generation is truly random, and based on a
468+
/// non-deterministic source of entropy.
469+
pub fn new(seed_opt: Option<u64>) -> Self {
470+
if let Some(seed) = seed_opt {
471+
Self(Arc::new(StdMutex::new(
472+
Box::new(ChaCha8Rng::seed_from_u64(seed)) as Box<dyn RngCore + Send>,
473+
)))
474+
} else {
475+
Self(Arc::new(StdMutex::new(Box::new(StdRng::from_entropy()))))
476+
}
477+
}
478+
}
479+
438480
#[derive(Clone)]
439481
pub struct Simulation {
440482
/// The lightning node that is being simulated.
@@ -453,6 +495,8 @@ pub struct Simulation {
453495
activity_multiplier: f64,
454496
/// Configurations for printing results to CSV. Results are not written if this option is None.
455497
write_results: Option<WriteResults>,
498+
/// Random number generator created from fixed seed.
499+
seeded_rng: MutRng,
456500
}
457501

458502
#[derive(Clone)]
@@ -462,7 +506,7 @@ pub struct WriteResults {
462506
/// The number of activity results to batch before printing in CSV.
463507
pub batch_size: u32,
464508
}
465-
///
509+
466510
/// ExecutorKit contains the components required to spin up an activity configured by the user, to be used to
467511
/// spin up the appropriate producers and consumers for the activity.
468512
struct ExecutorKit {
@@ -481,6 +525,7 @@ impl Simulation {
481525
expected_payment_msat: u64,
482526
activity_multiplier: f64,
483527
write_results: Option<WriteResults>,
528+
seed: Option<u64>,
484529
) -> Self {
485530
let (shutdown_trigger, shutdown_listener) = triggered::trigger();
486531
Self {
@@ -492,6 +537,7 @@ impl Simulation {
492537
expected_payment_msat,
493538
activity_multiplier,
494539
write_results,
540+
seeded_rng: MutRng::new(seed),
495541
}
496542
}
497543

@@ -823,8 +869,11 @@ impl Simulation {
823869
}
824870

825871
let network_generator = Arc::new(Mutex::new(
826-
NetworkGraphView::new(active_nodes.values().cloned().collect())
827-
.map_err(SimulationError::RandomActivityError)?,
872+
NetworkGraphView::new(
873+
active_nodes.values().cloned().collect(),
874+
self.seeded_rng.clone(),
875+
)
876+
.map_err(SimulationError::RandomActivityError)?,
828877
));
829878

830879
log::info!(
@@ -841,6 +890,7 @@ impl Simulation {
841890
*capacity,
842891
self.expected_payment_msat,
843892
self.activity_multiplier,
893+
self.seeded_rng.clone(),
844894
)
845895
.map_err(SimulationError::RandomActivityError)?,
846896
),
@@ -1047,11 +1097,11 @@ async fn produce_events<N: DestinationGenerator + ?Sized, A: PaymentGenerator +
10471097
}
10481098
start
10491099
} else {
1050-
log::debug!(
1051-
"Next payment for {source} in {:?}.",
1052-
node_generator.next_payment_wait()
1053-
);
1054-
node_generator.next_payment_wait()
1100+
let wait = node_generator
1101+
.next_payment_wait()
1102+
.map_err(SimulationError::PaymentGenerationError)?;
1103+
log::debug!("Next payment for {source} in {:?}.", wait);
1104+
wait
10551105
};
10561106

10571107
select! {
@@ -1062,7 +1112,7 @@ async fn produce_events<N: DestinationGenerator + ?Sized, A: PaymentGenerator +
10621112
// Wait until our time to next payment has elapsed then execute a random amount payment to a random
10631113
// destination.
10641114
_ = time::sleep(wait) => {
1065-
let (destination, capacity) = network_generator.lock().await.choose_destination(source.pubkey);
1115+
let (destination, capacity) = network_generator.lock().await.choose_destination(source.pubkey).map_err(SimulationError::DestinationGenerationError)?;
10661116

10671117
// Only proceed with a payment if the amount is non-zero, otherwise skip this round. If we can't get
10681118
// a payment amount something has gone wrong (because we should have validated that we can always
@@ -1327,3 +1377,34 @@ async fn track_payment_result(
13271377

13281378
Ok(())
13291379
}
1380+
1381+
#[cfg(test)]
1382+
mod tests {
1383+
use crate::MutRng;
1384+
1385+
#[test]
1386+
fn create_seeded_mut_rng() {
1387+
let seeds = vec![u64::MIN, u64::MAX];
1388+
1389+
for seed in seeds {
1390+
let mut_rng_1 = MutRng::new(Some(seed));
1391+
let mut_rng_2 = MutRng::new(Some(seed));
1392+
1393+
let mut rng_1 = mut_rng_1.0.lock().unwrap();
1394+
let mut rng_2 = mut_rng_2.0.lock().unwrap();
1395+
1396+
assert_eq!(rng_1.next_u64(), rng_2.next_u64())
1397+
}
1398+
}
1399+
1400+
#[test]
1401+
fn create_unseeded_mut_rng() {
1402+
let mut_rng_1 = MutRng::new(None);
1403+
let mut_rng_2 = MutRng::new(None);
1404+
1405+
let mut rng_1 = mut_rng_1.0.lock().unwrap();
1406+
let mut rng_2 = mut_rng_2.0.lock().unwrap();
1407+
1408+
assert_ne!(rng_1.next_u64(), rng_2.next_u64())
1409+
}
1410+
}

0 commit comments

Comments
 (0)