Skip to content

Commit 0c91cf7

Browse files
committed
simln-lib/feature: Add ability to run with mocked ln network
1 parent 7f5c6b3 commit 0c91cf7

File tree

8 files changed

+258
-22
lines changed

8 files changed

+258
-22
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+100
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,106 @@ project.
270270
## Docker
271271
If you want to run the cli in a containerized environment, see the docker set up docs [here](./docker/README.md)
272272

273+
## Advanced Usage - Network Simulation
274+
275+
If you are looking to simulate payments are large lightning networks
276+
without the resource consumption of setting up a large cluster of nodes,
277+
you may be interested in dispatching payments on a simulated network.
278+
279+
To run on a simulated network, you will need to provide the desired
280+
topology and channel policies for each edge in the graph. The nodes in
281+
the network will be inferred from the edges provided. Simulation of
282+
payments on both a mocked and real lightning network is not supported,
283+
so the `sim_network` field is mutually exclusive with the `nodes` section
284+
described above.
285+
286+
The example that follows will execute random payments on a network
287+
consisting of three nodes and two channels. You may specify defined
288+
activity to execute on the mocked network, though you must refer to
289+
nodes by their pubkey (aliases are not yet supported).
290+
291+
```
292+
{
293+
"sim_network": [
294+
{
295+
"scid": 1,
296+
"capacity_msat": 250000,
297+
"node_1": {
298+
"pubkey": "0344f37d544896dcc95a08ddd9bdfc2b756bf3f91b3f65bce588bd9d0228c24977",
299+
"max_htlc_count": 483,
300+
"max_in_flight_msat": 250000,
301+
"min_htlc_size_msat": 1,
302+
"max_htlc_size_msat": 100000,
303+
"cltv_expiry_delta": 40,
304+
"base_fee": 1000,
305+
"fee_rate_prop": 100
306+
},
307+
"node_2": {
308+
"pubkey": "020a30431ce58843eedf8051214dbfadb65b107cc598b8277f14bb9b33c9cd026f",
309+
"max_htlc_count": 15,
310+
"max_in_flight_msat": 100000,
311+
"min_htlc_size_msat": 1,
312+
"max_htlc_size_msat": 50000,
313+
"cltv_expiry_delta": 80,
314+
"base_fee": 2000,
315+
"fee_rate_prop": 500
316+
}
317+
},
318+
{
319+
"scid": 2,
320+
"capacity_msat": 100000,
321+
"node_1": {
322+
"pubkey": "020a30431ce58843eedf8051214dbfadb65b107cc598b8277f14bb9b33c9cd026f",
323+
"max_htlc_count": 200,
324+
"max_in_flight_msat": 100000,
325+
"min_htlc_size_msat": 1,
326+
"max_htlc_size_msat": 25000,
327+
"cltv_expiry_delta": 40,
328+
"base_fee": 1750,
329+
"fee_rate_prop": 100
330+
},
331+
"node_2": {
332+
"pubkey": "035c0b392725bb7298d56bf1bcb23634fc509d86a39a8141d435f9d4d6cd4b12eb",
333+
"max_htlc_count": 15,
334+
"max_in_flight_msat": 50000,
335+
"min_htlc_size_msat": 1,
336+
"max_htlc_size_msat": 50000,
337+
"cltv_expiry_delta": 80,
338+
"base_fee": 3000,
339+
"fee_rate_prop": 5
340+
}
341+
}
342+
]
343+
}
344+
```
345+
346+
Note that you need to provide forwarding policies in each direction,
347+
because each participant in the channel sets their own forwarding
348+
policy and restrictions on their counterparty.
349+
350+
### Inclusions and Limitations
351+
352+
The simulator will execute payments on the mocked out network as it
353+
would for a network of real nodes. See the inclusions and exclusions
354+
listed below for a description of the functionality covered by the
355+
simulated network.
356+
357+
Included:
358+
* Routing Policy Enforcement: mocked channels enforce fee and CLTV
359+
requirements.
360+
* Channel restrictions: mocked channels abide by the in-flight
361+
count and value limitations set on channel creation.
362+
* Liquidity checks: HTLCs are only forwarded if the node has sufficient
363+
liquidity in the mocked channel.
364+
365+
Not included:
366+
* Channel reserve: the required minimum reserve balance is not
367+
subtracted from a node's available balance.
368+
* On chain fees: the simulation does not subtract on chain fees from
369+
available liquidity.
370+
* Dust limits: the simulation node not account for restrictions on dust
371+
HTLCs.
372+
273373
## Developers
274374

