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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
11 changes: 9 additions & 2 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::SystemClock;
use simln_lib::{
cln, cln::ClnNode, eclair, eclair::EclairNode, lnd, lnd::LndNode, serializers,
ActivityDefinition, Amount, Interval, LightningError, LightningNode, NodeId, NodeInfo,
Expand Down Expand Up @@ -142,7 +143,7 @@ impl TryFrom<&Cli> for SimulationCfg {

/// Parses the cli options provided and creates a simulation to be run, connecting to lightning nodes and validating
/// any activity described in the simulation file.
pub async fn create_simulation(cli: &Cli) -> Result<Simulation, anyhow::Error> {
pub async fn create_simulation(cli: &Cli) -> Result<Simulation<SystemClock>, anyhow::Error> {
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;

let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file.clone()).await?;
Expand Down Expand Up @@ -175,7 +176,13 @@ pub async fn create_simulation(cli: &Cli) -> Result<Simulation, anyhow::Error> {
validate_activities(activity, pk_node_map, alias_node_map, get_node).await?;
let tasks = TaskTracker::new();

Ok(Simulation::new(cfg, clients, validated_activities, tasks))
Ok(Simulation::new(
cfg,
clients,
validated_activities,
tasks,
Arc::new(SystemClock {}),
))
}

/// Connects to the set of nodes providing, returning a map of node public keys to LightningNode implementations and
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.
speedup_multiplier: u32,
Copy link
Contributor

Choose a reason for hiding this comment

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

since this can only be in the [1..1000] range, maybe it could be a u16 instead?


/// 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: u32) -> 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)
}

/// 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)
}
}

#[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