Skip to content

Commit

Permalink
multi: allow running with simulated or real network
Browse files Browse the repository at this point in the history
  • Loading branch information
carlaKC committed Nov 18, 2024
1 parent 0440f56 commit f867420
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 27 deletions.
98 changes: 78 additions & 20 deletions sim-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -112,48 +113,105 @@ 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.");
sim2.shutdown();
})?;

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<Simulation, anyhow::Error> {
/// 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<Arc<Mutex<SimGraph>>>), 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<NodeInfo, LightningError> {
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<NodeInfo, LightningError> {
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::<Vec<SimulatedChannel>>();

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<NodeInfo, LightningError> {
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
Expand Down
59 changes: 55 additions & 4 deletions simln-lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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<NodeConnection>,
#[serde(default)]
pub sim_network: Vec<NetworkParser>,
#[serde(default)]
pub activity: Vec<ActivityParser>,
}

Expand Down Expand Up @@ -174,6 +180,16 @@ type Amount = ValueOrRange<u64>;
/// The interval of seconds between payments. Either a value or a range.
type Interval = ValueOrRange<u16>;

/// 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)]
Expand Down Expand Up @@ -558,6 +574,41 @@ impl Simulation {
}
}

pub async fn new_with_sim_network(
cfg: SimulationCfg,
channels: Vec<SimulatedChannel>,
activity: Vec<ActivityDefinition>,
) -> Result<(Simulation, Arc<Mutex<SimGraph>>), 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.
Expand Down
19 changes: 16 additions & 3 deletions simln-lib/src/sim_node.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use crate::{
LightningError, LightningNode, NodeInfo, PaymentOutcome, PaymentResult, SimulationError,
LightningError, LightningNode, NetworkParser, NodeInfo, PaymentOutcome, PaymentResult,
SimulationError,
};
use async_trait::async_trait;
use bitcoin::constants::ChainHash;
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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -426,6 +428,17 @@ impl SimulatedChannel {
}
}

impl From<NetworkParser> 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]
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit f867420

Please sign in to comment.