diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index 47dba7b0..9d147b05 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -6,13 +6,13 @@ use bitcoin::Network; use csv::WriterBuilder; use lightning::ln::features::NodeFeatures; use lightning::ln::PaymentHash; -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::hash::{DefaultHasher, Hash, Hasher}; use std::marker::Send; use std::path::PathBuf; use std::sync::Mutex as StdMutex; @@ -501,9 +501,23 @@ impl MutRng { if let Some(seed) = seed_opt { Self(Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(seed)))) } else { - Self(Arc::new(StdMutex::new(StdRng::from_entropy()))) + Self(Arc::new(StdMutex::new(ChaCha8Rng::from_entropy()))) } } + + /// Creates a new MutRng that is salted with a pubkey. This ensures that each node + /// gets a deterministic but different RNG sequence. + pub fn salted(&self, pubkey: &PublicKey, seed: u64) -> Self { + let mut hasher = DefaultHasher::new(); + + // Get pubkey bytes and concatenate with seed bytes + let mut combined = pubkey.serialize().to_vec(); + combined.extend_from_slice(&seed.to_le_bytes()); + + combined.hash(&mut hasher); + let salt = hasher.finish(); + Self(Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(salt)))) + } } /// Contains the configuration options for our simulation. @@ -518,8 +532,8 @@ pub struct SimulationCfg { activity_multiplier: f64, /// Configurations for printing results to CSV. Results are not written if this option is None. write_results: Option, - /// Random number generator created from fixed seed. - seeded_rng: MutRng, + /// Optional seed for deterministic random number generation. + seed: Option, } impl SimulationCfg { @@ -535,7 +549,7 @@ impl SimulationCfg { expected_payment_msat, activity_multiplier, write_results, - seeded_rng: MutRng::new(seed), + seed, } } } @@ -932,12 +946,11 @@ impl Simulation { active_nodes.insert(node_info.pubkey, (node_info, capacity)); } + // Create a base RNG from the seed for the network generator + let base_rng = MutRng::new(self.cfg.seed); let network_generator = Arc::new(Mutex::new( - NetworkGraphView::new( - active_nodes.values().cloned().collect(), - self.cfg.seeded_rng.clone(), - ) - .map_err(SimulationError::RandomActivityError)?, + NetworkGraphView::new(active_nodes.values().cloned().collect(), base_rng.clone()) + .map_err(SimulationError::RandomActivityError)?, )); log::info!( @@ -946,6 +959,8 @@ impl Simulation { ); for (node_info, capacity) in active_nodes.values() { + // Create a salted RNG for this node based on its pubkey + let salted_rng = base_rng.salted(&node_info.pubkey, self.cfg.seed.unwrap_or(0)); generators.push(ExecutorKit { source_info: node_info.clone(), network_generator: network_generator.clone(), @@ -954,7 +969,7 @@ impl Simulation { *capacity, self.cfg.expected_payment_msat, self.cfg.activity_multiplier, - self.cfg.seeded_rng.clone(), + salted_rng, ) .map_err(SimulationError::RandomActivityError)?, ), @@ -1528,6 +1543,40 @@ mod tests { assert_ne!(rng_1.next_u64(), rng_2.next_u64()) } + #[test] + fn create_salted_mut_rng() { + let base_rng = MutRng::new(Some(42)); + + let (_, pk1) = test_utils::get_random_keypair(); + let (_, pk2) = test_utils::get_random_keypair(); + + let salted_rng_1 = base_rng.salted(&pk1, 42); + let salted_rng_2 = base_rng.salted(&pk2, 42); + + let mut seq1 = Vec::new(); + let mut seq2 = Vec::new(); + + let mut rng1 = salted_rng_1.0.lock().unwrap(); + let mut rng2 = salted_rng_2.0.lock().unwrap(); + + for _ in 0..10 { + seq1.push(rng1.next_u64()); + seq2.push(rng2.next_u64()); + } + + assert_ne!(seq1, seq2); + + let salted_rng1_again = base_rng.salted(&pk1, 42); + let mut rng1_again = salted_rng1_again.0.lock().unwrap(); + let mut seq1_again = Vec::new(); + + for _ in 0..10 { + seq1_again.push(rng1_again.next_u64()); + } + + assert_eq!(seq1, seq1_again); + } + mock! { pub Generator {}