From fe7bef6935d43166d75d9bc72468bf82c6e593bc Mon Sep 17 00:00:00 2001 From: Sergi Delgado Segura Date: Wed, 25 Oct 2023 14:00:51 -0400 Subject: [PATCH] sim-lib: Allows amount and interval ranges in manual activity definition Amounts and intervals in activities can now be fixed values (e.g. 1000) or ranges (e.g. [500, 1000]). If a range is provided, the value will be sampled uniformly at random from it. --- sim-lib/src/defined_activity.rs | 32 ++++++++++++------ sim-lib/src/lib.rs | 58 ++++++++++++++++++++++++++++++--- sim-lib/src/serializers.rs | 36 ++++++++++++++++++++ 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/sim-lib/src/defined_activity.rs b/sim-lib/src/defined_activity.rs index 98bbe07e..289201a3 100644 --- a/sim-lib/src/defined_activity.rs +++ b/sim-lib/src/defined_activity.rs @@ -1,16 +1,18 @@ -use crate::{DestinationGenerator, NodeInfo, PaymentGenerationError, PaymentGenerator}; +use crate::{ + DestinationGenerator, NodeInfo, PaymentGenerationError, PaymentGenerator, ValueOrRange, +}; use std::fmt; use tokio::time::Duration; #[derive(Clone)] pub struct DefinedPaymentActivity { destination: NodeInfo, - wait: Duration, - amount: u64, + wait: ValueOrRange, + amount: ValueOrRange, } impl DefinedPaymentActivity { - pub fn new(destination: NodeInfo, wait: Duration, amount: u64) -> Self { + pub fn new(destination: NodeInfo, wait: ValueOrRange, amount: ValueOrRange) -> Self { DefinedPaymentActivity { destination, wait, @@ -23,7 +25,7 @@ impl fmt::Display for DefinedPaymentActivity { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "static payment of {} to {} every {:?}", + "static payment of {} to {} every {}s", self.amount, self.destination, self.wait ) } @@ -37,7 +39,11 @@ impl DestinationGenerator for DefinedPaymentActivity { impl PaymentGenerator for DefinedPaymentActivity { fn next_payment_wait(&self) -> Duration { - self.wait + let wait = Duration::from_secs(self.wait.value() as u64); + if self.wait.is_range() { + log::debug!("Resampling interval. New value: {}", wait.as_secs()); + } + wait } fn payment_amount( @@ -49,7 +55,11 @@ impl PaymentGenerator for DefinedPaymentActivity { "destination amount must not be set for defined activity generator".to_string(), )) } else { - Ok(self.amount) + let amount = self.amount.value(); + if self.amount.is_range() { + log::debug!("Resampling payment amount. New value: {}", amount); + } + Ok(amount) } } } @@ -59,7 +69,6 @@ mod tests { use super::DefinedPaymentActivity; use crate::test_utils::{create_nodes, get_random_keypair}; use crate::{DestinationGenerator, PaymentGenerationError, PaymentGenerator}; - use std::time::Duration; #[test] fn test_defined_activity_generator() { @@ -69,8 +78,11 @@ mod tests { let source = get_random_keypair(); let payment_amt = 50; - let generator = - DefinedPaymentActivity::new(node.clone(), Duration::from_secs(60), payment_amt); + let generator = DefinedPaymentActivity::new( + node.clone(), + crate::ValueOrRange::Value(60), + crate::ValueOrRange::Value(payment_amt), + ); let (dest, dest_capacity) = generator.choose_destination(source.1); assert_eq!(node.pubkey, dest.pubkey); diff --git a/sim-lib/src/lib.rs b/sim-lib/src/lib.rs index 3e28fb78..d3cfdf3b 100644 --- a/sim-lib/src/lib.rs +++ b/sim-lib/src/lib.rs @@ -4,6 +4,7 @@ use bitcoin::Network; use csv::WriterBuilder; use lightning::ln::features::NodeFeatures; use lightning::ln::PaymentHash; +use rand::Rng; use random_activity::RandomActivityError; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -129,6 +130,52 @@ pub struct SimParams { pub activity: Vec, } +/// Either a value or a range parsed from the simulation file. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ValueOrRange { + Value(T), + Range(T, T), +} + +impl ValueOrRange +where + T: std::cmp::PartialOrd + rand_distr::uniform::SampleUniform + Copy, +{ + /// Get the enclosed value. If value is defined as a range, sample from it uniformly at random. + pub fn value(&self) -> T { + match self { + ValueOrRange::Value(x) => *x, + ValueOrRange::Range(x, y) => { + let mut rng = rand::thread_rng(); + rng.gen_range(*x..*y) + }, + } + } + + /// Whether this is a range or not + pub fn is_range(&self) -> bool { + matches!(self, ValueOrRange::Range(..)) + } +} + +impl Display for ValueOrRange +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ValueOrRange::Value(x) => write!(f, "{x}"), + ValueOrRange::Range(x, y) => write!(f, "({x}-{y})"), + } + } +} + +/// The payment amount in msat. Either a value or a range. +type Amount = ValueOrRange; +/// The interval of seconds between payments. Either a value or a range. +type Interval = ValueOrRange; + /// Data structure used to parse information from the simulation file. It allows source and destination to be /// [NodeId], which enables the use of public keys and aliases in the simulation description. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -140,9 +187,10 @@ pub struct ActivityParser { #[serde(with = "serializers::serde_node_id")] pub destination: NodeId, /// The interval of the event, as in every how many seconds the payment is performed. - pub interval_secs: u16, + pub interval_secs: Interval, /// The amount of m_sat to used in this payment. - pub amount_msat: u64, + #[serde(with = "serializers::serde_value_or_range")] + pub amount_msat: Amount, } /// Data structure used internally by the simulator. Both source and destination are represented as [PublicKey] here. @@ -154,9 +202,9 @@ pub struct ActivityDefinition { /// The destination of the payment. pub destination: NodeInfo, /// The interval of the event, as in every how many seconds the payment is performed. - pub interval_secs: u16, + pub interval_secs: Interval, /// The amount of m_sat to used in this payment. - pub amount_msat: u64, + pub amount_msat: Amount, } #[derive(Debug, Error)] @@ -667,7 +715,7 @@ impl Simulation { for description in self.activity.iter() { let activity_generator = DefinedPaymentActivity::new( description.destination.clone(), - Duration::from_secs(description.interval_secs.into()), + description.interval_secs, description.amount_msat, ); diff --git a/sim-lib/src/serializers.rs b/sim-lib/src/serializers.rs index 3fd46fa7..2ce82892 100644 --- a/sim-lib/src/serializers.rs +++ b/sim-lib/src/serializers.rs @@ -45,6 +45,42 @@ pub mod serde_node_id { } } +pub mod serde_value_or_range { + use super::*; + use serde::de::Error; + + use crate::ValueOrRange; + + pub fn serialize(x: &ValueOrRange, serializer: S) -> Result + where + S: serde::Serializer, + T: std::fmt::Display, + { + serializer.serialize_str(&match x { + ValueOrRange::Value(p) => p.to_string(), + ValueOrRange::Range(x, y) => format!("[{}, {}]", x, y), + }) + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + T: serde::Deserialize<'de> + std::cmp::PartialOrd + std::fmt::Display + Copy, + { + let a = ValueOrRange::deserialize(deserializer)?; + if let ValueOrRange::Range(x, y) = a { + if x >= y { + return Err(D::Error::custom(format!( + "Cannot parse range. Ranges must be strictly increasing (i.e. [x, y] with x > y). Received [{}, {}]", + x, y + ))); + } + } + + Ok(a) + } +} + pub fn deserialize_path<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>,