Skip to content

Commit

Permalink
fix: run simulator with optional fixed seed
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
enigbe committed Jun 27, 2024
1 parent a0a32bd commit 18f9308
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 108 deletions.
21 changes: 2 additions & 19 deletions sim-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,6 @@ fn deserialize_f64_greater_than_zero(x: String) -> Result<f64, String> {
}
}

/// Deserializes a hexadecimal seed string into a byte array
fn deserialize_fix_seed(hex: String) -> Result<[u8; 32], String> {
if hex.len() != 64 {
return Err("Seed hex must be 64-characters long.".to_string());
}

hex::decode(hex)
.map(|byte_vec| {
let mut seed = [0; 32];
let seed_from_hex = byte_vec.as_slice();
seed.clone_from_slice(seed_from_hex);
seed
})
.map_err(|e| e.to_string())
}

#[derive(Parser)]
#[command(version, about)]
struct Cli {
Expand Down Expand Up @@ -91,9 +75,8 @@ struct Cli {
#[clap(long, default_value_t = false)]
no_results: bool,
/// Seed to run random activity generator "deterministically", i.e. in the same order.
/// Expected values are hexadecimal-encoded [u8;32] seeds, e.g. hex::encode([42;32])
#[clap(long, short, value_parser = clap::builder::StringValueParser::new().try_map(deserialize_fix_seed))]
fix_seed: Option<[u8; 32]>,
#[clap(long, short)]
fix_seed: Option<u64>,
}

#[tokio::main]
Expand Down
2 changes: 1 addition & 1 deletion sim-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ mockall = "0.12.1"
rand_chacha = "0.3.1"

[dev-dependencies]
ntest = "0.9.0"
ntest = "0.9.0"
20 changes: 12 additions & 8 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(&mut self) -> Result<Duration, PaymentGenerationError> {
Ok(Duration::from_secs(self.wait.value() as u64))
}

fn payment_amount(
Expand All @@ -81,8 +85,8 @@ mod tests {
use crate::test_utils::{create_nodes, get_random_keypair};
use crate::{DestinationGenerator, PaymentGenerationError, PaymentGenerator};

#[test]
fn test_defined_activity_generator() {
#[tokio::test]
async fn test_defined_activity_generator() {
let node = create_nodes(1, 100000);
let node = &node.first().unwrap().0;

Expand All @@ -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
29 changes: 20 additions & 9 deletions sim-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,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 +306,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 +331,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(&mut 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 @@ -454,7 +463,7 @@ pub struct Simulation {
/// Configurations for printing results to CSV. Results are not written if this option is None.
write_results: Option<WriteResults>,
/// Seed to deterministically run the random activity generator
seed: Option<[u8; 32]>,
seed: Option<u64>,
}

#[derive(Clone)]
Expand Down Expand Up @@ -483,7 +492,7 @@ impl Simulation {
expected_payment_msat: u64,
activity_multiplier: f64,
write_results: Option<WriteResults>,
seed: Option<[u8; 32]>,
seed: Option<u64>,
) -> Self {
let (shutdown_trigger, shutdown_listener) = triggered::trigger();
Self {
Expand Down Expand Up @@ -770,7 +779,7 @@ impl Simulation {

async fn activity_executors(
&self,
seed: Option<[u8; 32]>,
seed: Option<u64>,
) -> Result<Vec<ExecutorKit>, SimulationError> {
let mut generators = Vec::new();

Expand Down Expand Up @@ -805,7 +814,7 @@ impl Simulation {
/// that have sufficient capacity to generate payments of our expected payment amount.
async fn random_activity_nodes(
&self,
seed: Option<[u8; 32]>,
seed: Option<u64>,
) -> Result<Vec<ExecutorKit>, SimulationError> {
// Collect capacity of each node from its view of its own channels. Total capacity is divided by two to
// avoid double counting capacity (as each node has a counterparty in the channel).
Expand Down Expand Up @@ -1033,7 +1042,7 @@ async fn consume_events(
async fn produce_events<N: DestinationGenerator + ?Sized, A: PaymentGenerator + ?Sized>(
source: NodeInfo,
network_generator: Arc<Mutex<N>>,
node_generator: Box<A>,
mut node_generator: Box<A>,
sender: Sender<SimulationEvent>,
listener: Listener,
) -> Result<(), SimulationError> {
Expand Down Expand Up @@ -1062,7 +1071,9 @@ async fn produce_events<N: DestinationGenerator + ?Sized, A: PaymentGenerator +
"Next payment for {source} in {:?}.",
node_generator.next_payment_wait()
);
node_generator.next_payment_wait()
node_generator
.next_payment_wait()
.map_err(SimulationError::PaymentGenerationError)?
};

select! {
Expand All @@ -1073,7 +1084,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
Loading

0 comments on commit 18f9308

Please sign in to comment.