diff --git a/Cargo.lock b/Cargo.lock index b7f74997..66ef765c 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -2576,6 +2576,7 @@ dependencies = [ "simple_logger", "tokio", "tokio-util", + "triggered", ] [[package]] diff --git a/README.md b/README.md index bca2f1e3..949aaf0b 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,106 @@ project. ## Docker If you want to run the cli in a containerized environment, see the docker set up docs [here](./docker/README.md) +## Advanced Usage - Network Simulation + +If you are looking to simulate payments are large lightning networks +without the resource consumption of setting up a large cluster of nodes, +you may be interested in dispatching payments on a simulated network. + +To run on a simulated network, you will need to provide the desired +topology and channel policies for each edge in the graph. The nodes in +the network will be inferred from the edges provided. Simulation of +payments on both a mocked and real lightning network is not supported, +so the `sim_network` field is mutually exclusive with the `nodes` section +described above. + +The example that follows will execute random payments on a network +consisting of three nodes and two channels. You may specify defined +activity to execute on the mocked network, though you must refer to +nodes by their pubkey (aliases are not yet supported). + +``` +{ + "sim_network": [ + { + "scid": 1, + "capacity_msat": 250000, + "node_1": { + "pubkey": "0344f37d544896dcc95a08ddd9bdfc2b756bf3f91b3f65bce588bd9d0228c24977", + "max_htlc_count": 483, + "max_in_flight_msat": 250000, + "min_htlc_size_msat": 1, + "max_htlc_size_msat": 100000, + "cltv_expiry_delta": 40, + "base_fee": 1000, + "fee_rate_prop": 100 + }, + "node_2": { + "pubkey": "020a30431ce58843eedf8051214dbfadb65b107cc598b8277f14bb9b33c9cd026f", + "max_htlc_count": 15, + "max_in_flight_msat": 100000, + "min_htlc_size_msat": 1, + "max_htlc_size_msat": 50000, + "cltv_expiry_delta": 80, + "base_fee": 2000, + "fee_rate_prop": 500 + } + }, + { + "scid": 2, + "capacity_msat": 100000, + "node_1": { + "pubkey": "020a30431ce58843eedf8051214dbfadb65b107cc598b8277f14bb9b33c9cd026f", + "max_htlc_count": 200, + "max_in_flight_msat": 100000, + "min_htlc_size_msat": 1, + "max_htlc_size_msat": 25000, + "cltv_expiry_delta": 40, + "base_fee": 1750, + "fee_rate_prop": 100 + }, + "node_2": { + "pubkey": "035c0b392725bb7298d56bf1bcb23634fc509d86a39a8141d435f9d4d6cd4b12eb", + "max_htlc_count": 15, + "max_in_flight_msat": 50000, + "min_htlc_size_msat": 1, + "max_htlc_size_msat": 50000, + "cltv_expiry_delta": 80, + "base_fee": 3000, + "fee_rate_prop": 5 + } + } + ] +} +``` + +Note that you need to provide forwarding policies in each direction, +because each participant in the channel sets their own forwarding +policy and restrictions on their counterparty. + +### Inclusions and Limitations + +The simulator will execute payments on the mocked out network as it +would for a network of real nodes. See the inclusions and exclusions +listed below for a description of the functionality covered by the +simulated network. + +Included: +* Routing Policy Enforcement: mocked channels enforce fee and CLTV + requirements. +* Channel restrictions: mocked channels abide by the in-flight + count and value limitations set on channel creation. +* Liquidity checks: HTLCs are only forwarded if the node has sufficient + liquidity in the mocked channel. + +Not included: +* Channel reserve: the required minimum reserve balance is not + subtracted from a node's available balance. +* On chain fees: the simulation does not subtract on chain fees from + available liquidity. +* Dust limits: the simulation node not account for restrictions on dust + HTLCs. + ## Developers * [Developer documentation](docs/DEVELOPER.md) diff --git a/sim-cli/Cargo.toml b/sim-cli/Cargo.toml index c45094c0..6e4a9ac4 100755 --- a/sim-cli/Cargo.toml +++ b/sim-cli/Cargo.toml @@ -14,6 +14,7 @@ anyhow = { version = "1.0.69", features = ["backtrace"] } clap = { version = "4.1.6", features = ["derive", "env", "std", "help", "usage", "error-context", "suggestions"], default-features = false } dialoguer = "0.11.0" log = "0.4.20" +triggered = "0.1.2" serde = "1.0.183" serde_json = "1.0.104" simple_logger = "4.2.0" diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index 8ae8477f..60194c57 100755 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -1,7 +1,8 @@ use clap::Parser; use log::LevelFilter; -use sim_cli::parsing::{create_simulation, Cli}; +use sim_cli::parsing::{create_simulation, create_simulation_with_network, parse_sim_params, Cli}; use simple_logger::SimpleLogger; +use tokio_util::task::TaskTracker; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -12,6 +13,7 @@ async fn main() -> anyhow::Result<()> { } let cli = Cli::parse(); + let sim_params = parse_sim_params(&cli).await?; SimpleLogger::new() .with_level(LevelFilter::Warn) @@ -20,7 +22,15 @@ async fn main() -> anyhow::Result<()> { .init() .unwrap(); - let sim = create_simulation(&cli).await?; + 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 { + create_simulation_with_network(&cli, &sim_params, tasks.clone()).await? + }; let sim2 = sim.clone(); ctrlc::set_handler(move || { @@ -28,7 +38,7 @@ async fn main() -> anyhow::Result<()> { sim2.shutdown(); })?; - sim.run().await?; + sim.run(&validated_activities).await?; Ok(()) } diff --git a/sim-cli/src/parsing.rs b/sim-cli/src/parsing.rs index 84696ba7..0758ae43 100755 --- a/sim-cli/src/parsing.rs +++ b/sim-cli/src/parsing.rs @@ -3,11 +3,15 @@ use bitcoin::secp256k1::PublicKey; use clap::{builder::TypedValueParser, Parser}; use log::LevelFilter; use serde::{Deserialize, Serialize}; +use simln_lib::sim_node::{ + ln_node_from_graph, populate_network_graph, ChannelPolicy, SimGraph, SimulatedChannel, +}; use simln_lib::{ cln, cln::ClnNode, eclair, eclair::EclairNode, lnd, lnd::LndNode, serializers, ActivityDefinition, Amount, Interval, LightningError, LightningNode, NodeId, NodeInfo, Simulation, SimulationCfg, WriteResults, }; +use simln_lib::{ShortChannelID, SimulationError}; use std::collections::HashMap; use std::fs; use std::ops::AsyncFn; @@ -81,25 +85,70 @@ pub struct Cli { pub fix_seed: Option, } +impl Cli { + pub fn validate(&self, sim_params: &SimParams) -> Result<(), anyhow::Error> { + // Validate that nodes and sim_graph are exclusively set + if !sim_params.nodes.is_empty() && !sim_params.sim_network.is_empty() { + return Err(anyhow!( + "Simulation file cannot contain {} nodes and {} sim_graph entries, + simulation can only be run with real or simulated nodes not both.", + sim_params.nodes.len(), + sim_params.sim_network.len() + )); + } + if sim_params.nodes.is_empty() && sim_params.sim_network.is_empty() { + return Err(anyhow!( + "Simulation file must contain nodes to run with real lightning + nodes or sim_graph to run with simulated nodes" + )); + } + Ok(()) + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] -struct SimParams { - pub nodes: Vec, +pub struct SimParams { + #[serde(default)] + nodes: Vec, + #[serde(default)] + pub sim_network: Vec, #[serde(default)] pub activity: Vec, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] -enum NodeConnection { +pub enum NodeConnection { Lnd(lnd::LndConnection), Cln(cln::ClnConnection), Eclair(eclair::EclairConnection), } +/// Data structure that is used to parse information from the simulation file. It is used to +/// create a mocked network. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkParser { + pub scid: ShortChannelID, + pub capacity_msat: u64, + pub node_1: ChannelPolicy, + pub node_2: ChannelPolicy, +} + +impl From for SimulatedChannel { + fn from(network_parser: NetworkParser) -> Self { + SimulatedChannel::new( + network_parser.capacity_msat, + network_parser.scid, + network_parser.node_1, + network_parser.node_2, + ) + } +} + /// 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)] -struct ActivityParser { +pub struct ActivityParser { /// The source of the payment. #[serde(with = "serializers::serde_node_id")] pub source: NodeId, @@ -140,42 +189,87 @@ 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 { +struct NodeMapping { + pk_node_map: HashMap, + alias_node_map: HashMap, +} + +pub async fn create_simulation_with_network( + cli: &Cli, + sim_params: &SimParams, + tasks: TaskTracker, +) -> Result<(Simulation, Vec), anyhow::Error> { let cfg: SimulationCfg = SimulationCfg::try_from(cli)?; + let SimParams { + nodes: _, + sim_network, + activity: _activity, + } = sim_params; + + // Convert nodes representation for parsing to SimulatedChannel + let channels = sim_network + .clone() + .into_iter() + .map(SimulatedChannel::from) + .collect::>(); + + let mut nodes_info = HashMap::new(); + for channel in &channels { + let (node_1_info, node_2_info) = channel.create_simulated_nodes(); + nodes_info.insert(node_1_info.pubkey, node_1_info); + nodes_info.insert(node_2_info.pubkey, node_2_info); + } - let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file.clone()).await?; - let SimParams { nodes, activity } = 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: {}).", - e.line(), - e.column(), - e.to_string() - ) - })?; + let (shutdown_trigger, shutdown_listener) = triggered::trigger(); - let (clients, clients_info) = get_clients(nodes).await?; - // We need to be able to look up destination nodes in the graph, because we allow defined activities to send to - // nodes that we do not control. To do this, we can just grab the first node in our map and perform the lookup. - let get_node = async |pk: &PublicKey| -> Result { - if let Some(c) = clients.values().next() { - return c.lock().await.get_node_info(pk).await; - } + // Setup a simulation graph that will handle propagation of payments through the network + let simulation_graph = Arc::new(Mutex::new( + SimGraph::new(channels.clone(), tasks.clone(), shutdown_trigger.clone()) + .map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?, + )); - Err(LightningError::GetNodeInfoError( - "no nodes for query".to_string(), - )) - }; + // 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) + .map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?, + ); + + let nodes = ln_node_from_graph(simulation_graph.clone(), routing_graph).await; + let validated_activities = + get_validated_activities(&nodes, nodes_info, sim_params.activity.clone()).await?; + + Ok(( + Simulation::new(cfg, nodes, tasks, shutdown_trigger, shutdown_listener), + validated_activities, + )) +} + +/// 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, + sim_params: &SimParams, + tasks: TaskTracker, +) -> Result<(Simulation, Vec), anyhow::Error> { + let cfg: SimulationCfg = SimulationCfg::try_from(cli)?; + let SimParams { + nodes, + sim_network: _sim_network, + activity: _activity, + } = sim_params; - let (pk_node_map, alias_node_map) = add_node_to_maps(&clients_info).await?; + let (clients, clients_info) = get_clients(nodes.to_vec()).await?; + let (shutdown_trigger, shutdown_listener) = triggered::trigger(); let validated_activities = - validate_activities(activity, pk_node_map, alias_node_map, get_node).await?; - let tasks = TaskTracker::new(); + get_validated_activities(&clients, clients_info, sim_params.activity.clone()).await?; - Ok(Simulation::new(cfg, clients, validated_activities, tasks)) + Ok(( + Simulation::new(cfg, clients, tasks, shutdown_trigger, shutdown_listener), + validated_activities, + )) } /// Connects to the set of nodes providing, returning a map of node public keys to LightningNode implementations and @@ -213,9 +307,7 @@ async fn get_clients( /// Adds a lightning node to a client map and tracking maps used to lookup node pubkeys and aliases for activity /// validation. -async fn add_node_to_maps( - nodes: &HashMap, -) -> Result<(HashMap, HashMap), LightningError> { +fn add_node_to_maps(nodes: &HashMap) -> Result { let mut pk_node_map = HashMap::new(); let mut alias_node_map = HashMap::new(); @@ -247,7 +339,10 @@ async fn add_node_to_maps( pk_node_map.insert(node_info.pubkey, node_info.clone()); } - Ok((pk_node_map, alias_node_map)) + Ok(NodeMapping { + pk_node_map, + alias_node_map, + }) } /// Validates a set of defined activities, cross-checking aliases and public keys against the set of clients that @@ -362,3 +457,39 @@ fn mkdir(dir: PathBuf) -> anyhow::Result { fs::create_dir_all(&dir)?; 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?; + 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: {}).", + e.line(), + e.column(), + e.to_string() + ) + })?; + Ok(sim_params) +} + +pub async fn get_validated_activities( + clients: &HashMap>>, + nodes_info: HashMap, + activity: Vec, +) -> Result, LightningError> { + // We need to be able to look up destination nodes in the graph, because we allow defined activities to send to + // nodes that we do not control. To do this, we can just grab the first node in our map and perform the lookup. + let get_node = async |pk: &PublicKey| -> Result { + if let Some(c) = clients.values().next() { + return c.lock().await.get_node_info(pk).await; + } + Err(LightningError::GetNodeInfoError( + "no nodes for query".to_string(), + )) + }; + let NodeMapping { + pk_node_map, + alias_node_map, + } = add_node_to_maps(&nodes_info)?; + + validate_activities(activity.to_vec(), pk_node_map, alias_node_map, get_node).await +} diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index 304e5bef..f7e50a5e 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -89,7 +89,7 @@ impl std::fmt::Display for NodeId { } /// Represents a short channel ID, expressed as a struct so that we can implement display for the trait. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)] pub struct ShortChannelID(u64); /// Utility function to easily convert from u64 to `ShortChannelID` @@ -477,8 +477,6 @@ pub struct Simulation { cfg: SimulationCfg, /// The lightning node that is being simulated. nodes: HashMap>>, - /// The activity that are to be executed on the node. - activity: Vec, /// Results logger that holds the simulation statistics. results: Arc>, /// Track all tasks spawned for use in the simulation. When used in the `run` method, it will wait for @@ -511,14 +509,13 @@ impl Simulation { pub fn new( cfg: SimulationCfg, nodes: HashMap>>, - activity: Vec, tasks: TaskTracker, + shutdown_trigger: Trigger, + shutdown_listener: Listener, ) -> Self { - let (shutdown_trigger, shutdown_listener) = triggered::trigger(); Self { cfg, nodes, - activity, results: Arc::new(Mutex::new(PaymentResultLogger::new())), tasks, shutdown_trigger, @@ -529,9 +526,12 @@ impl Simulation { /// validate_activity validates that the user-provided activity description is achievable for the network that /// we're working with. If no activity description is provided, then it ensures that we have configured a network /// that is suitable for random activity generation. - async fn validate_activity(&self) -> Result<(), LightningError> { + async fn validate_activity( + &self, + activity: &[ActivityDefinition], + ) -> Result<(), LightningError> { // For now, empty activity signals random activity generation - if self.activity.is_empty() { + if activity.is_empty() { if self.nodes.len() <= 1 { return Err(LightningError::ValidationError( "At least two nodes required for random activity generation.".to_string(), @@ -549,7 +549,7 @@ impl Simulation { } } - for payment_flow in self.activity.iter() { + for payment_flow in activity.iter() { if payment_flow.amount_msat.value() == 0 { return Err(LightningError::ValidationError( "We do not allow defined activity amount_msat with zero values".to_string(), @@ -615,8 +615,8 @@ impl Simulation { /// run until the simulation completes or we hit an error. /// Note that it will wait for the tasks in self.tasks to complete /// before returning. - pub async fn run(&self) -> Result<(), SimulationError> { - self.internal_run().await?; + pub async fn run(&self, activity: &[ActivityDefinition]) -> Result<(), SimulationError> { + self.internal_run(activity).await?; // Close our TaskTracker and wait for any background tasks // spawned during internal_run to complete. self.tasks.close(); @@ -624,7 +624,7 @@ impl Simulation { Ok(()) } - async fn internal_run(&self) -> Result<(), SimulationError> { + async fn internal_run(&self, activity: &[ActivityDefinition]) -> Result<(), SimulationError> { if let Some(total_time) = self.cfg.total_time { log::info!("Running the simulation for {}s.", total_time.as_secs()); } else { @@ -632,11 +632,11 @@ impl Simulation { } self.validate_node_network().await?; - self.validate_activity().await?; + self.validate_activity(activity).await?; log::info!( "Simulating {} activity on {} nodes.", - self.activity.len(), + activity.len(), self.nodes.len() ); @@ -650,7 +650,7 @@ impl Simulation { self.run_data_collection(event_receiver, &self.tasks); // Get an execution kit per activity that we need to generate and spin up consumers for each source node. - let activities = match self.activity_executors().await { + let activities = match self.activity_executors(activity).await { Ok(a) => a, Err(e) => { // If we encounter an error while setting up the activity_executors, @@ -798,13 +798,16 @@ impl Simulation { log::debug!("Simulator data collection set up."); } - async fn activity_executors(&self) -> Result, SimulationError> { + async fn activity_executors( + &self, + activity: &[ActivityDefinition], + ) -> Result, SimulationError> { let mut generators = Vec::new(); // Note: when we allow configuring both defined and random activity, this will no longer be an if/else, we'll // just populate with each type as configured. - if !self.activity.is_empty() { - for description in self.activity.iter() { + if !activity.is_empty() { + for description in activity.iter() { let activity_generator = DefinedPaymentActivity::new( description.destination.clone(), description @@ -1530,8 +1533,8 @@ mod tests { #[tokio::test] async fn test_validate_activity_empty_with_sufficient_nodes() { let (_, clients) = LightningTestNodeBuilder::new(3).build_full(); - let simulation = test_utils::create_simulation(clients, vec![]); - let result = simulation.validate_activity().await; + let simulation = test_utils::create_simulation(clients); + let result = simulation.validate_activity(&[]).await; assert!(result.is_ok()); } @@ -1540,12 +1543,13 @@ mod tests { #[tokio::test] async fn test_validate_activity_empty_with_insufficient_nodes() { let (_, clients) = LightningTestNodeBuilder::new(1).build_full(); - let simulation = test_utils::create_simulation(clients, vec![]); - let result = simulation.validate_activity().await; + let simulation = test_utils::create_simulation(clients); + let result = simulation.validate_activity(&[]).await; assert!(result.is_err()); - assert!(matches!(result, - Err(LightningError::ValidationError(msg)) if msg.contains("At least two nodes required"))); + assert!( + matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("At least two nodes required")) + ); } /// Verifies that an empty activity fails when one of two nodes doesn’t support keysend, @@ -1555,12 +1559,13 @@ mod tests { let (_, clients) = LightningTestNodeBuilder::new(2) .with_keysend_nodes(vec![0]) .build_full(); - let simulation = test_utils::create_simulation(clients, vec![]); - let result = simulation.validate_activity().await; + let simulation = test_utils::create_simulation(clients); + let result = simulation.validate_activity(&[]).await; assert!(result.is_err()); - assert!(matches!(result, - Err(LightningError::ValidationError(msg)) if msg.contains("must support keysend"))); + assert!( + matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("must support keysend")) + ); } /// Verifies that an activity fails when the source node isn’t in the clients map, @@ -1573,12 +1578,13 @@ mod tests { let dest_node = nodes[0].clone(); let activity = test_utils::create_activity(missing_node, dest_node, 1000); - let simulation = test_utils::create_simulation(clients, vec![activity]); - let result = simulation.validate_activity().await; + let simulation = test_utils::create_simulation(clients); + let result = simulation.validate_activity(&vec![activity]).await; assert!(result.is_err()); - assert!(matches!(result, - Err(LightningError::ValidationError(msg)) if msg.contains("Source node not found"))); + assert!( + matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("Source node not found")) + ); } /// Verifies that an activity fails when the destination lacks keysend support, @@ -1590,12 +1596,13 @@ mod tests { let dest_node = dest_nodes.first().unwrap().0.clone(); let activity = test_utils::create_activity(nodes[0].clone(), dest_node, 1000); - let simulation = test_utils::create_simulation(clients, vec![activity]); - let result = simulation.validate_activity().await; + let simulation = test_utils::create_simulation(clients); + let result = simulation.validate_activity(&vec![activity]).await; assert!(result.is_err()); - assert!(matches!(result, - Err(LightningError::ValidationError(msg)) if msg.contains("does not support keysend"))); + assert!( + matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("does not support keysend")) + ); } /// Verifies that an activity with a non-zero amount between two keysend-enabled nodes @@ -1608,8 +1615,8 @@ mod tests { dest_node.features.set_keysend_optional(); let activity = test_utils::create_activity(nodes[0].clone(), dest_node, 1000); - let simulation = test_utils::create_simulation(clients, vec![activity]); - let result = simulation.validate_activity().await; + let simulation = test_utils::create_simulation(clients); + let result = simulation.validate_activity(&vec![activity]).await; assert!(result.is_ok()); } @@ -1621,12 +1628,13 @@ mod tests { let (nodes, clients) = LightningTestNodeBuilder::new(2).build_full(); let activity = test_utils::create_activity(nodes[0].clone(), nodes[1].clone(), 0); - let simulation = test_utils::create_simulation(clients, vec![activity]); - let result = simulation.validate_activity().await; + let simulation = test_utils::create_simulation(clients); + let result = simulation.validate_activity(&vec![activity]).await; assert!(result.is_err()); - assert!(matches!(result, - Err(LightningError::ValidationError(msg)) if msg.contains("zero values"))); + assert!( + matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("zero values")) + ); } /// Verifies that validation fails with no nodes, expecting a `ValidationError` with @@ -1635,12 +1643,13 @@ mod tests { async fn test_validate_node_network_empty_nodes() { let empty_nodes: HashMap>> = HashMap::new(); - let simulation = test_utils::create_simulation(empty_nodes, vec![]); + let simulation = test_utils::create_simulation(empty_nodes); let result = simulation.validate_node_network().await; assert!(result.is_err()); - assert!(matches!(result, - Err(LightningError::ValidationError(msg)) if msg.contains("we don't control any nodes"))); + assert!( + matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("we don't control any nodes")) + ); } /// Verifies that a node on Bitcoin mainnet fails validation, expecting a `ValidationError` @@ -1651,12 +1660,13 @@ mod tests { .with_networks(vec![Network::Bitcoin]) .build_clients_only(); - let simulation = test_utils::create_simulation(clients, vec![]); + let simulation = test_utils::create_simulation(clients); let result = simulation.validate_node_network().await; assert!(result.is_err()); - assert!(matches!(result, - Err(LightningError::ValidationError(msg)) if msg.contains("mainnet is not supported"))); + assert!( + matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("mainnet is not supported")) + ); } /// Verifies that nodes on Testnet and Regtest fail validation, expecting a @@ -1667,12 +1677,13 @@ mod tests { .with_networks(vec![Network::Testnet, Network::Regtest]) .build_clients_only(); - let simulation = test_utils::create_simulation(clients, vec![]); + let simulation = test_utils::create_simulation(clients); let result = simulation.validate_node_network().await; assert!(result.is_err()); - assert!(matches!(result, - Err(LightningError::ValidationError(msg)) if msg.contains("nodes are not on the same network"))); + assert!( + matches!(result, Err(LightningError::ValidationError(msg)) if msg.contains("nodes are not on the same network")) + ); } /// Verifies that three Testnet nodes pass validation, expecting an `Ok` result. @@ -1682,7 +1693,7 @@ mod tests { .with_networks(vec![Network::Testnet, Network::Testnet, Network::Testnet]) .build_clients_only(); - let simulation = test_utils::create_simulation(clients, vec![]); + let simulation = test_utils::create_simulation(clients); let result = simulation.validate_node_network().await; assert!(result.is_ok()); @@ -1694,7 +1705,7 @@ mod tests { .with_networks(vec![Network::Testnet]) .build_clients_only(); - let simulation = test_utils::create_simulation(clients, vec![]); + let simulation = test_utils::create_simulation(clients); let result = simulation.validate_node_network().await; assert!(result.is_ok()); diff --git a/simln-lib/src/sim_node.rs b/simln-lib/src/sim_node.rs index dde16835..ad8b0720 100755 --- a/simln-lib/src/sim_node.rs +++ b/simln-lib/src/sim_node.rs @@ -6,6 +6,7 @@ use bitcoin::constants::ChainHash; use bitcoin::secp256k1::PublicKey; use bitcoin::{Network, ScriptBuf, TxOut}; use lightning::ln::chan_utils::make_funding_redeemscript; +use serde::{Deserialize, Serialize}; use std::collections::{hash_map::Entry, HashMap}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -109,7 +110,7 @@ struct Htlc { /// Represents one node in the channel's forwarding policy and restrictions. Note that this doesn't directly map to /// a single concept in the protocol, a few things have been combined for the sake of simplicity. Used to manage the /// lightning "state machine" and check that HTLCs are added in accordance of the advertised policy. -#[derive(Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChannelPolicy { pub pubkey: PublicKey, pub max_htlc_count: u64, @@ -423,6 +424,13 @@ impl SimulatedChannel { self.get_node(forwarding_node)? .check_htlc_forward(cltv_delta, amount_msat, fee_msat) } + + pub fn create_simulated_nodes(&self) -> (NodeInfo, NodeInfo) { + ( + node_info(self.node_1.policy.pubkey), + node_info(self.node_2.policy.pubkey), + ) + } } /// SimNetwork represents a high level network coordinator that is responsible for the task of actually propagating diff --git a/simln-lib/src/test_utils.rs b/simln-lib/src/test_utils.rs index 6a754747..33adbc30 100644 --- a/simln-lib/src/test_utils.rs +++ b/simln-lib/src/test_utils.rs @@ -195,15 +195,14 @@ impl LightningTestNodeBuilder { /// Creates a new simulation with the given clients and activity definitions. /// Note: This sets a runtime for the simulation of 0, so run() will exit immediately. -pub fn create_simulation( - clients: HashMap>>, - activity: Vec, -) -> Simulation { +pub fn create_simulation(clients: HashMap>>) -> Simulation { + let (shutdown_trigger, shutdown_listener) = triggered::trigger(); Simulation::new( SimulationCfg::new(Some(0), 0, 0.0, None, None), clients, - activity, TaskTracker::new(), + shutdown_trigger, + shutdown_listener, ) } pub fn create_activity(