From f86742098a651f7496f836506e39c533f3c05478 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Fri, 22 Mar 2024 15:05:10 -0400 Subject: [PATCH] multi: allow running with simulated or real network --- sim-cli/src/main.rs | 98 +++++++++++++++++++++++++++++++-------- simln-lib/src/lib.rs | 59 +++++++++++++++++++++-- simln-lib/src/sim_node.rs | 19 ++++++-- 3 files changed, 149 insertions(+), 27 deletions(-) diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index 4dcb0392..3044623c 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -9,6 +9,7 @@ use anyhow::anyhow; use clap::builder::TypedValueParser; use clap::Parser; use log::LevelFilter; +use simln_lib::sim_node::{node_info, SimGraph, SimulatedChannel}; use simln_lib::{ cln::ClnNode, lnd::LndNode, ActivityDefinition, ActivityParser, LightningError, LightningNode, NodeConnection, NodeId, NodeInfo, SimParams, Simulation, SimulationCfg, WriteResults, @@ -112,8 +113,7 @@ async fn main() -> anyhow::Result<()> { cli.fix_seed, ); - let sim = create_simulation(&cli, sim_cfg).await?; - + let (sim, sim_network) = create_simulation(&cli, sim_cfg).await?; let sim2 = sim.clone(); ctrlc::set_handler(move || { log::info!("Shutting down simulation."); @@ -121,39 +121,97 @@ async fn main() -> anyhow::Result<()> { })?; sim.run().await?; + + if let Some(network) = sim_network { + network.lock().await.wait_for_shutdown().await; + } + Ok(()) } /// 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. -async fn create_simulation(cli: &Cli, cfg: SimulationCfg) -> Result { +/// any activity described in the simulation file. If the simulation is also running in simulated network mode, it +/// will return the simulated network graph as well. +async fn create_simulation( + cli: &Cli, + cfg: SimulationCfg, +) -> Result<(Simulation, Option>>), anyhow::Error> { 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)?) + let SimParams { nodes, sim_network, 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 {}).", + "Could not deserialize node connection data or activity description from simulation file (line {}, col {}, err: {}).", e.line(), - e.column() + e.column(), + e.to_string() ) })?; - let (clients, clients_info) = connect_nodes(nodes).await?; + // Validate that nodes and sim_graph are exclusively set, and setup node clients from the populated field. + if !nodes.is_empty() && !sim_network.is_empty() { + Err(anyhow!( + "Simulation file cannot contain {} nodes and {} sim_graph entries, simulation can only be run with real + or simulated nodes not both.", nodes.len(), sim_network.len(), + )) + } else if nodes.is_empty() && sim_network.is_empty() { + Err(anyhow!( + "Simulation file must contain nodes to run with real lightning nodes or sim_graph to run with + simulated nodes", + )) + } else if !nodes.is_empty() { + let (clients, clients_info) = connect_nodes(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. + // block_on is used to avoid asynchronous closures, which is okay because we expect these lookups to be very cheap. + let get_node = |pk: &PublicKey| -> Result { + if let Some(c) = clients.values().next() { + return block_on(async { c.lock().await.get_node_info(pk).await }); + } + + Err(LightningError::GetNodeInfoError( + "no nodes for query".to_string(), + )) + }; - // 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. - // block_on is used to avoid asynchronous closures, which is okay because we expect these lookups to be very cheap. - let get_node = |pk: &PublicKey| -> Result { - if let Some(c) = clients.values().next() { - return block_on(async { c.lock().await.get_node_info(pk).await }); + let validated_activities = validate_activities(activity, &clients_info, get_node).await?; + Ok((Simulation::new(cfg, clients, validated_activities), None)) + } else { + // 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 sim_channel in sim_network { + nodes_info.insert( + sim_channel.node_1.pubkey, + node_info(sim_channel.node_1.pubkey), + ); + nodes_info.insert( + sim_channel.node_2.pubkey, + node_info(sim_channel.node_2.pubkey), + ); } - Err(LightningError::GetNodeInfoError( - "no nodes for query".to_string(), - )) - }; - let validated_activities = validate_activities(activity, &clients_info, get_node).await?; + let get_node = |pk: &PublicKey| -> Result { + if let Some(node) = nodes_info.get(pk) { + Ok(node_info(node.pubkey)) + } else { + Err(LightningError::GetNodeInfoError(format!( + "node not found in simulated network: {}", + pk + ))) + } + }; - Ok(Simulation::new(cfg, clients, validated_activities)) + let validated_activities = validate_activities(activity, &nodes_info, get_node).await?; + + let (simulation, graph) = + Simulation::new_with_sim_network(cfg, channels, validated_activities).await?; + Ok((simulation, Some(graph))) + } } /// Connects to the set of nodes providing, returning a map of node public keys to LightningNode implementations and diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index 3cccf8c2..cefae5d1 100644 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -1,3 +1,8 @@ +use self::defined_activity::DefinedPaymentActivity; +use self::random_activity::{NetworkGraphView, RandomPaymentActivity}; +use self::sim_node::{ + ln_node_from_graph, populate_network_graph, ChannelPolicy, SimGraph, SimulatedChannel, +}; use async_trait::async_trait; use bitcoin::secp256k1::PublicKey; use bitcoin::Network; @@ -23,9 +28,6 @@ use tokio::task::JoinSet; use tokio::{select, time, time::Duration}; use triggered::{Listener, Trigger}; -use self::defined_activity::DefinedPaymentActivity; -use self::random_activity::{NetworkGraphView, RandomPaymentActivity}; - pub mod cln; mod defined_activity; pub mod lnd; @@ -96,7 +98,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` @@ -126,10 +128,14 @@ impl std::fmt::Display for ShortChannelID { } } +/// Parameters for the simulation provided in our simulation file. Serde default #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SimParams { + #[serde(default)] pub nodes: Vec, #[serde(default)] + pub sim_network: Vec, + #[serde(default)] pub activity: Vec, } @@ -174,6 +180,16 @@ type Amount = ValueOrRange; /// The interval of seconds between payments. Either a value or a range. type Interval = ValueOrRange; +/// Data structure that is used to parse information from the simulation file, used to pair two node policies together +/// without the other internal state that is used in our simulated network. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkParser { + pub scid: ShortChannelID, + pub capacity_msat: u64, + pub node_1: ChannelPolicy, + pub node_2: ChannelPolicy, +} + /// 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)] @@ -558,6 +574,41 @@ impl Simulation { } } + pub async fn new_with_sim_network( + cfg: SimulationCfg, + channels: Vec, + activity: Vec, + ) -> Result<(Simulation, Arc>), SimulationError> { + let (shutdown_trigger, shutdown_listener) = triggered::trigger(); + + // Setup a simulation graph that will handle propagation of payments through the network. + let simulation_graph = Arc::new(Mutex::new( + SimGraph::new(channels.clone(), shutdown_trigger.clone()) + .map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?, + )); + + // Copy all of our simulated channels into a read-only routing graph, allowing us to pathfind for individual + // payments without locking the simulation graph (this is a duplication of our 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; + + Ok(( + Self { + nodes, + activity, + shutdown_trigger, + shutdown_listener, + cfg, + }, + simulation_graph, + )) + } + /// 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. diff --git a/simln-lib/src/sim_node.rs b/simln-lib/src/sim_node.rs index 9c560bd3..a0bc6971 100644 --- a/simln-lib/src/sim_node.rs +++ b/simln-lib/src/sim_node.rs @@ -1,5 +1,6 @@ use crate::{ - LightningError, LightningNode, NodeInfo, PaymentOutcome, PaymentResult, SimulationError, + LightningError, LightningNode, NetworkParser, NodeInfo, PaymentOutcome, PaymentResult, + SimulationError, }; use async_trait::async_trait; use bitcoin::constants::ChainHash; @@ -7,6 +8,7 @@ use bitcoin::hashes::{sha256::Hash as Sha256, Hash}; 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}; @@ -110,7 +112,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, @@ -426,6 +428,17 @@ impl SimulatedChannel { } } +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, + ) + } +} + /// SimNetwork represents a high level network coordinator that is responsible for the task of actually propagating /// payments through the simulated network. #[async_trait] @@ -476,7 +489,7 @@ impl<'a, T: SimNetwork> SimNode<'a, T> { } /// Produces the node info for a mocked node, filling in the features that the simulator requires. -fn node_info(pubkey: PublicKey) -> NodeInfo { +pub fn node_info(pubkey: PublicKey) -> NodeInfo { // Set any features that the simulator requires here. let mut features = NodeFeatures::empty(); features.set_keysend_optional();