275375
* [Developer documentation](docs/DEVELOPER.md)

sim-cli/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ anyhow = { version = "1.0.69", features = ["backtrace"] }
1414
clap = { version = "4.1.6", features = ["derive", "env", "std", "help", "usage", "error-context", "suggestions"], default-features = false }
1515
dialoguer = "0.11.0"
1616
log = "0.4.20"
17+
triggered = "0.1.2"
1718
serde = "1.0.183"
1819
serde_json = "1.0.104"
1920
simple_logger = "4.2.0"

sim-cli/src/main.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use clap::Parser;
22
use log::LevelFilter;
3-
use sim_cli::parsing::{create_simulation, get_validated_activities, parse_sim_params, Cli};
3+
use sim_cli::parsing::{create_simulation, create_simulation_with_network, parse_sim_params, Cli};
44
use simple_logger::SimpleLogger;
5+
use tokio_util::task::TaskTracker;
56

67
#[tokio::main]
78
async fn main() -> anyhow::Result<()> {
@@ -21,17 +22,22 @@ async fn main() -> anyhow::Result<()> {
2122
.init()
2223
.unwrap();
2324

24-
let (sim, nodes_info) = create_simulation(&cli, &sim_params).await?;
25+
cli.validate(&sim_params)?;
26+
27+
let tasks = TaskTracker::new();
28+
29+
let (sim, validated_activities) = if sim_params.sim_network.is_empty() {
30+
create_simulation(&cli, &sim_params, tasks.clone()).await?
31+
} else {
32+
create_simulation_with_network(&cli, &sim_params, tasks.clone()).await?
33+
};
2534
let sim2 = sim.clone();
2635

2736
ctrlc::set_handler(move || {
2837
log::info!("Shutting down simulation.");
2938
sim2.shutdown();
3039
})?;
3140

32-
let validated_activities =
33-
get_validated_activities(&sim.nodes, nodes_info, sim_params.activity).await?;
34-
3541
sim.run(&validated_activities).await?;
3642

3743
Ok(())

sim-cli/src/parsing.rs

+129-13
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ use bitcoin::secp256k1::PublicKey;
33
use clap::{builder::TypedValueParser, Parser};
44
use log::LevelFilter;
55
use serde::{Deserialize, Serialize};
6+
use simln_lib::sim_node::{
7+
ln_node_from_graph, populate_network_graph, ChannelPolicy, SimGraph, SimulatedChannel,
8+
};
69
use simln_lib::{
710
cln, cln::ClnNode, eclair, eclair::EclairNode, lnd, lnd::LndNode, serializers,
811
ActivityDefinition, Amount, Interval, LightningError, LightningNode, NodeId, NodeInfo,
912
Simulation, SimulationCfg, WriteResults,
1013
};
14+
use simln_lib::{ShortChannelID, SimulationError};
1115
use std::collections::HashMap;
1216
use std::fs;
1317
use std::ops::AsyncFn;
@@ -81,9 +85,33 @@ pub struct Cli {
8185
pub fix_seed: Option<u64>,
8286
}
8387

88+
impl Cli {
89+
pub fn validate(&self, sim_params: &SimParams) -> Result<(), anyhow::Error> {
90+
// Validate that nodes and sim_graph are exclusively set
91+
if !sim_params.nodes.is_empty() && !sim_params.sim_network.is_empty() {
92+
return Err(anyhow!(
93+
"Simulation file cannot contain {} nodes and {} sim_graph entries,
94+
simulation can only be run with real or simulated nodes not both.",
95+
sim_params.nodes.len(),
96+
sim_params.sim_network.len()
97+
));
98+
}
99+
if sim_params.nodes.is_empty() && sim_params.sim_network.is_empty() {
100+
return Err(anyhow!(
101+
"Simulation file must contain nodes to run with real lightning
102+
nodes or sim_graph to run with simulated nodes"
103+
));
104+
}
105+
Ok(())
106+
}
107+
}
108+
84109
#[derive(Debug, Serialize, Deserialize, Clone)]
85110
pub struct SimParams {
86-
pub nodes: Vec<NodeConnection>,
111+
#[serde(default)]
112+
nodes: Vec<NodeConnection>,
113+
#[serde(default)]
114+
pub sim_network: Vec<NetworkParser>,
87115
#[serde(default)]
88116
pub activity: Vec<ActivityParser>,
89117
}
@@ -96,6 +124,27 @@ pub enum NodeConnection {
96124
Eclair(eclair::EclairConnection),
97125
}
98126

127+
/// Data structure that is used to parse information from the simulation file. It is used to
128+
/// create a mocked network.
129+
#[derive(Debug, Clone, Serialize, Deserialize)]
130+
pub struct NetworkParser {
131+
pub scid: ShortChannelID,
132+
pub capacity_msat: u64,
133+
pub node_1: ChannelPolicy,
134+
pub node_2: ChannelPolicy,
135+
}
136+
137+
impl From<NetworkParser> for SimulatedChannel {
138+
fn from(network_parser: NetworkParser) -> Self {
139+
SimulatedChannel::new(
140+
network_parser.capacity_msat,
141+
network_parser.scid,
142+
network_parser.node_1,
143+
network_parser.node_2,
144+
)
145+
}
146+
}
147+
99148
/// Data structure used to parse information from the simulation file. It allows source and destination to be
100149
/// [NodeId], which enables the use of public keys and aliases in the simulation description.
101150
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -140,23 +189,87 @@ impl TryFrom<&Cli> for SimulationCfg {
140189
}
141190
}
142191

192+
struct NodeMapping {
193+
pk_node_map: HashMap<PublicKey, NodeInfo>,
194+
alias_node_map: HashMap<String, NodeInfo>,
195+
}
196+
197+
pub async fn create_simulation_with_network(
198+
cli: &Cli,
199+
sim_params: &SimParams,
200+
tasks: TaskTracker,
201+
) -> Result<(Simulation, Vec<ActivityDefinition>), anyhow::Error> {
202+
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
203+
let SimParams {
204+
nodes: _,
205+
sim_network,
206+
activity: _activity,
207+
} = sim_params;
208+
209+
// Convert nodes representation for parsing to SimulatedChannel
210+
let channels = sim_network
211+
.clone()
212+
.into_iter()
213+
.map(SimulatedChannel::from)
214+
.collect::<Vec<SimulatedChannel>>();
215+
216+
let mut nodes_info = HashMap::new();
217+
for channel in &channels {
218+
let (node_1_info, node_2_info) = channel.create_simulated_nodes();
219+
nodes_info.insert(node_1_info.pubkey, node_1_info);
220+
nodes_info.insert(node_2_info.pubkey, node_2_info);
221+
}
222+
223+
let (shutdown_trigger, shutdown_listener) = triggered::trigger();
224+
225+
// Setup a simulation graph that will handle propagation of payments through the network
226+
let simulation_graph = Arc::new(Mutex::new(
227+
SimGraph::new(channels.clone(), tasks.clone(), shutdown_trigger.clone())
228+
.map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?,
229+
));
230+
231+
// Copy all simulated channels into a read-only routing graph, allowing to pathfind for
232+
// individual payments without locking th simulation graph (this is a duplication of the channels,
233+
// but the performance tradeoff is worthwhile for concurrent pathfinding).
234+
let routing_graph = Arc::new(
235+
populate_network_graph(channels)
236+
.map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?,
237+
);
238+
239+
let nodes = ln_node_from_graph(simulation_graph.clone(), routing_graph).await;
240+
let validated_activities =
241+
get_validated_activities(&nodes, nodes_info, sim_params.activity.clone()).await?;
242+
243+
Ok((
244+
Simulation::new(cfg, nodes, tasks, shutdown_trigger, shutdown_listener),
245+
validated_activities,
246+
))
247+
}
248+
143249
/// Parses the cli options provided and creates a simulation to be run, connecting to lightning nodes and validating
144250
/// any activity described in the simulation file.
145251
pub async fn create_simulation(
146252
cli: &Cli,
147253
sim_params: &SimParams,
148-
) -> Result<(Simulation, HashMap<PublicKey, NodeInfo>), anyhow::Error> {
254+
tasks: TaskTracker,
255+
) -> Result<(Simulation, Vec<ActivityDefinition>), anyhow::Error> {
149256
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
150-
151257
let SimParams {
152258
nodes,
259+
sim_network: _sim_network,
153260
activity: _activity,
154261
} = sim_params;
155262

156263
let (clients, clients_info) = get_clients(nodes.to_vec()).await?;
157-
let tasks = TaskTracker::new();
264+
let (shutdown_trigger, shutdown_listener) = triggered::trigger();
158265

159-
Ok((Simulation::new(cfg, clients, tasks), clients_info))
266+
let validated_activities =
267+
get_validated_activities(&clients, clients_info, sim_params.activity.clone()).await?;
268+
269+
Ok((
270+
Simulation::new(cfg, clients, tasks, shutdown_trigger, shutdown_listener),
271+
validated_activities,
272+
))
160273
}
161274

162275
/// Connects to the set of nodes providing, returning a map of node public keys to LightningNode implementations and
@@ -194,9 +307,7 @@ async fn get_clients(
194307

195308
/// Adds a lightning node to a client map and tracking maps used to lookup node pubkeys and aliases for activity
196309
/// validation.
197-
async fn add_node_to_maps(
198-
nodes: &HashMap<PublicKey, NodeInfo>,
199-
) -> Result<(HashMap<PublicKey, NodeInfo>, HashMap<String, NodeInfo>), LightningError> {
310+
fn add_node_to_maps(nodes: &HashMap<PublicKey, NodeInfo>) -> Result<NodeMapping, LightningError> {
200311
let mut pk_node_map = HashMap::new();
201312
let mut alias_node_map = HashMap::new();
202313

@@ -228,7 +339,10 @@ async fn add_node_to_maps(
228339
pk_node_map.insert(node_info.pubkey, node_info.clone());
229340
}
230341

231-
Ok((pk_node_map, alias_node_map))
342+
Ok(NodeMapping {
343+
pk_node_map,
344+
alias_node_map,
345+
})
232346
}
233347

234348
/// Validates a set of defined activities, cross-checking aliases and public keys against the set of clients that
@@ -372,8 +486,10 @@ pub async fn get_validated_activities(
372486
"no nodes for query".to_string(),
373487
))
374488
};
375-
let (pk_node_map, alias_node_map) = add_node_to_maps(&nodes_info).await?;
376-
let validated_activities =
377-
validate_activities(activity.to_vec(), pk_node_map, alias_node_map, get_node).await?;
378-
Ok(validated_activities)
489+
let NodeMapping {
490+
pk_node_map,
491+
alias_node_map,
492+
} = add_node_to_maps(&nodes_info)?;
493+
494+
validate_activities(activity.to_vec(), pk_node_map, alias_node_map, get_node).await
379495
}

