diff --git a/sim-lib/src/lib.rs b/sim-lib/src/lib.rs index c572e955..b3542fd6 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 serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fmt::{Display, Formatter}; @@ -86,6 +87,52 @@ pub struct SimParams { pub activity: Vec, } +/// Either a value or a range parsed from the simulation file. +#[derive(Debug, Clone, 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 aa 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)] @@ -97,9 +144,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. @@ -111,9 +159,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)] @@ -762,7 +810,8 @@ async fn produce_events( shutdown: Trigger, listener: Listener, ) { - let interval = time::Duration::from_secs(act.interval_secs as u64); + let mut interval = time::Duration::from_secs(act.interval_secs.value() as u64); + let mut amt = act.amount_msat.value(); log::debug!( "Started producer for {} every {}s: {} -> {}.", @@ -776,8 +825,17 @@ async fn produce_events( tokio::select! { biased; _ = time::sleep(interval) => { - // Consumer was dropped - if sender.send(SimulationEvent::SendPayment(act.destination.clone(), act.amount_msat)).await.is_err() { + // Resample if needed + if act.interval_secs.is_range() { + interval = time::Duration::from_secs(act.interval_secs.value() as u64); + log::debug!("Resampling interval. New value: {}", interval.as_secs()); + } + if act.amount_msat.is_range() { + amt = act.amount_msat.value(); + log::debug!("Resampling payment amount. New value: {}", amt); + } + if sender.send(SimulationEvent::SendPayment(act.destination.clone(), amt)).await.is_err() { + // Consumer was dropped log::debug!( "Stopped producer for {}: {} -> {}. Consumer cannot be reached.", act.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>,