From f8a73599a4d397ae47dfd5738a93404ffc5d77d7 Mon Sep 17 00:00:00 2001 From: elnosh Date: Tue, 23 Sep 2025 08:53:34 -0400 Subject: [PATCH 1/2] simln-lib: repurpose list_channels to channel_capacities Currently, list_channels is only used for returning a list of channel capacities and using that to determine how much the node should send in a month. Change it to channel_capacities that will return the sum of the capacities of the channels that should be taken into account. --- simln-lib/src/cln.rs | 4 ++-- simln-lib/src/eclair.rs | 4 ++-- simln-lib/src/lib.rs | 12 +++++------ simln-lib/src/lnd.rs | 4 ++-- simln-lib/src/sim_node.rs | 41 +++++++++++++++---------------------- simln-lib/src/test_utils.rs | 2 +- 6 files changed, 29 insertions(+), 38 deletions(-) diff --git a/simln-lib/src/cln.rs b/simln-lib/src/cln.rs index a141152f..5bfa1aa0 100644 --- a/simln-lib/src/cln.rs +++ b/simln-lib/src/cln.rs @@ -264,10 +264,10 @@ impl LightningNode for ClnNode { } } - async fn list_channels(&self) -> Result, LightningError> { + async fn channel_capacities(&self) -> Result { let mut node_channels = self.node_channels(true).await?; node_channels.extend(self.node_channels(false).await?); - Ok(node_channels) + Ok(node_channels.iter().sum()) } async fn get_graph(&self) -> Result { diff --git a/simln-lib/src/eclair.rs b/simln-lib/src/eclair.rs index cc0de8ca..bea94575 100644 --- a/simln-lib/src/eclair.rs +++ b/simln-lib/src/eclair.rs @@ -222,7 +222,7 @@ impl LightningNode for EclairNode { }) } - async fn list_channels(&self) -> Result, LightningError> { + async fn channel_capacities(&self) -> Result { let client = self.client.lock().await; let channels: ChannelsResponse = client .request("channels", None) @@ -242,7 +242,7 @@ impl LightningNode for EclairNode { }) .collect(); - Ok(capacities_msat) + Ok(capacities_msat.iter().sum()) } async fn get_graph(&self) -> Result { diff --git a/simln-lib/src/lib.rs b/simln-lib/src/lib.rs index 3cd1a99e..a1b5fe0e 100755 --- a/simln-lib/src/lib.rs +++ b/simln-lib/src/lib.rs @@ -343,9 +343,9 @@ pub trait LightningNode: Send { ) -> Result; /// Gets information on a specific node. async fn get_node_info(&self, node_id: &PublicKey) -> Result; - /// Lists all channels, at present only returns a vector of channel capacities in msat because no further - /// information is required. - async fn list_channels(&self) -> Result, LightningError>; + /// Sum of channel capacities. This is used when running with random activity generation to + /// determine how much the node will send per month. + async fn channel_capacities(&self) -> Result; /// Get the network graph from the point of view of a given node. async fn get_graph(&self) -> Result; } @@ -1003,7 +1003,7 @@ impl Simulation { // While we're at it, we get the node info and store it with capacity to create activity generators in our // second pass. for (pk, node) in self.nodes.iter() { - let chan_capacity = node.lock().await.list_channels().await?.iter().sum::(); + let chan_capacity = node.lock().await.channel_capacities().await?; if let Err(e) = RandomPaymentActivity::validate_capacity( chan_capacity, @@ -1935,8 +1935,8 @@ mod tests { .expect_get_network() .returning(|| Network::Regtest); mock_node - .expect_list_channels() - .returning(|| Ok(vec![100_000_000])); + .expect_channel_capacities() + .returning(|| Ok(100_000_000)); mock_node .expect_get_node_info() .returning(move |_| Ok(node_info.clone())); diff --git a/simln-lib/src/lnd.rs b/simln-lib/src/lnd.rs index 36f31d96..4bbc9470 100644 --- a/simln-lib/src/lnd.rs +++ b/simln-lib/src/lnd.rs @@ -252,7 +252,7 @@ impl LightningNode for LndNode { } } - async fn list_channels(&self) -> Result, LightningError> { + async fn channel_capacities(&self) -> Result { let mut client = self.client.lock().await; let channels = client .lightning() @@ -268,7 +268,7 @@ impl LightningNode for LndNode { .channels .iter() .map(|channel| 1000 * channel.capacity as u64) - .collect()) + .sum()) } async fn get_graph(&self) -> Result { diff --git a/simln-lib/src/sim_node.rs b/simln-lib/src/sim_node.rs index e8262cf1..ff20c378 100755 --- a/simln-lib/src/sim_node.rs +++ b/simln-lib/src/sim_node.rs @@ -797,8 +797,9 @@ impl LightningNode for SimNode { Ok(self.network.lock().await.lookup_node(node_id)?.0) } - async fn list_channels(&self) -> Result, LightningError> { - Ok(self.network.lock().await.lookup_node(&self.info.pubkey)?.1) + async fn channel_capacities(&self) -> Result { + let channels = self.network.lock().await.lookup_node(&self.info.pubkey)?; + Ok(channels.1.iter().sum()) } async fn get_graph(&self) -> Result { @@ -1965,31 +1966,16 @@ mod tests { .await .unwrap(); - let node_1_channels = nodes - .get(&pk1) - .unwrap() - .lock() - .await - .list_channels() - .await - .unwrap(); + let node_1 = nodes.get(&pk1).unwrap().lock().await; + let node_1_capacity = node_1.channel_capacities().await.unwrap(); // Node 1 has 2 channels but one was excluded so here we should only have the one that was // not excluded. - assert!(node_1_channels.len() == 1); - assert!(node_1_channels[0] == capacity_1); - - let node_2_channels = nodes - .get(&pk2) - .unwrap() - .lock() - .await - .list_channels() - .await - .unwrap(); + assert!(node_1_capacity == capacity_1); - assert!(node_2_channels.len() == 1); - assert!(node_2_channels[0] == capacity_1); + let node_2 = nodes.get(&pk2).unwrap().lock().await; + let node_2_capacity = node_2.channel_capacities().await.unwrap(); + assert!(node_2_capacity == capacity_1); // Node 3's only channel was excluded so it won't be present here. assert!(!nodes.contains_key(&pk3)); @@ -2103,12 +2089,17 @@ mod tests { .lock() .await .expect_lookup_node() - .returning(move |_| Ok((node_info(lookup_pk, String::default()), vec![1, 2, 3]))); + .returning(move |_| { + Ok(( + node_info(lookup_pk, String::default()), + vec![10_000, 20_000, 10_000], + )) + }); // Assert that we get three channels from the mock. let node_info = node.get_node_info(&lookup_pk).await.unwrap(); assert_eq!(lookup_pk, node_info.pubkey); - assert_eq!(node.list_channels().await.unwrap().len(), 3); + assert_eq!(node.channel_capacities().await.unwrap(), 40_000); // Next, we're going to test handling of in-flight payments. To do this, we'll mock out calls to our dispatch // function to send different results depending on the destination. diff --git a/simln-lib/src/test_utils.rs b/simln-lib/src/test_utils.rs index 398dd53e..e00af515 100644 --- a/simln-lib/src/test_utils.rs +++ b/simln-lib/src/test_utils.rs @@ -89,7 +89,7 @@ mock! { shutdown: triggered::Listener, ) -> Result; async fn get_node_info(&self, node_id: &PublicKey) -> Result; - async fn list_channels(&self) -> Result, LightningError>; + async fn channel_capacities(&self) -> Result; async fn get_graph(&self) -> Result; } } From bfb24fd34b225f599e58d952bee075d76984cfa0 Mon Sep 17 00:00:00 2001 From: elnosh Date: Tue, 23 Sep 2025 10:04:19 -0400 Subject: [PATCH 2/2] simln-lib/bugfix: return all nodes from ln_node_from_graph 39257da6cc077c34992c7962c4c3909b8200e247 introduced bug where we would not return all `SimNode`s from `ln_node_from_graph` because `SimGraph` did not have the excluded nodes. Here we store all the nodes but exclude them from the capacity based on the exclude field in the `SimulatedChannel`. --- sim-cli/src/parsing.rs | 4 +- simln-lib/src/sim_node.rs | 92 +++++++++++++++++++++++++-------------- 2 files changed, 62 insertions(+), 34 deletions(-) diff --git a/sim-cli/src/parsing.rs b/sim-cli/src/parsing.rs index f0103c49..c5b06b7f 100755 --- a/sim-cli/src/parsing.rs +++ b/sim-cli/src/parsing.rs @@ -301,7 +301,7 @@ pub async fn create_simulation_with_network( )); // 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, + // individual payments without locking the 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, clock.clone()) @@ -312,7 +312,7 @@ pub async fn create_simulation_with_network( // custom actions on the simulated network. For the nodes we'll pass our simulation, cast them // to a dyn trait and exclude any nodes that shouldn't be included in random activity // generation. - let nodes = ln_node_from_graph(simulation_graph.clone(), routing_graph, clock.clone()).await?; + let nodes = ln_node_from_graph(simulation_graph, routing_graph, clock.clone()).await?; let mut nodes_dyn: HashMap<_, Arc>> = nodes .iter() .map(|(pk, node)| (*pk, Arc::clone(node) as Arc>)) diff --git a/simln-lib/src/sim_node.rs b/simln-lib/src/sim_node.rs index ff20c378..801315f1 100755 --- a/simln-lib/src/sim_node.rs +++ b/simln-lib/src/sim_node.rs @@ -477,6 +477,7 @@ impl SimulatedChannel { /// SimNetwork represents a high level network coordinator that is responsible for the task of actually propagating /// payments through the simulated network. +#[async_trait] pub trait SimNetwork: Send + Sync { /// Sends payments over the route provided through the network, reporting the final payment outcome to the sender /// channel provided. @@ -490,7 +491,7 @@ pub trait SimNetwork: Send + Sync { ); /// Looks up a node in the simulated network and a list of its channel capacities. - fn lookup_node(&self, node: &PublicKey) -> Result<(NodeInfo, Vec), LightningError>; + async fn lookup_node(&self, node: &PublicKey) -> Result<(NodeInfo, Vec), LightningError>; /// Lists all nodes in the simulated network. fn list_nodes(&self) -> Vec; } @@ -794,11 +795,16 @@ impl LightningNode for SimNode { } async fn get_node_info(&self, node_id: &PublicKey) -> Result { - Ok(self.network.lock().await.lookup_node(node_id)?.0) + Ok(self.network.lock().await.lookup_node(node_id).await?.0) } async fn channel_capacities(&self) -> Result { - let channels = self.network.lock().await.lookup_node(&self.info.pubkey)?; + let channels = self + .network + .lock() + .await + .lookup_node(&self.info.pubkey) + .await?; Ok(channels.1.iter().sum()) } @@ -1018,9 +1024,9 @@ async fn handle_intercepted_htlc( /// Graph is the top level struct that is used to coordinate simulation of lightning nodes. pub struct SimGraph { - /// nodes caches the list of nodes in the network with a vector of their channel capacities, only used for quick + /// nodes caches the list of nodes in the network with a vector of their channel ids, only used for quick /// lookup. - nodes: HashMap)>, + nodes: HashMap)>, /// channels maps the scid of a channel to its current simulation state. channels: Arc>>, @@ -1052,7 +1058,7 @@ impl SimGraph { default_custom_records: CustomRecords, shutdown_signal: (Trigger, Listener), ) -> Result { - let mut nodes: HashMap)> = HashMap::new(); + let mut nodes: HashMap)> = HashMap::new(); let mut channels = HashMap::new(); for channel in graph_channels.iter() { @@ -1069,18 +1075,16 @@ impl SimGraph { Entry::Vacant(v) => v.insert(channel.clone()), }; - if !channel.exclude_capacity { - // It's okay to have duplicate pubkeys because one node can have many channels. - for info in [&channel.node_1.policy, &channel.node_2.policy] { - match nodes.entry(info.pubkey) { - Entry::Occupied(o) => o.into_mut().1.push(channel.capacity_msat), - Entry::Vacant(v) => { - v.insert(( - node_info(info.pubkey, info.alias.clone()), - vec![channel.capacity_msat], - )); - }, - } + // It's okay to have duplicate pubkeys because one node can have many channels. + for info in [&channel.node_1.policy, &channel.node_2.policy] { + match nodes.entry(info.pubkey) { + Entry::Occupied(o) => o.into_mut().1.push(channel.short_channel_id), + Entry::Vacant(v) => { + v.insert(( + node_info(info.pubkey, info.alias.clone()), + vec![channel.short_channel_id], + )); + }, } } } @@ -1102,9 +1106,11 @@ pub async fn ln_node_from_graph( routing_graph: Arc, clock: Arc, ) -> Result>>>, LightningError> { - let mut nodes: HashMap>>> = HashMap::new(); + let sim_graph = graph.lock().await; + let mut nodes: HashMap>>> = + HashMap::with_capacity(sim_graph.nodes.len()); - for node in graph.lock().await.nodes.iter() { + for node in sim_graph.nodes.iter() { nodes.insert( *node.0, Arc::new(Mutex::new(SimNode::new( @@ -1183,6 +1189,7 @@ pub fn populate_network_graph( Ok(graph) } +#[async_trait] impl SimNetwork for SimGraph { /// dispatch_payment asynchronously propagates a payment through the simulated network, returning a tracking /// channel that can be used to obtain the result of the payment. At present, MPP payments are not supported. @@ -1232,13 +1239,27 @@ impl SimNetwork for SimGraph { } /// lookup_node fetches a node's information and channel capacities. - fn lookup_node(&self, node: &PublicKey) -> Result<(NodeInfo, Vec), LightningError> { - match self.nodes.get(node) { - Some(node) => Ok(node.clone()), - None => Err(LightningError::GetNodeInfoError( - "Node not found".to_string(), - )), - } + async fn lookup_node(&self, node: &PublicKey) -> Result<(NodeInfo, Vec), LightningError> { + let node_info = match self.nodes.get(node) { + Some(node) => node.clone(), + None => { + return Err(LightningError::GetNodeInfoError(format!( + "Node {} not found", + node + ))) + }, + }; + + let channels = self.channels.lock().await; + let capacities: Vec = node_info + .1 + .iter() + .filter_map(|scid| channels.get(scid)) + .filter(|channel| !channel.exclude_capacity) + .map(|channel| channel.capacity_msat) + .collect(); + + Ok((node_info.0, capacities)) } fn list_nodes(&self) -> Vec { @@ -1966,19 +1987,25 @@ mod tests { .await .unwrap(); + assert!(nodes.len() == 3); + let node_1 = nodes.get(&pk1).unwrap().lock().await; let node_1_capacity = node_1.channel_capacities().await.unwrap(); - // Node 1 has 2 channels but one was excluded so here we should only have the one that was - // not excluded. + // Node 1 has 2 channels but one was excluded so here we should only have the capacity of + // the channel that was not excluded. assert!(node_1_capacity == capacity_1); let node_2 = nodes.get(&pk2).unwrap().lock().await; let node_2_capacity = node_2.channel_capacities().await.unwrap(); assert!(node_2_capacity == capacity_1); - // Node 3's only channel was excluded so it won't be present here. - assert!(!nodes.contains_key(&pk3)); + // Node 3 should be returned from ln_node_from_graph but it won't have any channel capacity + // present because its only channel was excluded. + let node_3 = nodes.get(&pk3); + assert!(node_3.is_some()); + let node_3 = node_3.unwrap().lock().await; + assert!(node_3.channel_capacities().await.unwrap() == 0); } /// Tests basic functionality of a `SimulatedChannel` but does no endeavor to test the underlying @@ -2048,6 +2075,7 @@ mod tests { mock! { Network{} + #[async_trait] impl SimNetwork for Network{ fn dispatch_payment( &mut self, @@ -2058,7 +2086,7 @@ mod tests { sender: Sender>, ); - fn lookup_node(&self, node: &PublicKey) -> Result<(NodeInfo, Vec), LightningError>; + async fn lookup_node(&self, node: &PublicKey) -> Result<(NodeInfo, Vec), LightningError>; fn list_nodes(&self) -> Vec; } }