Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: run simulator with fixed seed #178

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ of the random activity that is generated:
capacity in a month.
* `capacity-multiplier=0.5` means that each node sends half their
capacity in a month.
* `--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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice docs!

nit: double space in The simulations


### Setup - Defined Activity
If you would like SimLN to generate a specific payments between source
Expand Down
2 changes: 2 additions & 0 deletions sim-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ sim-lib = { path = "../sim-lib" }
tokio = { version = "1.26.0", features = ["full"] }
bitcoin = { version = "0.30.1" }
ctrlc = "3.4.0"
rand = "0.8.5"
hex = {version = "0.4.3"}
4 changes: 4 additions & 0 deletions sim-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ struct Cli {
/// Do not create an output file containing the simulations results
#[clap(long, default_value_t = false)]
no_results: bool,
/// Seed to run random activity generator deterministically
#[clap(long, short)]
fix_seed: Option<u64>,
}

#[tokio::main]
Expand Down Expand Up @@ -208,6 +211,7 @@ async fn main() -> anyhow::Result<()> {
cli.expected_pmt_amt,
cli.capacity_multiplier,
write_results,
cli.fix_seed,
);
let sim2 = sim.clone();

Expand Down
1 change: 1 addition & 0 deletions sim-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ csv = "1.2.2"
serde_millis = "0.1.1"
rand_distr = "0.4.3"
mockall = "0.12.1"
rand_chacha = "0.3.1"

[dev-dependencies]
ntest = "0.9.0"
16 changes: 10 additions & 6 deletions sim-lib/src/defined_activity.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
DestinationGenerator, NodeInfo, PaymentGenerationError, PaymentGenerator, ValueOrRange,
DestinationGenerationError, DestinationGenerator, NodeInfo, PaymentGenerationError,
PaymentGenerator, ValueOrRange,
};
use std::fmt;
use tokio::time::Duration;
Expand Down Expand Up @@ -42,8 +43,11 @@ impl fmt::Display for DefinedPaymentActivity {
}

impl DestinationGenerator for DefinedPaymentActivity {
fn choose_destination(&self, _: bitcoin::secp256k1::PublicKey) -> (NodeInfo, Option<u64>) {
(self.destination.clone(), None)
fn choose_destination(
&self,
_: bitcoin::secp256k1::PublicKey,
) -> Result<(NodeInfo, Option<u64>), DestinationGenerationError> {
Ok((self.destination.clone(), None))
}
}

Expand All @@ -56,8 +60,8 @@ impl PaymentGenerator for DefinedPaymentActivity {
self.count
}

fn next_payment_wait(&self) -> Duration {
Duration::from_secs(self.wait.value() as u64)
fn next_payment_wait(&self) -> Result<Duration, PaymentGenerationError> {
Ok(Duration::from_secs(self.wait.value() as u64))
}

fn payment_amount(
Expand Down Expand Up @@ -97,7 +101,7 @@ mod tests {
crate::ValueOrRange::Value(payment_amt),
);

let (dest, dest_capacity) = generator.choose_destination(source.1);
let (dest, dest_capacity) = generator.choose_destination(source.1).unwrap();
assert_eq!(node.pubkey, dest.pubkey);
assert!(dest_capacity.is_none());

Expand Down
105 changes: 93 additions & 12 deletions sim-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ use bitcoin::Network;
use csv::WriterBuilder;
use lightning::ln::features::NodeFeatures;
use lightning::ln::PaymentHash;
use rand::Rng;
use rand::rngs::StdRng;
use rand::{Rng, RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
use random_activity::RandomActivityError;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::marker::Send;
use std::path::PathBuf;
use std::sync::Mutex as StdMutex;
use std::time::{SystemTimeError, UNIX_EPOCH};
use std::{collections::HashMap, sync::Arc, time::SystemTime};
use thiserror::Error;
Expand Down Expand Up @@ -235,6 +238,8 @@ pub enum SimulationError {
MpscChannelError(String),
#[error("Payment Generation Error: {0}")]
PaymentGenerationError(PaymentGenerationError),
#[error("Destination Generation Error: {0}")]
DestinationGenerationError(DestinationGenerationError),
}

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

#[derive(Debug, Error)]
#[error("Destination generation error: {0}")]
pub struct DestinationGenerationError(String);

pub trait DestinationGenerator: Send {
/// choose_destination picks a destination node within the network, returning the node's information and its
/// capacity (if available).
fn choose_destination(&self, source: PublicKey) -> (NodeInfo, Option<u64>);
fn choose_destination(
&self,
source: PublicKey,
) -> Result<(NodeInfo, Option<u64>), DestinationGenerationError>;
}

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

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

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

/// MutRngType is a convenient type alias for any random number generator (RNG) type that
/// allows shared and exclusive access. This is necessary because a single RNG
/// is to be shared across multiple `DestinationGenerator`s and `PaymentGenerator`s
/// for deterministic outcomes.
///
/// **Note**: `StdMutex`, i.e. (`std::sync::Mutex`), is used here to avoid making the traits
/// `DestinationGenerator` and `PaymentGenerator` async.
type MutRngType = Arc<StdMutex<Box<dyn RngCore + Send>>>;
carlaKC marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Box is unnecessary here?


/// Newtype for `MutRngType` to encapsulate and hide implementation details for
/// creating new `MutRngType` types. Provides convenient API for the same purpose.
#[derive(Clone)]
struct MutRng(pub MutRngType);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not need pub?


impl MutRng {
/// Creates a new MutRng given an optional `u64` argument. If `seed_opt` is `Some`,
/// random activity generation in the simulator occurs near-deterministically.
/// If it is `None`, activity generation is truly random, and based on a
/// non-deterministic source of entropy.
pub fn new(seed_opt: Option<u64>) -> Self {
if let Some(seed) = seed_opt {
Self(Arc::new(StdMutex::new(
Box::new(ChaCha8Rng::seed_from_u64(seed)) as Box<dyn RngCore + Send>,
)))
} else {
Self(Arc::new(StdMutex::new(Box::new(StdRng::from_entropy()))))
}
}
}

#[derive(Clone)]
pub struct Simulation {
/// The lightning node that is being simulated.
Expand All @@ -453,6 +495,8 @@ pub struct Simulation {
activity_multiplier: f64,
/// Configurations for printing results to CSV. Results are not written if this option is None.
write_results: Option<WriteResults>,
/// Random number generator created from fixed seed.
seeded_rng: MutRng,
}

#[derive(Clone)]
Expand All @@ -462,7 +506,7 @@ pub struct WriteResults {
/// The number of activity results to batch before printing in CSV.
pub batch_size: u32,
}
///

/// ExecutorKit contains the components required to spin up an activity configured by the user, to be used to
/// spin up the appropriate producers and consumers for the activity.
struct ExecutorKit {
Expand All @@ -481,6 +525,7 @@ impl Simulation {
expected_payment_msat: u64,
activity_multiplier: f64,
write_results: Option<WriteResults>,
seed: Option<u64>,
) -> Self {
let (shutdown_trigger, shutdown_listener) = triggered::trigger();
Self {
Expand All @@ -492,6 +537,7 @@ impl Simulation {
expected_payment_msat,
activity_multiplier,
write_results,
seeded_rng: MutRng::new(seed),
}
}

Expand Down Expand Up @@ -823,8 +869,11 @@ impl Simulation {
}

let network_generator = Arc::new(Mutex::new(
NetworkGraphView::new(active_nodes.values().cloned().collect())
.map_err(SimulationError::RandomActivityError)?,
NetworkGraphView::new(
active_nodes.values().cloned().collect(),
self.seeded_rng.clone(),
)
.map_err(SimulationError::RandomActivityError)?,
));

log::info!(
Expand All @@ -841,6 +890,7 @@ impl Simulation {
*capacity,
self.expected_payment_msat,
self.activity_multiplier,
self.seeded_rng.clone(),
)
.map_err(SimulationError::RandomActivityError)?,
),
Expand Down Expand Up @@ -1047,11 +1097,11 @@ async fn produce_events<N: DestinationGenerator + ?Sized, A: PaymentGenerator +
}
start
} else {
log::debug!(
"Next payment for {source} in {:?}.",
node_generator.next_payment_wait()
);
node_generator.next_payment_wait()
let wait = node_generator
.next_payment_wait()
.map_err(SimulationError::PaymentGenerationError)?;
log::debug!("Next payment for {source} in {:?}.", wait);
wait
};

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

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

Ok(())
}

#[cfg(test)]
mod tests {
use crate::MutRng;

#[test]
fn create_seeded_mut_rng() {
let seeds = vec![u64::MIN, u64::MAX];

for seed in seeds {
let mut_rng_1 = MutRng::new(Some(seed));
let mut_rng_2 = MutRng::new(Some(seed));

let mut rng_1 = mut_rng_1.0.lock().unwrap();
let mut rng_2 = mut_rng_2.0.lock().unwrap();

assert_eq!(rng_1.next_u64(), rng_2.next_u64())
}
}

#[test]
fn create_unseeded_mut_rng() {
let mut_rng_1 = MutRng::new(None);
let mut_rng_2 = MutRng::new(None);

let mut rng_1 = mut_rng_1.0.lock().unwrap();
let mut rng_2 = mut_rng_2.0.lock().unwrap();

assert_ne!(rng_1.next_u64(), rng_2.next_u64())
}
}
Loading