diff --git a/sim-cli/Cargo.toml b/sim-cli/Cargo.toml index 6e4a9ac4..e3249418 100755 --- a/sim-cli/Cargo.toml +++ b/sim-cli/Cargo.toml @@ -18,7 +18,8 @@ triggered = "0.1.2" serde = "1.0.183" serde_json = "1.0.104" simple_logger = "4.2.0" -simln-lib = { path = "../simln-lib" } +# The virtual-time feature is required for the --virtual-time flag, which runs simulations on a paused runtime. +simln-lib = { path = "../simln-lib", features = ["virtual-time"] } tokio = { version = "1.26.0", features = ["full"] } bitcoin = { version = "0.30.1" } ctrlc = "3.4.0" diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index 6f698746..91c08e79 100755 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -1,19 +1,19 @@ use std::sync::Arc; +use std::time::SystemTime; use clap::Parser; use log::LevelFilter; use sim_cli::parsing::{create_simulation, create_simulation_with_network, parse_sim_params, Cli}; +use simln_lib::latency_interceptor::LatencyIntercepor; +use simln_lib::sim_node::Interceptor; use simln_lib::{ - clock::SimulationClock, - latency_interceptor::LatencyIntercepor, - sim_node::{CustomRecords, Interceptor}, - SimulationCfg, + clock::SimulationClock, runtime::block_on_virtual_time, sim_node::CustomRecords, + ActivityDefinition, Simulation, SimulationCfg, }; use simple_logger::SimpleLogger; use tokio_util::task::TaskTracker; -#[tokio::main] -async fn main() -> anyhow::Result<()> { +fn main() -> anyhow::Result<()> { // Enable tracing if building in developer mode. #[cfg(feature = "dev")] { @@ -21,7 +21,7 @@ async fn main() -> anyhow::Result<()> { } let cli = Cli::parse(); - let sim_params = parse_sim_params(&cli).await?; + let sim_params = parse_sim_params(&cli)?; SimpleLogger::new() .with_level(LevelFilter::Warn) @@ -32,35 +32,52 @@ async fn main() -> anyhow::Result<()> { cli.validate(&sim_params)?; - let tasks = TaskTracker::new(); - - let (sim, validated_activities) = if sim_params.sim_network.is_empty() { - create_simulation(&cli, &sim_params, tasks.clone()).await? - } else { - let latency = cli.latency_ms.unwrap_or(0); - let interceptors = if latency > 0 { - vec![Arc::new(LatencyIntercepor::new_poisson( - latency as f32, - cli.fix_seed, - )?) as Arc] + let cfg = SimulationCfg::try_from(&cli)?; + let latency = cli.latency_ms.unwrap_or(0); + let build_and_run = move |clock: Arc| async move { + let (sim, activities) = if sim_params.sim_network.is_empty() { + create_simulation(cfg, &sim_params, clock, TaskTracker::new()).await? } else { - vec![] + let (sim, activities, _) = create_simulation_with_network( + cfg, + &sim_params, + clock, + TaskTracker::new(), + // Create an interceptor to add latency to payments, otherwise none. + if latency > 0 { + vec![Arc::new(LatencyIntercepor::new_poisson( + latency as f32, + cli.fix_seed, + )?) as Arc] + } else { + vec![] + }, + CustomRecords::default(), + ) + .await?; + (sim, activities) }; - let sim_cfg: SimulationCfg = SimulationCfg::try_from(&cli)?; - let clock = Arc::new(SimulationClock::new(cli.speedup_clock.unwrap_or(1))?); - let (sim, validated_activities, _) = create_simulation_with_network( - sim_cfg, - &sim_params, - clock, - tasks.clone(), - interceptors, - CustomRecords::default(), - ) - .await?; - (sim, validated_activities) + + run_simulation(sim, activities).await }; - let sim2 = sim.clone(); + // For virtual time, our helper will build the clock we need and pass it to build_and_run. If + // running with regular time, we can just pass a clock right in. + if cli.virtual_time { + block_on_virtual_time(SystemTime::now(), build_and_run)? + } else { + tokio::runtime::Runtime::new()?.block_on(build_and_run(Arc::new(SimulationClock::new( + SystemTime::now(), + )))) + } +} + +/// Drives a fully-configured simulation to completion, wiring up a ctrl-c handler that triggers a clean shutdown. +async fn run_simulation( + sim: Simulation, + validated_activities: Vec, +) -> anyhow::Result<()> { + let sim2 = sim.clone(); ctrlc::set_handler(move || { log::info!("Shutting down simulation."); sim2.shutdown(); diff --git a/sim-cli/src/parsing.rs b/sim-cli/src/parsing.rs index c5b06b7f..9e7902af 100755 --- a/sim-cli/src/parsing.rs +++ b/sim-cli/src/parsing.rs @@ -84,14 +84,15 @@ pub struct Cli { /// Seed to run random activity generator deterministically #[clap(long, short)] pub fix_seed: Option, - /// 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, /// Latency to optionally introduce for payments in a simulated network expressed in /// milliseconds. #[clap(long)] pub latency_ms: Option, + /// Run the simulated network on virtual time: time advances instantly to the next event instead of sleeping on the + /// wall clock, so the run finishes as fast as the CPU allows and is reproducible for a given seed. Only available on + /// a simulated network, and requires --total-time to bound the run. + #[clap(long, default_value_t = false)] + pub virtual_time: bool, } impl Cli { @@ -111,18 +112,25 @@ 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" - )); - } - if !sim_params.nodes.is_empty() && self.latency_ms.is_some() { return Err(anyhow!( "Latency for payments is only allowed when running on a simulated network" )); } + if self.virtual_time { + if !sim_params.nodes.is_empty() { + return Err(anyhow!( + "Virtual time is only allowed when running on a simulated network; real nodes run on wall time" + )); + } + if self.total_time.is_none() { + return Err(anyhow!( + "Virtual time requires --total-time, otherwise it advances forever" + )); + } + } + if !sim_params.exclude.is_empty() { if sim_params.sim_network.is_empty() { return Err(anyhow!( @@ -338,14 +346,15 @@ pub async fn create_simulation_with_network( )) } -/// 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. +/// Creates a simulation to be run against a set of real lightning nodes, connecting to the nodes described in +/// `sim_params` and validating any activity described in the simulation file. The simulation is driven by `clock`, +/// which must be constructed on the runtime that will run the simulation. pub async fn create_simulation( - cli: &Cli, + cfg: SimulationCfg, sim_params: &SimParams, + clock: Arc, tasks: TaskTracker, ) -> Result<(Simulation, Vec), anyhow::Error> { - let cfg: SimulationCfg = SimulationCfg::try_from(cli)?; let SimParams { nodes, sim_network: _sim_network, @@ -364,9 +373,7 @@ pub async fn create_simulation( 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)?), + clock, shutdown_trigger, shutdown_listener, ), @@ -529,7 +536,7 @@ async fn validate_activities( Ok(validated_activities) } -async fn read_sim_path(data_dir: PathBuf, sim_file: PathBuf) -> anyhow::Result { +fn read_sim_path(data_dir: PathBuf, sim_file: PathBuf) -> anyhow::Result { if sim_file.exists() { Ok(sim_file) } else if sim_file.is_relative() { @@ -538,15 +545,15 @@ async fn read_sim_path(data_dir: PathBuf, sim_file: PathBuf) -> anyhow::Result

