diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index a2cf9906..9a255dce 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -45,22 +45,6 @@ fn deserialize_f64_greater_than_zero(x: String) -> Result { } } -/// 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 { @@ -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, } #[tokio::main] diff --git a/sim-lib/src/defined_activity.rs b/sim-lib/src/defined_activity.rs index c27cf0eb..eb4cd2b6 100644 --- a/sim-lib/src/defined_activity.rs +++ b/sim-lib/src/defined_activity.rs @@ -1,5 +1,6 @@ use crate::{ - DestinationGenerator, NodeInfo, PaymentGenerationError, PaymentGenerator, ValueOrRange, + DestinationGenerationError, DestinationGenerator, NodeInfo, PaymentGenerationError, + PaymentGenerator, ValueOrRange, }; use std::fmt; use tokio::time::Duration; @@ -42,8 +43,11 @@ impl fmt::Display for DefinedPaymentActivity { } impl DestinationGenerator for DefinedPaymentActivity { - fn choose_destination(&self, _: bitcoin::secp256k1::PublicKey) -> (NodeInfo, Option) { - (self.destination.clone(), None) + fn choose_destination( + &self, + _: bitcoin::secp256k1::PublicKey, + ) -> Result<(NodeInfo, Option), DestinationGenerationError> { + Ok((self.destination.clone(), None)) } } @@ -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 { + Ok(Duration::from_secs(self.wait.value() as u64)) } fn payment_amount( @@ -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; @@ -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()); diff --git a/sim-lib/src/lib.rs b/sim-lib/src/lib.rs index 4eb102c8..5239e63d 100644 --- a/sim-lib/src/lib.rs +++ b/sim-lib/src/lib.rs @@ -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)] @@ -304,10 +306,17 @@ pub trait LightningNode: Send { async fn list_channels(&mut self) -> Result, 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); + fn choose_destination( + &self, + source: PublicKey, + ) -> Result<(NodeInfo, Option), DestinationGenerationError>; } #[derive(Debug, Error)] @@ -322,7 +331,7 @@ pub trait PaymentGenerator: Display + Send { fn payment_count(&self) -> Option; /// 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; /// Returns a payment amount based, with a destination capacity optionally provided to inform the amount picked. fn payment_amount( @@ -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, /// Seed to deterministically run the random activity generator - seed: Option<[u8; 32]>, + seed: Option, } #[derive(Clone)] @@ -483,7 +492,7 @@ impl Simulation { expected_payment_msat: u64, activity_multiplier: f64, write_results: Option, - seed: Option<[u8; 32]>, + seed: Option, ) -> Self { let (shutdown_trigger, shutdown_listener) = triggered::trigger(); Self { @@ -770,7 +779,7 @@ impl Simulation { async fn activity_executors( &self, - seed: Option<[u8; 32]>, + seed: Option, ) -> Result, SimulationError> { let mut generators = Vec::new(); @@ -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, ) -> Result, 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). @@ -1033,7 +1042,7 @@ async fn consume_events( async fn produce_events( source: NodeInfo, network_generator: Arc>, - node_generator: Box, + mut node_generator: Box, sender: Sender, listener: Listener, ) -> Result<(), SimulationError> { @@ -1062,7 +1071,9 @@ async fn produce_events { - 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 diff --git a/sim-lib/src/random_activity.rs b/sim-lib/src/random_activity.rs index 8860805b..202f3e25 100644 --- a/sim-lib/src/random_activity.rs +++ b/sim-lib/src/random_activity.rs @@ -1,14 +1,18 @@ use core::fmt; -use rand::{rngs::ThreadRng, CryptoRng, Rng, RngCore, SeedableRng}; +use rand::{rngs::StdRng, RngCore, SeedableRng}; use rand_chacha::ChaCha8Rng; -use std::fmt::Display; +use std::sync::Mutex; +use std::{fmt::Display, sync::Arc}; use thiserror::Error; use bitcoin::secp256k1::PublicKey; use rand_distr::{Distribution, Exp, LogNormal, WeightedIndex}; use std::time::Duration; -use crate::{DestinationGenerator, NodeInfo, PaymentGenerationError, PaymentGenerator}; +use crate::{ + DestinationGenerationError, DestinationGenerator, NodeInfo, PaymentGenerationError, + PaymentGenerator, +}; const HOURS_PER_MONTH: u64 = 30 * 24; const SECONDS_PER_MONTH: u64 = HOURS_PER_MONTH * 60 * 60; @@ -21,6 +25,10 @@ pub enum RandomActivityError { InsufficientCapacity(String), } +/// Seeded random number generator `T`, i.e. `Option>` wrapped in `Arc>` +/// for thread-safe shared access with interior mutability +type SeededRng = Arc>>>; + /// `NetworkGraphView` maintains a view of the network graph that can be used to pick nodes by their deployed liquidity /// and track node capacity within the network. The `NetworkGraphView` also keeps a handle on an optional seed that /// allows it to deterministically pick nodes. Tracking nodes in the network is memory-expensive, so we @@ -29,7 +37,7 @@ pub enum RandomActivityError { pub struct NetworkGraphView { node_picker: WeightedIndex, nodes: Vec<(NodeInfo, u64)>, - seed: Option<[u8; 32]>, + seeded_rng: SeededRng, } impl NetworkGraphView { @@ -38,7 +46,7 @@ impl NetworkGraphView { /// send to itself). pub fn new( nodes: Vec<(NodeInfo, u64)>, - seed: Option<[u8; 32]>, + seed: Option, ) -> Result { if nodes.len() < 2 { return Err(RandomActivityError::ValueError( @@ -59,10 +67,15 @@ impl NetworkGraphView { let node_picker = WeightedIndex::new(nodes.iter().map(|(_, v)| *v).collect::>()) .map_err(|e| RandomActivityError::ValueError(e.to_string()))?; + let seeded_rng = + Arc::new(Mutex::new(seed.map(|val| { + Box::new(ChaCha8Rng::seed_from_u64(val)) as Box + }))); + Ok(NetworkGraphView { node_picker, nodes, - seed, + seeded_rng, }) } } @@ -71,20 +84,27 @@ impl DestinationGenerator for NetworkGraphView { /// Randomly samples the network for a node, weighted by capacity. Using a single graph view means that it's /// possible for a source node to select itself. After sufficient retries, this is highly improbable (even with /// very small graphs, or those with one node significantly more capitalized than others). - fn choose_destination(&self, source: PublicKey) -> (NodeInfo, Option) { - let mut rng = sim_rng(self.seed); + fn choose_destination( + &self, + source: PublicKey, + ) -> Result<(NodeInfo, Option), DestinationGenerationError> { + let mut rng = self + .seeded_rng + .lock() + .map_err(|e| DestinationGenerationError(e.to_string()))?; + let rng = rng.get_or_insert_with(|| Box::new(StdRng::from_entropy())); // While it's very unlikely that we can't pick a destination that is not our source, it's possible that there's // a bug in our selection, so we track attempts to select a non-source node so that we can warn if this takes // improbably long. let mut i = 1; loop { - let index = self.node_picker.sample(&mut rng); + let index = self.node_picker.sample(&mut *rng); // Unwrapping is safe given `NetworkGraphView` has the same amount of elements for `nodes` and `node_picker` let (node_info, capacity) = self.nodes.get(index).unwrap(); if node_info.pubkey != source { - return (node_info.clone(), Some(*capacity)); + return Ok((node_info.clone(), Some(*capacity))); } if i % 50 == 0 { @@ -112,7 +132,7 @@ pub struct RandomPaymentActivity { expected_payment_amt: u64, source_capacity: u64, event_dist: Exp, - seed: Option<[u8; 32]>, + seeded_rng: SeededRng, } impl RandomPaymentActivity { @@ -123,7 +143,7 @@ impl RandomPaymentActivity { source_capacity_msat: u64, expected_payment_amt: u64, multiplier: f64, - seed: Option<[u8; 32]>, + seed: Option, ) -> Result { if source_capacity_msat == 0 { return Err(RandomActivityError::ValueError( @@ -154,12 +174,17 @@ impl RandomPaymentActivity { let event_dist = Exp::new(lamda).map_err(|e| RandomActivityError::ValueError(e.to_string()))?; + let seeded_rng = + Arc::new(Mutex::new(seed.map(|val| { + Box::new(ChaCha8Rng::seed_from_u64(val)) as Box + }))); + Ok(RandomPaymentActivity { multiplier, expected_payment_amt, source_capacity: source_capacity_msat, event_dist, - seed, + seeded_rng, }) } @@ -208,10 +233,6 @@ fn events_per_month(source_capacity_msat: u64, multiplier: f64, expected_payment (source_capacity_msat as f64 * multiplier) / expected_payment_amt as f64 } -trait SimRng: CryptoRng + RngCore {} -impl SimRng for ChaCha8Rng {} -impl SimRng for ThreadRng {} - impl PaymentGenerator for RandomPaymentActivity { /// Returns the time that the payments should start. This will always be 0 for the RandomPaymentActivity type. fn payment_start(&self) -> Duration { @@ -224,9 +245,18 @@ impl PaymentGenerator for RandomPaymentActivity { } /// Returns the amount of time until the next payment should be scheduled for the node. - fn next_payment_wait(&self) -> Duration { - let mut rng = sim_rng(self.seed); - Duration::from_secs(self.event_dist.sample(&mut rng) as u64) + fn next_payment_wait(&mut self) -> Result { + let mut rng_guard = self + .seeded_rng + .lock() + .map_err(|e| PaymentGenerationError(e.to_string()))?; + let mut rng = rng_guard + .take() + .unwrap_or_else(|| Box::new(StdRng::from_entropy())); + let duration_in_secs = self.event_dist.sample(&mut *rng) as u64; + + *rng_guard = Some(rng); + Ok(Duration::from_secs(duration_in_secs)) } /// Returns the payment amount for a payment to a node with the destination capacity provided. The expected value @@ -261,9 +291,18 @@ impl PaymentGenerator for RandomPaymentActivity { let log_normal = LogNormal::new(mu, sigma_square.sqrt()) .map_err(|e| PaymentGenerationError(e.to_string()))?; - let mut rng = sim_rng(self.seed); + let mut rng_guard = self + .seeded_rng + .lock() + .map_err(|e| PaymentGenerationError(e.to_string()))?; + let mut rng = rng_guard + .take() + .unwrap_or_else(|| Box::new(StdRng::from_entropy())); + + let payment_amount = log_normal.sample(&mut *rng) as u64; - Ok(log_normal.sample(&mut rng) as u64) + *rng_guard = Some(rng); + Ok(payment_amount) } } @@ -286,22 +325,8 @@ impl Display for RandomPaymentActivity { } } -/// Random number generator that takes an optional `u64` seed with which deterministic -/// behaviour can be simulated -fn sim_rng(seed: Option<[u8; 32]>) -> impl SimRng { - match seed { - Some(seed) => rand_chacha::ChaCha8Rng::from_seed(seed), - None => { - let mut thread_rng = rand::thread_rng(); - ChaCha8Rng::seed_from_u64(thread_rng.gen::()) - }, - } -} - #[cfg(test)] mod tests { - use crate::test_utils::{generate_random_seed, seed_hex}; - mod test_network_graph_view { use ntest::timeout; @@ -367,7 +392,7 @@ mod tests { let view = NetworkGraphView::new(nodes, None).unwrap(); for _ in 0..10 { - view.choose_destination(big_node); + view.choose_destination(big_node).unwrap(); } } } @@ -386,7 +411,7 @@ mod tests { 2 * expected_payment, expected_payment, 1.0, - Some([42; 32]) + Some(u64::MAX) ) .is_ok()); assert!(matches!( @@ -394,7 +419,7 @@ mod tests { 2 * expected_payment, expected_payment + 1, 1.0, - Some([42; 32]) + Some(u64::MAX) ), Err(RandomActivityError::InsufficientCapacity { .. }) )); @@ -406,7 +431,7 @@ mod tests { 0, get_random_int(1, 10), get_random_int(1, 10) as f64, - Some([42; 32]) + Some(u64::MAX) ), Err(RandomActivityError::ValueError { .. }) )); @@ -415,7 +440,7 @@ mod tests { get_random_int(1, 10), 0, get_random_int(1, 10) as f64, - Some([42; 32]) + Some(u64::MAX) ), Err(RandomActivityError::ValueError { .. }) )); @@ -424,7 +449,7 @@ mod tests { get_random_int(1, 10), get_random_int(1, 10), 0.0, - Some([42; 32]) + Some(u64::MAX) ), Err(RandomActivityError::ValueError { .. }) )); @@ -450,8 +475,8 @@ mod tests { } } - #[test] - fn test_payment_amount() { + #[tokio::test] + async fn test_payment_amount() { // The special cases for payment_amount are those who may make the internal log normal distribution fail to build, which happens if // sigma squared is either +-INF or NaN. Given that the constructor of the PaymentActivityGenerator already forces its internal values // to be greater than zero, the only values that are left are all values of `destination_capacity` smaller or equal to the `source_capacity` @@ -459,7 +484,7 @@ mod tests { let expected_payment = get_random_int(1, 100); let source_capacity = 2 * expected_payment; let pag = - RandomPaymentActivity::new(source_capacity, expected_payment, 1.0, Some([42; 32])) + RandomPaymentActivity::new(source_capacity, expected_payment, 1.0, Some(u64::MAX)) .unwrap(); // Wrong cases @@ -486,15 +511,4 @@ mod tests { )); } } - - #[test] - fn random_seed_generation() { - let seed = generate_random_seed(); - let seed_hex = seed_hex(seed); - let hex_decode = hex::decode(seed_hex).expect("Hex decoding failed"); - - for (i, e) in hex_decode.into_iter().enumerate() { - assert_eq!(e, seed[i]); - } - } } diff --git a/sim-lib/src/test_utils.rs b/sim-lib/src/test_utils.rs index 3bd665a9..8d3505bc 100644 --- a/sim-lib/src/test_utils.rs +++ b/sim-lib/src/test_utils.rs @@ -1,6 +1,6 @@ use lightning::ln::features::Features; -use rand::{distributions::Uniform, thread_rng}; -use rand::{Rng, RngCore}; +use rand::distributions::Uniform; +use rand::Rng; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; @@ -45,16 +45,3 @@ pub fn create_nodes(n: usize, node_capacity: u64) -> Vec<(NodeInfo, u64)> { }) .collect() } - -/// Create hexadecimal encoding of a seed byte-array -pub fn seed_hex(seed: [u8; 32]) -> String { - hex::encode(seed) -} - -/// Generates a random seed -pub fn generate_random_seed() -> [u8; 32] { - let mut seed = [0u8; 32]; - let mut rng = thread_rng(); - rng.fill_bytes(&mut seed); - seed -}