From cfd394abf5714d88be43afbafa2dd75f940f587d Mon Sep 17 00:00:00 2001 From: sangbida Date: Wed, 30 Apr 2025 08:52:14 +1000 Subject: [PATCH 1/6] Add support for salted rng for each node --- simln-lib/src/lib.rs | 94 ++++++++++++++++++++++++++++---- simln-lib/src/random_activity.rs | 6 +- 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index 47dba7b0..fb743344 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -6,7 +6,6 @@ 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; @@ -490,7 +489,10 @@ type MutRngType = Arc>; /// 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(MutRngType); +struct MutRng { + rng: MutRngType, + seed: Option, +} impl MutRng { /// Creates a new MutRng given an optional `u64` argument. If `seed_opt` is `Some`, @@ -498,10 +500,33 @@ impl MutRng { /// If it is `None`, activity generation is truly random, and based on a /// non-deterministic source of entropy. pub fn new(seed_opt: Option) -> Self { - 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 { + rng: Arc::new(StdMutex::new(if let Some(seed) = seed_opt { + ChaCha8Rng::seed_from_u64(seed) + } else { + ChaCha8Rng::from_entropy() + })), + seed: seed_opt, + } + } + + /// 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) -> Self { + // Get the pubkey bytes + let pubkey_bytes = pubkey.serialize(); + + // Creates a 64-bit salt value from a pubkey, which is typically much longer than 8 bytes. + let mut salt: u64 = 0; + for (i, &byte) in pubkey_bytes.iter().enumerate() { + salt ^= (byte as u64) << ((i % 8) * 8); + } + + let salted_seed = self.seed.unwrap_or(0) ^ salt; + + Self { + rng: Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(salted_seed))), + seed: Some(salted_seed), } } } @@ -946,6 +971,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 = self.cfg.seeded_rng.salted(&node_info.pubkey); generators.push(ExecutorKit { source_info: node_info.clone(), network_generator: network_generator.clone(), @@ -954,7 +981,7 @@ impl Simulation { *capacity, self.cfg.expected_payment_msat, self.cfg.activity_multiplier, - self.cfg.seeded_rng.clone(), + salted_rng, ) .map_err(SimulationError::RandomActivityError)?, ), @@ -1510,8 +1537,8 @@ mod tests { 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(); + let mut rng_1 = mut_rng_1.rng.lock().unwrap(); + let mut rng_2 = mut_rng_2.rng.lock().unwrap(); assert_eq!(rng_1.next_u64(), rng_2.next_u64()) } @@ -1522,12 +1549,57 @@ mod tests { 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(); + let mut rng_1 = mut_rng_1.rng.lock().unwrap(); + let mut rng_2 = mut_rng_2.rng.lock().unwrap(); 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 = PublicKey::from_slice(&[ + 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, + 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, + 0x5B, 0x16, 0xF8, 0x17, 0x98, + ]) + .unwrap(); + + let pk2 = PublicKey::from_slice(&[ + 0x03, 0xDF, 0xF1, 0xD7, 0x7F, 0x2A, 0x67, 0x1C, 0x5F, 0x36, 0x18, 0x37, 0xF1, 0x81, + 0x6E, 0x96, 0x76, 0x87, 0xF5, 0x64, 0x7A, 0xD8, 0x4A, 0xD0, 0x89, 0xFA, 0x3B, 0xFC, + 0x58, 0xE5, 0x47, 0xCD, 0x18, + ]) + .unwrap(); + + let salted_rng_1 = base_rng.salted(&pk1); + let salted_rng_2 = base_rng.salted(&pk2); + + let mut seq1 = Vec::new(); + let mut seq2 = Vec::new(); + + let mut rng1 = salted_rng_1.rng.lock().unwrap(); + let mut rng2 = salted_rng_2.rng.lock().unwrap(); + + for _ in 0..10 { + seq1.push(rng1.next_u64()); + seq2.push(rng2.next_u64()); + } + + assert_ne!(seq1, seq2); + + let salted_rng_1_again = base_rng.salted(&pk1); + let mut rng1_again = salted_rng_1_again.rng.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 {} diff --git a/simln-lib/src/random_activity.rs b/simln-lib/src/random_activity.rs index b22139d9..a32559a5 100644 --- a/simln-lib/src/random_activity.rs +++ b/simln-lib/src/random_activity.rs @@ -75,7 +75,7 @@ impl DestinationGenerator for NetworkGraphView { ) -> Result<(NodeInfo, Option), DestinationGenerationError> { let mut rng = self .rng - .0 + .rng .lock() .map_err(|e| DestinationGenerationError(e.to_string()))?; // While it's very unlikely that we can't pick a destination that is not our source, it's possible that there's @@ -227,7 +227,7 @@ impl PaymentGenerator for RandomPaymentActivity { fn next_payment_wait(&self) -> Result { let mut rng = self .rng - .0 + .rng .lock() .map_err(|e| PaymentGenerationError(e.to_string()))?; let duration_in_secs = self.event_dist.sample(&mut *rng) as u64; @@ -269,7 +269,7 @@ impl PaymentGenerator for RandomPaymentActivity { let mut rng = self .rng - .0 + .rng .lock() .map_err(|e| PaymentGenerationError(e.to_string()))?; let payment_amount = log_normal.sample(&mut *rng) as u64; From 83118f97ffc52d8e6b23895a9dd57da582f95800 Mon Sep 17 00:00:00 2001 From: sangbida Date: Fri, 2 May 2025 16:13:41 +1000 Subject: [PATCH 2/6] remove seed from MutRng --- simln-lib/src/lib.rs | 69 ++++++++++---------------------- simln-lib/src/random_activity.rs | 6 +-- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index fb743344..b2ca929e 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -12,6 +12,7 @@ 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; @@ -489,10 +490,7 @@ type MutRngType = Arc>; /// 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 { - rng: MutRngType, - seed: Option, -} +struct MutRng(MutRngType); impl MutRng { /// Creates a new MutRng given an optional `u64` argument. If `seed_opt` is `Some`, @@ -500,34 +498,20 @@ impl MutRng { /// If it is `None`, activity generation is truly random, and based on a /// non-deterministic source of entropy. pub fn new(seed_opt: Option) -> Self { - Self { - rng: Arc::new(StdMutex::new(if let Some(seed) = seed_opt { - ChaCha8Rng::seed_from_u64(seed) - } else { - ChaCha8Rng::from_entropy() - })), - seed: seed_opt, + if let Some(seed) = seed_opt { + Self(Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(seed)))) + } else { + 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) -> Self { - // Get the pubkey bytes - let pubkey_bytes = pubkey.serialize(); - - // Creates a 64-bit salt value from a pubkey, which is typically much longer than 8 bytes. - let mut salt: u64 = 0; - for (i, &byte) in pubkey_bytes.iter().enumerate() { - salt ^= (byte as u64) << ((i % 8) * 8); - } - - let salted_seed = self.seed.unwrap_or(0) ^ salt; - - Self { - rng: Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(salted_seed))), - seed: Some(salted_seed), - } + let mut hasher = DefaultHasher::new(); + pubkey.hash(&mut hasher); + let salt = hasher.finish(); + Self(Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(salt)))) } } @@ -960,7 +944,7 @@ impl Simulation { let network_generator = Arc::new(Mutex::new( NetworkGraphView::new( active_nodes.values().cloned().collect(), - self.cfg.seeded_rng.clone(), + MutRng(self.cfg.seeded_rng.0.clone()), ) .map_err(SimulationError::RandomActivityError)?, )); @@ -1537,8 +1521,8 @@ mod tests { let mut_rng_1 = MutRng::new(Some(seed)); let mut_rng_2 = MutRng::new(Some(seed)); - let mut rng_1 = mut_rng_1.rng.lock().unwrap(); - let mut rng_2 = mut_rng_2.rng.lock().unwrap(); + 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()) } @@ -1549,8 +1533,8 @@ mod tests { let mut_rng_1 = MutRng::new(None); let mut_rng_2 = MutRng::new(None); - let mut rng_1 = mut_rng_1.rng.lock().unwrap(); - let mut rng_2 = mut_rng_2.rng.lock().unwrap(); + 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()) } @@ -1559,19 +1543,8 @@ mod tests { fn create_salted_mut_rng() { let base_rng = MutRng::new(Some(42)); - let pk1 = PublicKey::from_slice(&[ - 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, - 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, - 0x5B, 0x16, 0xF8, 0x17, 0x98, - ]) - .unwrap(); - - let pk2 = PublicKey::from_slice(&[ - 0x03, 0xDF, 0xF1, 0xD7, 0x7F, 0x2A, 0x67, 0x1C, 0x5F, 0x36, 0x18, 0x37, 0xF1, 0x81, - 0x6E, 0x96, 0x76, 0x87, 0xF5, 0x64, 0x7A, 0xD8, 0x4A, 0xD0, 0x89, 0xFA, 0x3B, 0xFC, - 0x58, 0xE5, 0x47, 0xCD, 0x18, - ]) - .unwrap(); + let (_, pk1) = test_utils::get_random_keypair(); + let (_, pk2) = test_utils::get_random_keypair(); let salted_rng_1 = base_rng.salted(&pk1); let salted_rng_2 = base_rng.salted(&pk2); @@ -1579,8 +1552,8 @@ mod tests { let mut seq1 = Vec::new(); let mut seq2 = Vec::new(); - let mut rng1 = salted_rng_1.rng.lock().unwrap(); - let mut rng2 = salted_rng_2.rng.lock().unwrap(); + 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()); @@ -1589,8 +1562,8 @@ mod tests { assert_ne!(seq1, seq2); - let salted_rng_1_again = base_rng.salted(&pk1); - let mut rng1_again = salted_rng_1_again.rng.lock().unwrap(); + let salted_rng1_again = base_rng.salted(&pk1); + let mut rng1_again = salted_rng1_again.0.lock().unwrap(); let mut seq1_again = Vec::new(); for _ in 0..10 { diff --git a/simln-lib/src/random_activity.rs b/simln-lib/src/random_activity.rs index a32559a5..b22139d9 100644 --- a/simln-lib/src/random_activity.rs +++ b/simln-lib/src/random_activity.rs @@ -75,7 +75,7 @@ impl DestinationGenerator for NetworkGraphView { ) -> Result<(NodeInfo, Option), DestinationGenerationError> { let mut rng = self .rng - .rng + .0 .lock() .map_err(|e| DestinationGenerationError(e.to_string()))?; // While it's very unlikely that we can't pick a destination that is not our source, it's possible that there's @@ -227,7 +227,7 @@ impl PaymentGenerator for RandomPaymentActivity { fn next_payment_wait(&self) -> Result { let mut rng = self .rng - .rng + .0 .lock() .map_err(|e| PaymentGenerationError(e.to_string()))?; let duration_in_secs = self.event_dist.sample(&mut *rng) as u64; @@ -269,7 +269,7 @@ impl PaymentGenerator for RandomPaymentActivity { let mut rng = self .rng - .rng + .0 .lock() .map_err(|e| PaymentGenerationError(e.to_string()))?; let payment_amount = log_normal.sample(&mut *rng) as u64; From 4a6076d06f5164645841c749b4bb48eb8be6122b Mon Sep 17 00:00:00 2001 From: sangbida Date: Mon, 5 May 2025 14:19:26 +1000 Subject: [PATCH 3/6] Add seed to SimulationCfg --- simln-lib/src/lib.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index b2ca929e..f647567b 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -527,8 +527,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 { @@ -544,7 +544,7 @@ impl SimulationCfg { expected_payment_msat, activity_multiplier, write_results, - seeded_rng: MutRng::new(seed), + seed, } } } @@ -941,12 +941,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(), - MutRng(self.cfg.seeded_rng.0.clone()), - ) - .map_err(SimulationError::RandomActivityError)?, + NetworkGraphView::new(active_nodes.values().cloned().collect(), base_rng.clone()) + .map_err(SimulationError::RandomActivityError)?, )); log::info!( @@ -956,7 +955,7 @@ impl Simulation { for (node_info, capacity) in active_nodes.values() { // Create a salted RNG for this node based on its pubkey - let salted_rng = self.cfg.seeded_rng.salted(&node_info.pubkey); + let salted_rng = base_rng.salted(&node_info.pubkey); generators.push(ExecutorKit { source_info: node_info.clone(), network_generator: network_generator.clone(), From fc783594e68e98d02e7b33b5bd574130e07c18f6 Mon Sep 17 00:00:00 2001 From: sangbida Date: Mon, 5 May 2025 14:58:52 +1000 Subject: [PATCH 4/6] Add base seed to salt --- simln-lib/src/lib.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index f647567b..9d147b05 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -507,9 +507,14 @@ impl MutRng { /// 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) -> Self { + pub fn salted(&self, pubkey: &PublicKey, seed: u64) -> Self { let mut hasher = DefaultHasher::new(); - pubkey.hash(&mut hasher); + + // 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)))) } @@ -955,7 +960,7 @@ 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); + 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(), @@ -1545,8 +1550,8 @@ mod tests { let (_, pk1) = test_utils::get_random_keypair(); let (_, pk2) = test_utils::get_random_keypair(); - let salted_rng_1 = base_rng.salted(&pk1); - let salted_rng_2 = base_rng.salted(&pk2); + 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(); @@ -1561,7 +1566,7 @@ mod tests { assert_ne!(seq1, seq2); - let salted_rng1_again = base_rng.salted(&pk1); + 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(); From 47cf93bb6b1518d67b933e1914f14f3e8e378f08 Mon Sep 17 00:00:00 2001 From: sangbida Date: Wed, 7 May 2025 06:56:43 +1000 Subject: [PATCH 5/6] fix PR nits --- simln-lib/src/lib.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index 9d147b05..4dee7af2 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -507,10 +507,10 @@ impl MutRng { /// 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 { + pub fn salted(pubkey: &PublicKey, seed: u64) -> Self { let mut hasher = DefaultHasher::new(); - // Get pubkey bytes and concatenate with seed bytes + // Get pubkey bytes and concatenate with seed bytes. let mut combined = pubkey.serialize().to_vec(); combined.extend_from_slice(&seed.to_le_bytes()); @@ -946,7 +946,7 @@ impl Simulation { active_nodes.insert(node_info.pubkey, (node_info, capacity)); } - // Create a base RNG from the seed for the network generator + // 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(), base_rng.clone()) @@ -959,8 +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)); + // Create a salted RNG for this node based on its pubkey. + let salted_rng = MutRng::salted(&node_info.pubkey, self.cfg.seed.unwrap_or(0)); generators.push(ExecutorKit { source_info: node_info.clone(), network_generator: network_generator.clone(), @@ -1002,7 +1002,7 @@ impl Simulation { // Generate a consumer for the receiving end of the channel. It takes the event receiver that it'll pull // events from and the results sender to report the events it has triggered for further monitoring. - // ce: consume event + // ce: consume event. let ce_listener = self.shutdown_listener.clone(); let ce_shutdown = self.shutdown_trigger.clone(); let ce_output_sender = output_sender.clone(); @@ -1545,13 +1545,9 @@ mod tests { #[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(); @@ -1566,7 +1562,7 @@ mod tests { assert_ne!(seq1, seq2); - let salted_rng1_again = base_rng.salted(&pk1, 42); + let salted_rng1_again = MutRng::salted(&pk1, 42); let mut rng1_again = salted_rng1_again.0.lock().unwrap(); let mut seq1_again = Vec::new(); @@ -1761,7 +1757,7 @@ mod tests { let result = simulation.validate_node_network().await; assert!(result.is_err()); - assert!(matches!(result, + assert!(matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("we don't control any nodes"))); } @@ -1777,7 +1773,7 @@ mod tests { let result = simulation.validate_node_network().await; assert!(result.is_err()); - assert!(matches!(result, + assert!(matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("mainnet is not supported"))); } @@ -1793,7 +1789,7 @@ mod tests { let result = simulation.validate_node_network().await; assert!(result.is_err()); - assert!(matches!(result, + assert!(matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("nodes are not on the same network"))); } From 74223815fef64d065b7292e0ef187b32dea3761c Mon Sep 17 00:00:00 2001 From: sangbida Date: Wed, 7 May 2025 07:21:11 +1000 Subject: [PATCH 6/6] Consolidate salted seed functionality into new --- simln-lib/src/lib.rs | 61 +++++++++++++++++--------------- simln-lib/src/random_activity.rs | 8 ++--- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index 4dee7af2..c80f94d5 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -493,30 +493,29 @@ type MutRngType = Arc>; struct MutRng(MutRngType); impl MutRng { - /// Creates a new MutRng given an optional `u64` argument. If `seed_opt` is `Some`, + /// Creates a new MutRng given an optional `u64` seed and optional pubkey. 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) -> Self { - if let Some(seed) = seed_opt { - Self(Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(seed)))) + /// If a pubkey is provided, it will be used to salt the seed to ensure each node gets a deterministic but + /// different RNG sequence. + pub fn new(seed_opt: Option, pubkey: Option<&PublicKey>) -> Self { + let seed = if let Some(seed) = seed_opt { + if let Some(pk) = pubkey { + let mut hasher = DefaultHasher::new(); + let mut combined = pk.serialize().to_vec(); + combined.extend_from_slice(&seed.to_le_bytes()); + combined.hash(&mut hasher); + hasher.finish() + } else { + seed + } } else { - 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(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()); + // If no seed is provided, use a random one. + rand::random() + }; - combined.hash(&mut hasher); - let salt = hasher.finish(); - Self(Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(salt)))) + Self(Arc::new(StdMutex::new(ChaCha8Rng::seed_from_u64(seed)))) } } @@ -946,11 +945,13 @@ 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); + // Create a network generator with a shared RNG for all nodes. let network_generator = Arc::new(Mutex::new( - NetworkGraphView::new(active_nodes.values().cloned().collect(), base_rng.clone()) - .map_err(SimulationError::RandomActivityError)?, + NetworkGraphView::new( + active_nodes.values().cloned().collect(), + MutRng::new(self.cfg.seed, None), + ) + .map_err(SimulationError::RandomActivityError)?, )); log::info!( @@ -960,7 +961,7 @@ impl Simulation { for (node_info, capacity) in active_nodes.values() { // Create a salted RNG for this node based on its pubkey. - let salted_rng = MutRng::salted(&node_info.pubkey, self.cfg.seed.unwrap_or(0)); + let salted_rng = MutRng::new(self.cfg.seed, Some(&node_info.pubkey)); generators.push(ExecutorKit { source_info: node_info.clone(), network_generator: network_generator.clone(), @@ -1522,8 +1523,8 @@ mod tests { 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 = MutRng::new(Some(seed), None); + let mut_rng_2 = MutRng::new(Some(seed), None); let mut rng_1 = mut_rng_1.0.lock().unwrap(); let mut rng_2 = mut_rng_2.0.lock().unwrap(); @@ -1534,8 +1535,8 @@ mod tests { #[test] fn create_unseeded_mut_rng() { - let mut_rng_1 = MutRng::new(None); - let mut_rng_2 = MutRng::new(None); + let mut_rng_1 = MutRng::new(None, None); + let mut_rng_2 = MutRng::new(None, None); let mut rng_1 = mut_rng_1.0.lock().unwrap(); let mut rng_2 = mut_rng_2.0.lock().unwrap(); @@ -1548,6 +1549,8 @@ mod tests { let (_, pk1) = test_utils::get_random_keypair(); let (_, pk2) = test_utils::get_random_keypair(); + let salted_rng_1 = MutRng::new(Some(42), Some(&pk1)); + let salted_rng_2 = MutRng::new(Some(42), Some(&pk2)); let mut seq1 = Vec::new(); let mut seq2 = Vec::new(); @@ -1562,7 +1565,7 @@ mod tests { assert_ne!(seq1, seq2); - let salted_rng1_again = MutRng::salted(&pk1, 42); + let salted_rng1_again = MutRng::new(Some(42), Some(&pk1)); let mut rng1_again = salted_rng1_again.0.lock().unwrap(); let mut seq1_again = Vec::new(); diff --git a/simln-lib/src/random_activity.rs b/simln-lib/src/random_activity.rs index b22139d9..3638874a 100644 --- a/simln-lib/src/random_activity.rs +++ b/simln-lib/src/random_activity.rs @@ -308,7 +308,7 @@ mod tests { #[test] fn test_new() { // Check that we need, at least, two nodes - let rng = MutRng::new(Some(u64::MAX)); + let rng = MutRng::new(Some(u64::MAX), None); for i in 0..2 { assert!(matches!( NetworkGraphView::new(create_nodes(i, 42 * (i as u64 + 1)), rng.clone()), @@ -362,7 +362,7 @@ mod tests { nodes.extend(create_nodes(big_node_count, big_node_capacity)); let big_node = nodes.last().unwrap().0.pubkey; - let rng = MutRng::new(Some(u64::MAX)); + let rng = MutRng::new(Some(u64::MAX), None); let view = NetworkGraphView::new(nodes, rng).unwrap(); for _ in 0..10 { @@ -380,7 +380,7 @@ mod tests { // For the payment activity generator to fail during construction either the provided capacity must fail validation or the exponential // distribution must fail building given the inputs. The former will be thoroughly tested in its own unit test, but we'll test some basic cases // here. Mainly, if the `capacity < expected_payment_amnt / 2`, the generator will fail building - let rng = MutRng::new(Some(u64::MAX)); + let rng = MutRng::new(Some(u64::MAX), None); let expected_payment = get_random_int(1, 100); assert!(RandomPaymentActivity::new( 2 * expected_payment, @@ -453,7 +453,7 @@ mod tests { // All of them will yield a sigma squared smaller than 0, which we have a sanity check for. let expected_payment = get_random_int(1, 100); let source_capacity = 2 * expected_payment; - let rng = MutRng::new(Some(u64::MAX)); + let rng = MutRng::new(Some(u64::MAX), None); let pag = RandomPaymentActivity::new(source_capacity, expected_payment, 1.0, rng).unwrap();