Skip to content

Commit

Permalink
sim-lib: Allows amount and interval ranges in manual activity definition
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sr-gi committed Oct 25, 2023
1 parent 3001a22 commit cc19a99
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 7 deletions.
73 changes: 66 additions & 7 deletions sim-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -86,6 +87,52 @@ pub struct SimParams {
pub activity: Vec<ActivityParser>,
}

/// Either a value or a range parsed from the simulation file.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ValueOrRange<T> {
Value(T),
Range(T, T),
}

impl<T> ValueOrRange<T>
where
T: std::cmp::PartialOrd + rand_distr::uniform::SampleUniform + Copy,
{
/// Get the amount for the payment, in msats. If amount is a range, samples from it uniformly at random to ger the value.
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<T> Display for ValueOrRange<T>
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 amount of m_sat to used in a payment. Either a value or a range.
type Amount = ValueOrRange<u64>;
/// The interval of seconds between payments. Either a value or a range.
type Interval = ValueOrRange<u16>;

/// 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)]
Expand All @@ -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.
Expand All @@ -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)]
Expand Down Expand Up @@ -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: {} -> {}.",
Expand All @@ -776,8 +825,18 @@ 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,
Expand Down
36 changes: 36 additions & 0 deletions sim-lib/src/serializers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S, T>(x: &ValueOrRange<T>, serializer: S) -> Result<S::Ok, S::Error>
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<ValueOrRange<T>, 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 payment 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<String, D::Error>
where
D: serde::Deserializer<'de>,
Expand Down

0 comments on commit cc19a99

Please sign in to comment.