Skip to content

Commit 55cc91a

Browse files
authored
Merge pull request #254 from chuksys/allow-unspecified-alias-destinations
Allow unspecified alias destinations
2 parents b8a563c + 690c4b1 commit 55cc91a

File tree

8 files changed

+238
-29
lines changed

8 files changed

+238
-29
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,11 @@ The example simulation file below sets up the following simulation:
248248
```
249249

250250

251-
Nodes can be identified by their public key or an id string (as
252-
described above). Activity sources and destinations may reference the
253-
`id` defined in `nodes`, but destinations that are not listed in `nodes`
254-
*must* provide a valid public key.
251+
Activity sources must reference an `id` defined in `nodes`, because the simulator can
252+
only send payments from nodes that it controls. Destinations may reference either an
253+
`id` defined in `nodes` or provide a pubkey or alias of a node in the public network.
254+
If the alias provided is not unique in the public network, a pubkey must be used
255+
to identify the node.
255256

256257
### Simulation Output
257258

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)