simln-lib/src/lib.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ impl std::fmt::Display for NodeId {
8989
}
9090

9191
/// Represents a short channel ID, expressed as a struct so that we can implement display for the trait.
92-
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
92+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)]
9393
pub struct ShortChannelID(u64);
9494

9595
/// Utility function to easily convert from u64 to `ShortChannelID`
@@ -476,7 +476,7 @@ pub struct Simulation {
476476
/// Config for the simulation itself.
477477
cfg: SimulationCfg,
478478
/// The lightning node that is being simulated.
479-
pub nodes: HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>>,
479+
nodes: HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>>,
480480
/// Results logger that holds the simulation statistics.
481481
results: Arc<Mutex<PaymentResultLogger>>,
482482
/// Track all tasks spawned for use in the simulation. When used in the `run` method, it will wait for
@@ -510,8 +510,9 @@ impl Simulation {
510510
cfg: SimulationCfg,
511511
nodes: HashMap<PublicKey, Arc<Mutex<dyn LightningNode>>>,
512512
tasks: TaskTracker,
513+
shutdown_trigger: Trigger,
514+
shutdown_listener: Listener,
513515
) -> Self {
514-
let (shutdown_trigger, shutdown_listener) = triggered::trigger();
515516
Self {
516517
cfg,
517518
nodes,

0 commit comments

Comments
 (0)