Skip to content

sim-lib: add simulated clock to speed up simulations #242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions sim-cli/src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use bitcoin::secp256k1::PublicKey;
use clap::{builder::TypedValueParser, Parser};
use log::LevelFilter;
use serde::{Deserialize, Serialize};
use simln_lib::clock::SimulationClock;
use simln_lib::sim_node::{
ln_node_from_graph, populate_network_graph, ChannelPolicy, SimGraph, SimulatedChannel,
};
Expand Down Expand Up @@ -83,6 +84,10 @@ pub struct Cli {
/// Seed to run random activity generator deterministically
#[clap(long, short)]
pub fix_seed: Option<u64>,
/// A multiplier to wall time to speed up the simulation's clock. Only available when when running on a network of
/// simulated nodes.
#[clap(long)]
pub speedup_clock: Option<u16>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue with this argument

Command sim-cli: Short option names must be unique for each argument, but '-s' is in use by both 'sim_file' and 'speedup_clock'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, fixed!

}

impl Cli {
Expand All @@ -102,6 +107,12 @@ impl Cli {
nodes or sim_graph to run with simulated nodes"
));
}
if !sim_params.nodes.is_empty() && self.speedup_clock.is_some() {
return Err(anyhow!(
"Clock speedup is only allowed when running on a simulated network"
));
}