anyhow::Result { +pub fn select_sim_file(data_dir: PathBuf) -> anyhow::Result { let sim_files = std::fs::read_dir(data_dir.clone())? .filter_map(|f| { f.ok().and_then(|f| { @@ -583,8 +590,8 @@ fn mkdir(dir: PathBuf) -> anyhow::Result { Ok(dir) } -pub async fn parse_sim_params(cli: &Cli) -> anyhow::Result { - let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file.clone()).await?; +pub fn parse_sim_params(cli: &Cli) -> anyhow::Result { + let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file.clone())?; let sim_params = serde_json::from_str(&std::fs::read_to_string(sim_path)?).map_err(|e| { anyhow!( "Could not deserialize node connection data or activity description from simulation file (line {}, col {}, err: {}).", diff --git a/simln-lib/Cargo.toml b/simln-lib/Cargo.toml index 39d2f5ca..8851babb 100755 --- a/simln-lib/Cargo.toml +++ b/simln-lib/Cargo.toml @@ -24,7 +24,7 @@ thiserror = "1.0.45" log = "0.4.20" triggered = "0.1.2" mpsc = "0.2.0" -tokio = "1.31.0" +tokio = { version = "1.31.0", features = ["rt", "sync", "time", "macros", "fs", "io-util"] } rand = "0.8.5" hex = "0.4.3" csv = "1.2.2" @@ -34,8 +34,12 @@ rand_chacha = "0.3.1" reqwest = { version = "0.12", features = ["json", "multipart"] } tokio-util = { version = "0.7.13", features = ["rt"] } +[features] +virtual-time = ["tokio/test-util"] + [dev-dependencies] ntest = "0.9.0" mockall = "0.13.1" futures = "0.3.31" tempfile = "3" +tokio = { version = "1.31.0", features = ["macros", "rt-multi-thread"] } diff --git a/simln-lib/src/clock.rs b/simln-lib/src/clock.rs index 7e7a4e01..fde12690 100644 --- a/simln-lib/src/clock.rs +++ b/simln-lib/src/clock.rs @@ -1,146 +1,98 @@ 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. +/// Provides a flexible implementation of [Clock] that can be used for simulations. +/// +/// For regular runtimes, this will just function as a regular wall clock, seeded with the provided starting time. +/// +/// If run with a paused Tokio runtime, this clock provides the ability to speed up simulations. When no task is +/// runnable, the runtime will auto-advance to the next time instantly, providing the ability to "progress time" +/// rather than wait for these sleeps to elapse. +/// +/// Some considerations for use of this mode: +/// * You cannot use SystemTime::now, as it will not match the simulation's +/// * If there are no tasks waiting, the simulation will "jump forward" to the next timer. Code interacting with the +/// simulator should be aware of this; if you're not actively interacting with it, it'll move on without you. There +/// is a "grace period" in the runtime that allows tasks the change to schedule themselves after the clock has been +/// progressed. +/// * This mode will misbehave if the code waits on operations that are not contained within its runtime. For example, +/// if waiting on I/O or a file operation, the tokio runtime can't see this wait and will advance without waiting on +/// the operation. +/// * If the system never reaches the state where all tasks are waiting, the clock will not advance. This may happen if +/// there is an always-pollable loop, for example. +/// +/// The clock must be constructed on the runtime it will be driven on, because it captures a [tokio::time::Instant] whose +/// progression is governed by that runtime's (possibly paused) clock. #[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. + /// The wall clock starting time that this clock was created at, used to provide calendar timestamps. + start_time: SystemTime, + /// Tracks time elapsed since the clock was created, according to the runtime used. + /// + /// Note that in a paused runtime, this will capture the progression of "virtual time" as we jump forward. 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 { - 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(), - }) - } - - /// Returns the instant that the simulation clock was started at. - pub fn get_start_instant(&self) -> Instant { - self.start_instant - } - - /// Returns the speedup multiplier applied to time. - pub fn get_speedup_multiplier(&self) -> u16 { - self.speedup_multiplier - } - - /// Calculates the current simulation time based on the current wall clock time. + /// Creates a new simulation clock that reports time relative to `start_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()) + /// Must be called on the runtime that will drive the simulation. + pub fn new(start_time: SystemTime) -> Self { + SimulationClock { + start_time, + start_instant: Instant::now(), + } } } #[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. + /// Reports the current time of the simulation. fn now(&self) -> SystemTime { - self.calc_now(SystemTime::now(), self.start_instant.elapsed()) + self.start_time + .checked_add(self.start_instant.elapsed()) + .expect("simulation clock time overflow") } - /// To provide a sped up sleep time, we scale the proposed wait time by our multiplier and sleep. + /// Sleeps for the duration provided. + /// + /// If running on a paused runtime, the clock will skip forward to the earliest waiting sleep's time if no other + /// tasks are runnable. async fn sleep(&self, wait: Duration) { - time::sleep(self.wall_clock_to_simulated(wait)).await; + time::sleep(wait).await; } } -#[cfg(test)] +// The paused-runtime test relies on tokio's test-util, which is only enabled by the virtual-time feature. +#[cfg(all(test, feature = "virtual-time"))] 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()); + use crate::clock::{Clock, SimulationClock}; - 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(), - ); - } + /// Tests that, on a paused runtime, the simulation clock advances by exactly the slept duration and consumes no + /// wall-clock time. + #[tokio::test(start_paused = true)] + async fn test_simulation_clock_advances_on_sleep() { + let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000); + let clock = SimulationClock::new(start); - /// Test that time is sped up by multiplier. - #[test] - fn test_clock_speedup() { - let clock = SimulationClock::new(10).unwrap(); - let now = SystemTime::now(); + // No virtual time has elapsed yet. + assert_eq!(clock.now(), start); - assert_eq!( - clock.calc_now(now, Duration::from_secs(1)), - now.checked_add(Duration::from_secs(10)).unwrap(), - ); + // Sleeping advances virtual time by exactly the requested duration. + clock.sleep(Duration::from_secs(3600)).await; + assert_eq!(clock.now(), start + Duration::from_secs(3600)); - assert_eq!( - clock.calc_now(now, Duration::from_secs(50)), - now.checked_add(Duration::from_secs(500)).unwrap(), - ); + // A subsequent sleep continues from where the previous one left off. + clock.sleep(Duration::from_secs(60)).await; + assert_eq!(clock.now(), start + Duration::from_secs(3660)); } } diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index ca30f4be..fc1e2923 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -38,6 +38,8 @@ pub mod eclair; pub mod latency_interceptor; pub mod lnd; mod random_activity; +#[cfg(feature = "virtual-time")] +pub mod runtime; pub mod serializers; pub mod sim_node; mod test_utils; @@ -229,6 +231,9 @@ pub enum SimulationError { /// Error that occurred while generating destination nodes. #[error("Destination Generation Error: {0}")] DestinationGenerationError(DestinationGenerationError), + /// Error that occurred while building or driving the virtual-time runtime. + #[error("Runtime Error: {0}")] + RuntimeError(String), } /// Represents errors that can occur during Lightning Network operations. @@ -559,7 +564,8 @@ impl MutRng { /// Contains the configuration options for our simulation. #[derive(Clone)] pub struct SimulationCfg { - /// Total simulation time. The simulation will run forever if undefined. + /// Total simulation time. This value must be set if running with `virtual-time`. If running with a regular wall + /// clock, the simulation will run forever if this value is not set. total_time: Option, /// The expected payment size for the network. expected_payment_msat: u64, @@ -1603,7 +1609,7 @@ async fn track_payment_result( #[cfg(test)] mod tests { - use crate::clock::SystemClock; + use crate::clock::SimulationClock; use crate::test_utils::{MockLightningNode, TestNodesResult}; use crate::{ get_payment_delay, test_utils, test_utils::LightningTestNodeBuilder, LightningError, @@ -1618,7 +1624,7 @@ mod tests { use std::str::FromStr; use std::sync::Arc; use std::sync::Mutex as StdMutex; - use std::time::Duration; + use std::time::{Duration, SystemTime}; use tokio::sync::Mutex; use tokio_util::task::TaskTracker; @@ -2079,7 +2085,7 @@ mod tests { SimulationCfg::new(None, 100, 2.0, None, None), network.get_client_hashmap(), TaskTracker::new(), - Arc::new(SystemClock {}), + Arc::new(SimulationClock::new(SystemTime::now())), shutdown_trigger, shutdown_listener, ); @@ -2147,7 +2153,7 @@ mod tests { SimulationCfg::new(Some(25), 100, 2.0, None, Some(42)), network.get_client_hashmap(), TaskTracker::new(), - Arc::new(SystemClock {}), + Arc::new(SimulationClock::new(SystemTime::now())), shutdown_trigger, shutdown_listener, ); @@ -2183,7 +2189,7 @@ mod tests { SimulationCfg::new(Some(25), 100, 2.0, None, Some(500)), network.get_client_hashmap(), TaskTracker::new(), - Arc::new(SystemClock {}), + Arc::new(SimulationClock::new(SystemTime::now())), shutdown_trigger, shutdown_listener, ); diff --git a/simln-lib/src/runtime.rs b/simln-lib/src/runtime.rs new file mode 100644 index 00000000..43932412 --- /dev/null +++ b/simln-lib/src/runtime.rs @@ -0,0 +1,99 @@ +//! Runtime helpers for driving a simulated network on virtual time. +//! +//! Running a simulation on virtual time requires a very specific runtime: single-threaded (so task scheduling is +//! deterministic) and time-paused (so virtual time auto-advances to the next timer instead of sleeping on the wall +//! clock). Rather than ask callers to configure this themselves and risk silently losing determinism or the virtual-time +//! advance, the library builds and owns the runtime here. + +use std::future::Future; +use std::sync::Arc; +use std::time::SystemTime; + +use tokio::runtime::{Builder, Handle}; + +use crate::clock::SimulationClock; +use crate::SimulationError; + +/// Runs a simulation to completion on virtual time and returns its result. +/// +/// A single-threaded, time-paused Tokio runtime is created and owned for the duration of the call. On this runtime, +/// once every task is parked on a timer the runtime advances virtual time straight to the next timer, so the simulation +/// runs as fast as the CPU allows. A [SimulationClock] anchored at `start_time` is constructed on the runtime and handed +/// to `build`, which should build the network and simulation against that clock and drive them to completion. +/// +/// This must not be called from within an existing Tokio runtime (for example, from an `async fn` or a `#[tokio::main]` +/// entry point): a runtime cannot be nested inside another. Doing so returns a [SimulationError::RuntimeError] rather +/// than panicking. Call it from a synchronous context instead. +/// +/// # Example +/// +/// ```no_run +/// use std::time::{Duration, SystemTime}; +/// use simln_lib::clock::Clock; +/// use simln_lib::runtime::block_on_virtual_time; +/// +/// let _now = block_on_virtual_time(SystemTime::UNIX_EPOCH, |clock| async move { +/// // Build your network and simulation against `clock`, then run it. A day-long sleep returns immediately. +/// clock.sleep(Duration::from_secs(86_400)).await; +/// Ok::<_, simln_lib::SimulationError>(clock.now()) +/// })??; +/// # Ok::<(), simln_lib::SimulationError>(()) +/// ``` +pub fn block_on_virtual_time( + start_time: SystemTime, + build: F, +) -> Result +where + F: FnOnce(Arc) -> Fut, + Fut: Future, +{ + if Handle::try_current().is_ok() { + return Err(SimulationError::RuntimeError( + "block_on_virtual_time cannot be called from within a Tokio runtime; call it from a \ + synchronous context so it can own a paused single-threaded runtime" + .to_string(), + )); + } + + let runtime = Builder::new_current_thread() + .enable_all() + .start_paused(true) + .build() + .map_err(|e| { + SimulationError::RuntimeError(format!("could not build virtual-time runtime: {e}")) + })?; + + Ok(runtime.block_on(async move { + // Construct the clock on the runtime so that its virtual-time instant is governed by the paused runtime. + let clock = Arc::new(SimulationClock::new(start_time)); + build(clock).await + })) +} + +#[cfg(test)] +mod tests { + use super::block_on_virtual_time; + use crate::clock::Clock; + use std::time::{Duration, SystemTime}; + + /// A simulated day elapses with no real wall-clock wait, and the clock reflects the advance. + #[test] + fn test_block_on_virtual_time_advances_virtually() { + let start = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000); + + let now = block_on_virtual_time(start, |clock| async move { + clock.sleep(Duration::from_secs(86_400)).await; + clock.now() + }) + .unwrap(); + + assert_eq!(now, start + Duration::from_secs(86_400)); + } + + /// Calling from within a runtime is rejected rather than panicking. + #[tokio::test] + async fn test_block_on_virtual_time_rejects_nested_runtime() { + let result = block_on_virtual_time(SystemTime::UNIX_EPOCH, |_clock| async {}); + assert!(result.is_err()); + } +} diff --git a/simln-lib/src/sim_node.rs b/simln-lib/src/sim_node.rs index d0940798..bb1a8873 100755 --- a/simln-lib/src/sim_node.rs +++ b/simln-lib/src/sim_node.rs @@ -1130,6 +1130,9 @@ pub async fn ln_node_from_graph( /// announcements, which has the effect of adding the nodes in each channel to the graph, because LDK does not export /// all of the fields required to apply node announcements. This means that we will not have node-level information /// (such as features) available in the routing graph. +/// +/// Note that LDK's channel validation uses the wall clock to validate that announcements are not more than 24 hours +/// in the future. If using a sped up clock, this must be called before the clock has advanced beyond that. pub fn populate_network_graph( channels: Vec, clock: Arc, @@ -1164,9 +1167,6 @@ pub fn populate_network_graph( }; graph.update_channel_from_unsigned_announcement(&announcement, &Some(&utxo_validator))?; - - // LDK only allows channel announcements up to 24h in the future. Use a fixed timestamp so that even if we've - // sped up our clock dramatically, we won't hit that limit. let now = clock.now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32; for (i, node) in [channel.node_1, channel.node_2].iter().enumerate() { let update = UnsignedChannelUpdate { @@ -1620,13 +1620,13 @@ impl UtxoLookup for UtxoValidator { mod tests { use super::*; use crate::clock::SimulationClock; - use crate::clock::SystemClock; use crate::test_utils::get_random_keypair; use lightning::routing::router::build_route_from_hops; use lightning::routing::router::Route; use mockall::mock; use ntest::assert_true; use std::time::Duration; + use std::time::SystemTime; use tokio::sync::oneshot; use tokio::time::{self, timeout}; use triggered::trigger; @@ -1981,7 +1981,7 @@ mod tests { .unwrap(), )); - let clock = Arc::new(SimulationClock::new(1).unwrap()); + let clock = Arc::new(SimulationClock::new(SystemTime::now())); let routing_graph = Arc::new(populate_network_graph(channels, Arc::clone(&clock)).unwrap()); let nodes = ln_node_from_graph(sim_graph, routing_graph, clock) @@ -2098,9 +2098,10 @@ mod tests { async fn test_simulated_node() { // Mock out our network and create a routing graph with 5 hops. let mock = MockNetwork::new(); + let clock = Arc::new(SimulationClock::new(SystemTime::now())); let sim_network = Arc::new(Mutex::new(mock)); let channels = create_simulated_channels(5, 300000000); - let graph = populate_network_graph(channels.clone(), Arc::new(SystemClock {})).unwrap(); + let graph = populate_network_graph(channels.clone(), clock.clone()).unwrap(); // Create a simulated node for the first channel in our network. let pk = channels[0].node_1.policy.pubkey; @@ -2108,7 +2109,7 @@ mod tests { node_info(pk, String::default()), sim_network.clone(), Arc::new(graph), - Arc::new(SystemClock {}), + clock, ) .unwrap(); @@ -2198,7 +2199,7 @@ mod tests { node_info(test_kit.nodes[0], String::default()), Arc::new(Mutex::new(test_kit.graph)), test_kit.routing_graph.clone(), - Arc::new(SystemClock {}), + Arc::new(SimulationClock::new(SystemTime::now())), ) .unwrap(); @@ -2286,7 +2287,11 @@ mod tests { let shutdown_signal = triggered::trigger(); let channels = create_simulated_channels(3, capacity); let routing_graph = Arc::new( - populate_network_graph(channels.clone(), Arc::new(SystemClock {})).unwrap(), + populate_network_graph( + channels.clone(), + Arc::new(SimulationClock::new(SystemTime::now())), + ) + .unwrap(), ); let scorer = Mutex::new(ProbabilisticScorer::new( @@ -2569,7 +2574,7 @@ mod tests { node_info(test_kit.nodes[0], String::default()), Arc::new(Mutex::new(test_kit.graph)), test_kit.routing_graph.clone(), - Arc::new(SystemClock {}), + Arc::new(SimulationClock::new(SystemTime::now())), ) .unwrap(); diff --git a/simln-lib/src/test_utils.rs b/simln-lib/src/test_utils.rs index e00af515..2760ea23 100644 --- a/simln-lib/src/test_utils.rs +++ b/simln-lib/src/test_utils.rs @@ -7,11 +7,12 @@ use mockall::mock; use rand::distributions::Uniform; use rand::Rng; use std::collections::HashMap; +use std::time::SystemTime; use std::{fmt, sync::Arc, time::Duration}; use tokio::sync::Mutex; use tokio_util::task::TaskTracker; -use crate::clock::SystemClock; +use crate::clock::SimulationClock; use crate::{ ActivityDefinition, Graph, LightningError, LightningNode, NodeInfo, PaymentGenerationError, PaymentGenerator, Simulation, SimulationCfg, ValueOrRange, @@ -225,13 +226,13 @@ impl LightningTestNodeBuilder { /// Note: This sets a runtime for the simulation of 0, so run() will exit immediately. pub fn create_simulation( clients: HashMap>>, -) -> Simulation { +) -> Simulation { let (shutdown_trigger, shutdown_listener) = triggered::trigger(); Simulation::new( SimulationCfg::new(Some(0), 0, 0.0, None, None), clients, TaskTracker::new(), - Arc::new(SystemClock {}), + Arc::new(SimulationClock::new(SystemTime::now())), shutdown_trigger, shutdown_listener, )