Skip to content

Commit 9f3a4f2

Browse files
committed
sim-ln/refactor: Improve activity dest node lookup via Graph
Switches from an async closure to direct NetworkGraph usage for activity destination node lookup. This improves efficiency and maintainability, and enables destination aliases by validating against the graph and erroring on duplicates.
1 parent b8a563c commit 9f3a4f2

File tree

7 files changed

+233
-25
lines changed

7 files changed

+233
-25
lines changed

sim-cli/src/parsing.rs

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ use simln_lib::{
1515
use simln_lib::{ShortChannelID, SimulationError};
1616
use std::collections::HashMap;
1717
use std::fs;
18-
use std::ops::AsyncFn;
1918
use std::path::PathBuf;
2019
use std::sync::Arc;
2120
use tokio::sync::Mutex;
@@ -179,6 +178,15 @@ pub struct ActivityParser {
179178
pub amount_msat: Amount,
180179
}
181180

181+
struct ActivityValidationParams {
182+
pk_node_map: HashMap<PublicKey, NodeInfo>,
183+
alias_node_map: HashMap<String, NodeInfo>,
184+
graph_nodes_by_pk: HashMap<PublicKey, NodeInfo>,
185+
// Store graph nodes' information keyed by their alias.
186+
// An alias can be mapped to multiple nodes because it is not a unique identifier.
187+
graph_nodes_by_alias: HashMap<String, Vec<NodeInfo>>,
188+
}
189+
182190
impl TryFrom<&Cli> for SimulationCfg {
183191
type Error = anyhow::Error;
184192

@@ -378,15 +386,20 @@ fn add_node_to_maps(nodes: &HashMap<PublicKey, NodeInfo>) -> Result<NodeMapping,
378386
/// have been configured.
379387
async fn validate_activities(
380388
activity: Vec<ActivityParser>,
381-
pk_node_map: HashMap<PublicKey, NodeInfo>,
382-
alias_node_map: HashMap<String, NodeInfo>,
383-
get_node_info: impl AsyncFn(&PublicKey) -> Result<NodeInfo, LightningError>,
389+
activity_validation_params: ActivityValidationParams,
384390
) -> Result<Vec<ActivityDefinition>, LightningError> {
385391
let mut validated_activities = vec![];
386392

393+
let ActivityValidationParams {
394+
pk_node_map,
395+
alias_node_map,
396+
graph_nodes_by_pk,
397+
graph_nodes_by_alias,
398+
} = activity_validation_params;
399+
387400
// Make all the activities identifiable by PK internally
388401
for act in activity.into_iter() {
389-
// We can only map aliases to nodes we control, so if either the source or destination alias
402+
// We can only map source aliases to nodes we control, so if the source alias
390403
// is not in alias_node_map, we fail
391404
let source = if let Some(source) = match &act.source {
392405
NodeId::PublicKey(pk) => pk_node_map.get(pk),
@@ -402,8 +415,21 @@ async fn validate_activities(
402415

403416
let destination = match &act.destination {
404417
NodeId::Alias(a) => {
405-
if let Some(info) = alias_node_map.get(a) {
406-
info.clone()
418+
if let Some(node_info) = alias_node_map.get(a) {
419+
node_info.clone()
420+
} else if let Some(node_infos) = graph_nodes_by_alias.get(a) {
421+
if node_infos.len() > 1 {
422+
let pks: Vec<PublicKey> = node_infos
423+
.iter()
424+
.map(|node_info| node_info.pubkey)
425+
.collect();
426+
return Err(LightningError::ValidationError(format!(
427+
"Multiple nodes in the graph have the same destination alias - {}.
428+
Use one of these public keys as the destination instead - {:?}",
429+
a, pks
430+
)));
431+
}
432+
node_infos[0].clone()
407433
} else {
408434
return Err(LightningError::ValidationError(format!(
409435
"unknown activity destination: {}.",
@@ -412,10 +438,15 @@ async fn validate_activities(
412438
}
413439
},
414440
NodeId::PublicKey(pk) => {
415-
if let Some(info) = pk_node_map.get(pk) {
416-
info.clone()
441+
if let Some(node_info) = pk_node_map.get(pk) {
442+
node_info.clone()
443+
} else if let Some(node_info) = graph_nodes_by_pk.get(pk) {
444+
node_info.clone()
417445
} else {
418-
get_node_info(pk).await?
446+
return Err(LightningError::ValidationError(format!(
447+
"unknown activity destination: {}.",
448+
act.destination
449+
)));
419450
}
420451
},
421452
};
@@ -507,18 +538,35 @@ pub async fn get_validated_activities(
507538
) -> Result<Vec<ActivityDefinition>, LightningError> {
508539
// We need to be able to look up destination nodes in the graph, because we allow defined activities to send to
509540
// nodes that we do not control. To do this, we can just grab the first node in our map and perform the lookup.
510-
let get_node = async |pk: &PublicKey| -> Result<NodeInfo, LightningError> {
511-
if let Some(c) = clients.values().next() {
512-
return c.lock().await.get_node_info(pk).await;
513-
}
514-
Err(LightningError::GetNodeInfoError(
515-
"no nodes for query".to_string(),
516-
))
517-
};
541+
let graph = match clients.values().next() {
542+
Some(client) => client
543+
.lock()
544+
.await
545+
.get_graph()
546+
.await
547+
.map_err(|e| LightningError::GetGraphError(format!("Error getting graph {:?}", e))),
548+
None => Err(LightningError::GetGraphError("Graph is empty".to_string())),
549+
}?;
550+
let mut graph_nodes_by_alias: HashMap<String, Vec<NodeInfo>> = HashMap::new();
551+
552+
for node in &graph.nodes_by_pk {
553+
graph_nodes_by_alias
554+
.entry(node.1.alias.clone())
555+
.or_default()
556+
.push(node.1.clone());
557+
}
558+
518559
let NodeMapping {
519560
pk_node_map,
520561
alias_node_map,
521562
} = add_node_to_maps(&nodes_info)?;
522563

523-
validate_activities(activity.to_vec(), pk_node_map, alias_node_map, get_node).await
564+
let activity_validation_params = ActivityValidationParams {
565+
pk_node_map,
566+
alias_node_map,
567+
graph_nodes_by_pk: graph.nodes_by_pk,
568+
graph_nodes_by_alias,
569+
};
570+
571+
validate_activities(activity.to_vec(), activity_validation_params).await
524572
}

simln-lib/src/cln.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashMap;
2+
13
use async_trait::async_trait;
24
use bitcoin::secp256k1::PublicKey;
35
use bitcoin::Network;
@@ -17,7 +19,8 @@ use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
1719
use triggered::Listener;
1820

1921
use crate::{
20-
serializers, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome, PaymentResult,
22+
serializers, Graph, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome,
23+
PaymentResult,
2124
};
2225

2326
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -263,6 +266,34 @@ impl LightningNode for ClnNode {
263266
node_channels.extend(self.node_channels(false).await?);
264267
Ok(node_channels)
265268
}
269+
270+
async fn get_graph(&mut self) -> Result<Graph, LightningError> {
271+
let nodes: Vec<cln_grpc::pb::ListnodesNodes> = self
272+
.client
273+
.list_nodes(ListnodesRequest { id: None })
274+
.await
275+
.map_err(|err| LightningError::GetNodeInfoError(err.to_string()))?
276+
.into_inner()
277+
.nodes;
278+
279+
let mut nodes_by_pk: HashMap<PublicKey, NodeInfo> = HashMap::new();
280+
281+
for node in nodes {
282+
nodes_by_pk.insert(
283+
PublicKey::from_slice(&node.nodeid).expect("Public Key not valid"),
284+
NodeInfo {
285+
pubkey: PublicKey::from_slice(&node.nodeid).expect("Public Key not valid"),
286+
alias: node.clone().alias.unwrap_or(String::new()),
287+
features: node
288+
.features
289+
.clone()
290+
.map_or(NodeFeatures::empty(), NodeFeatures::from_be_bytes),
291+
},
292+
);
293+
}
294+
295+
Ok(Graph { nodes_by_pk })
296+
}
266297
}
267298

268299
async fn reader(filename: &str) -> Result<Vec<u8>, Error> {

simln-lib/src/eclair.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{
2-
serializers, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome, PaymentResult,
2+
serializers, Graph, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome,
3+
PaymentResult,
34
};
45
use async_trait::async_trait;
56
use bitcoin::secp256k1::PublicKey;
@@ -243,6 +244,29 @@ impl LightningNode for EclairNode {
243244

244245
Ok(capacities_msat)
245246
}
247+
248+
async fn get_graph(&mut self) -> Result<Graph, LightningError> {
249+
let nodes: NodesResponse = self
250+
.client
251+
.request("nodes", None)
252+
.await
253+
.map_err(|err| LightningError::GetNodeInfoError(err.to_string()))?;
254+
255+
let mut nodes_by_pk: HashMap<PublicKey, NodeInfo> = HashMap::new();
256+
257+
for node in nodes {
258+
nodes_by_pk.insert(
259+
PublicKey::from_str(&node.node_id).expect("Public Key not valid"),
260+
NodeInfo {
261+
pubkey: PublicKey::from_str(&node.node_id).expect("Public Key not valid"),
262+
alias: node.alias.clone(),
263+
features: parse_json_to_node_features(&node.features),
264+
},
265+
);
266+
}
267+
268+
Ok(Graph { nodes_by_pk })
269+
}
246270
}
247271

248272
#[derive(Debug, Deserialize)]
@@ -288,7 +312,16 @@ struct NodeResponse {
288312
announcement: Announcement,
289313
}
290314

315+
#[derive(Debug, Deserialize)]
316+
struct NodeInGraph {
317+
#[serde(rename = "nodeId")]
318+
node_id: String,
319+
alias: String,
320+
features: Value,
321+
}
322+
291323
type ChannelsResponse = Vec<Channel>;
324+
type NodesResponse = Vec<NodeInGraph>;
292325

293326
#[derive(Debug, Deserialize)]
294327
struct Channel {

simln-lib/src/lib.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ pub enum LightningError {
258258
/// Error that occurred while listing channels.
259259
#[error("List channels error: {0}")]
260260
ListChannelsError(String),
261+
/// Error that occurred while getting graph.
262+
#[error("Get graph error: {0}")]
263+
GetGraphError(String),
261264
}
262265

263266
/// Information about a Lightning Network node.
@@ -286,6 +289,33 @@ impl Display for NodeInfo {
286289
}
287290
}
288291

292+
#[derive(Debug, Clone)]
293+
pub struct ChannelInfo {
294+
pub channel_id: ShortChannelID,
295+
pub capacity_msat: u64,
296+
}
297+
298+
#[derive(Debug, Clone)]
299+
/// Graph represents the network graph of the simulated network and is useful for efficient lookups.
300+
pub struct Graph {
301+
// Store nodes' information keyed by their public key.
302+
pub nodes_by_pk: HashMap<PublicKey, NodeInfo>,
303+
}
304+
305+
impl Graph {
306+
pub fn new() -> Self {
307+
Graph {
308+
nodes_by_pk: HashMap::new(),
309+
}
310+
}
311+
}
312+
313+
impl Default for Graph {
314+
fn default() -> Self {
315+
Self::new()
316+
}
317+
}
318+
289319
/// LightningNode represents the functionality that is required to execute events on a lightning node.
290320
#[async_trait]
291321
pub trait LightningNode: Send {
@@ -310,6 +340,8 @@ pub trait LightningNode: Send {
310340
/// Lists all channels, at present only returns a vector of channel capacities in msat because no further
311341
/// information is required.
312342
async fn list_channels(&mut self) -> Result<Vec<u64>, LightningError>;
343+
/// Get the network graph from the point of view of a given node.
344+
async fn get_graph(&mut self) -> Result<Graph, LightningError>;
313345
}
314346

315347
/// Represents an error that occurs when generating a destination for a payment.

simln-lib/src/lnd.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use std::collections::HashSet;
22
use std::{collections::HashMap, str::FromStr};
33

44
use crate::{
5-
serializers, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome, PaymentResult,
5+
serializers, Graph, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome,
6+
PaymentResult,
67
};
78
use async_trait::async_trait;
89
use bitcoin::hashes::{sha256, Hash};
@@ -12,7 +13,9 @@ use lightning::ln::features::NodeFeatures;
1213
use lightning::ln::{PaymentHash, PaymentPreimage};
1314
use serde::{Deserialize, Serialize};
1415
use tonic_lnd::lnrpc::{payment::PaymentStatus, GetInfoRequest, GetInfoResponse};
15-
use tonic_lnd::lnrpc::{ListChannelsRequest, NodeInfoRequest, PaymentFailureReason};
16+
use tonic_lnd::lnrpc::{
17+
ChannelGraphRequest, ListChannelsRequest, NodeInfoRequest, PaymentFailureReason,
18+
};
1619
use tonic_lnd::routerrpc::TrackPaymentRequest;
1720
use tonic_lnd::tonic::Code::Unavailable;
1821
use tonic_lnd::tonic::Status;
@@ -275,6 +278,34 @@ impl LightningNode for LndNode {
275278
.map(|channel| 1000 * channel.capacity as u64)
276279
.collect())
277280
}
281+
282+
async fn get_graph(&mut self) -> Result<Graph, LightningError> {
283+
let nodes = self
284+
.client
285+
.lightning()
286+
.describe_graph(ChannelGraphRequest {
287+
include_unannounced: false,
288+
})
289+
.await
290+
.map_err(|err| LightningError::GetNodeInfoError(err.to_string()))?
291+
.into_inner()
292+
.nodes;
293+
294+
let mut nodes_by_pk: HashMap<PublicKey, NodeInfo> = HashMap::new();
295+
296+
for node in nodes {
297+
nodes_by_pk.insert(
298+
PublicKey::from_str(&node.pub_key).expect("Public Key not valid"),
299+
NodeInfo {
300+
pubkey: PublicKey::from_str(&node.pub_key).expect("Public Key not valid"),
301+
alias: node.alias.clone(),
302+
features: parse_node_features(node.features.keys().cloned().collect()),
303+
},
304+
);
305+
}
306+
307+
Ok(Graph { nodes_by_pk })
308+
}
278309
}
279310

280311
fn string_to_payment_hash(hash: &str) -> Result<PaymentHash, LightningError> {

0 commit comments

Comments
 (0)