Ok(())
}
}
Expand Down Expand Up @@ -198,7 +209,7 @@ pub async fn create_simulation_with_network(
cli: &Cli,
sim_params: &SimParams,
tasks: TaskTracker,
) -> Result<(Simulation, Vec<ActivityDefinition>), anyhow::Error> {
) -> Result<(Simulation<SimulationClock>, Vec<ActivityDefinition>), anyhow::Error> {
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
let SimParams {
nodes: _,
Expand Down Expand Up @@ -228,11 +239,13 @@ pub async fn create_simulation_with_network(
.map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?,
));

let clock = Arc::new(SimulationClock::new(cli.speedup_clock.unwrap_or(1))?);

// Copy all simulated channels into a read-only routing graph, allowing to pathfind for
// individual payments without locking th simulation graph (this is a duplication of the channels,
// but the performance tradeoff is worthwhile for concurrent pathfinding).
let routing_graph = Arc::new(
populate_network_graph(channels)
populate_network_graph(channels, clock.clone())
.map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?,
);

Expand All @@ -241,7 +254,14 @@ pub async fn create_simulation_with_network(
get_validated_activities(&nodes, nodes_info, sim_params.activity.clone()).await?;

Ok((
Simulation::new(cfg, nodes, tasks, shutdown_trigger, shutdown_listener),
Simulation::new(
cfg,
nodes,
tasks,
clock,
shutdown_trigger,
shutdown_listener,
),
validated_activities,
))
}
Expand All @@ -252,7 +272,7 @@ pub async fn create_simulation(
cli: &Cli,
sim_params: &SimParams,
tasks: TaskTracker,
) -> Result<(Simulation, Vec<ActivityDefinition>), anyhow::Error> {
) -> Result<(Simulation<SimulationClock>, Vec<ActivityDefinition>), anyhow::Error> {
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
let SimParams {
nodes,
Expand All @@ -267,7 +287,16 @@ pub async fn create_simulation(
get_validated_activities(&clients, clients_info, sim_params.activity.clone()).await?;

Ok((
Simulation::new(cfg, clients, tasks, shutdown_trigger, shutdown_listener),
Simulation::new(
cfg,
clients,
tasks,
// When running on a real network, the underlying node may use wall time so we always use a clock with no
// speedup.
Arc::new(SimulationClock::new(1)?),
shutdown_trigger,
shutdown_listener,
),
validated_activities,
))
}
Expand Down
136 changes: 136 additions & 0 deletions simln-lib/src/clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use async_trait::async_trait;
use std::ops::{Div, Mul};
use std::time::{Duration, SystemTime};
use tokio::time::{self, Instant};

use crate::SimulationError;

#[async_trait]
pub trait Clock: Send + Sync {
fn now(&self) -> SystemTime;
async fn sleep(&self, wait: Duration);
}

/// Provides a wall clock implementation of the Clock trait.
#[derive(Clone)]
pub struct SystemClock {}

#[async_trait]
impl Clock for SystemClock {
fn now(&self) -> SystemTime {
SystemTime::now()
}

async fn sleep(&self, wait: Duration) {
time::sleep(wait).await;
}
}

/// Provides an implementation of the Clock trait that speeds up wall clock time by some factor.
#[derive(Clone)]
pub struct SimulationClock {
// The multiplier that the regular wall clock is sped up by, must be in [1, 1000].
speedup_multiplier: u16,

/// Tracked so that we can calculate our "fast-forwarded" present relative to the time that we started running at.
/// This is useful, because it allows us to still rely on the wall clock, then just convert based on our speedup.
/// This field is expressed as an Instant for convenience.
start_instant: Instant,
}

impl SimulationClock {
/// Creates a new simulated clock that will speed up wall clock time by the multiplier provided, which must be in
/// [1;1000] because our asynchronous sleep only supports a duration of ms granularity.
pub fn new(speedup_multiplier: u16) -> Result<Self, SimulationError> {
if speedup_multiplier < 1 {
return Err(SimulationError::SimulatedNetworkError(
"speedup_multiplier must be at least 1".to_string(),
));
}

if speedup_multiplier > 1000 {
return Err(SimulationError::SimulatedNetworkError(
"speedup_multiplier must be less than 1000, because the simulation sleeps with millisecond
granularity".to_string(),
));
}

Ok(SimulationClock {
speedup_multiplier,
start_instant: Instant::now(),
})
}

/// Calculates the current simulation time based on the current wall clock time.
///
/// Separated for testing purposes so that we can fix the current wall clock time and elapsed interval.
fn calc_now(&self, now: SystemTime, elapsed: Duration) -> SystemTime {
now.checked_add(self.simulated_to_wall_clock(elapsed))
.expect("simulation time overflow")
}

/// Converts a duration expressed in wall clock time to the amount of equivalent time that should be used in our
/// sped up time.
fn wall_clock_to_simulated(&self, d: Duration) -> Duration {
d.div(self.speedup_multiplier.into())
}

/// Converts a duration expressed in sped up simulation time to the be expressed in wall clock time.
fn simulated_to_wall_clock(&self, d: Duration) -> Duration {
d.mul(self.speedup_multiplier.into())
}
}

#[async_trait]
impl Clock for SimulationClock {
/// To get the current time according to our simulation clock, we get the amount of wall clock time that has
/// elapsed since the simulator clock was created and multiply it by our speedup.
fn now(&self) -> SystemTime {
self.calc_now(SystemTime::now(), self.start_instant.elapsed())
}

/// To provide a sped up sleep time, we scale the proposed wait time by our multiplier and sleep.
async fn sleep(&self, wait: Duration) {
time::sleep(self.wall_clock_to_simulated(wait)).await;
}
}

#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};

use crate::clock::SimulationClock;

/// Tests validation and that a multplier of 1 is a regular clock.
#[test]
fn test_simulation_clock() {
assert!(SimulationClock::new(0).is_err());
assert!(SimulationClock::new(1001).is_err());

let clock = SimulationClock::new(1).unwrap();
let now = SystemTime::now();
let elapsed = Duration::from_secs(15);

assert_eq!(
clock.calc_now(now, elapsed),
now.checked_add(elapsed).unwrap(),
);
}

/// Test that time is sped up by multiplier.
#[test]
fn test_clock_speedup() {
let clock = SimulationClock::new(10).unwrap();
let now = SystemTime::now();

assert_eq!(
clock.calc_now(now, Duration::from_secs(1)),
now.checked_add(Duration::from_secs(10)).unwrap(),
);

assert_eq!(
clock.calc_now(now, Duration::from_secs(50)),
now.checked_add(Duration::from_secs(500)).unwrap(),
);
}
}
Loading