Skip to content

Commit ebef034

Browse files
committed
feat: deterministic random activity for simulator
fix: run simulator with optional fixed seed **Initial commit** What this commit does: - Makes it possible to run the simulator with an optional u64 fixed-seed for deterministic outcomes for randomly generated payment activities Notes: - This commit defines SeededRng: a thread-safe, mutually exclusive option of any type that implements RngCore and Send. - SeededRng is a field in both NetworkGraphView and RandomPaymentActivity. Both the DestinationGenerator and PaymentGenerator hold references to SeededRng in their trait implementations. If SeededRng is defined as an Option<Box<dyn RngCore + Send>>, it will be impossible to gain exclusive access (&mut) to self.seeded_rng, which is shared access (&). Mutable reference to the SeededRng is required by the distribution samplers. - Thus, SeededRng as previously defined (Option<Box<dyn RngCore + Send>>) is wrapped in Arc<Mutex<>> for exclusive access. - additionally, removes unnecessary async from defined & random activity tests **Commit update** feat: use single shared RNG across all executor kits - Creates a new type MutRng that wraps any type that implements RngCore and provides shared and exclusive access to the type. - Creates a RNG on the 'Simulation' that's shared across and accessible to all 'ExecutorKit's - add MutRng test & fix documentation issues
1 parent a9aba5d commit ebef034

File tree

7 files changed

+200
-50
lines changed

7 files changed

+200
-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.

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)