From e2d5fb3badc39ce29a704b00ef2e15635406b61f Mon Sep 17 00:00:00 2001 From: Sergi Delgado Segura Date: Wed, 20 Sep 2023 11:42:36 +0000 Subject: [PATCH] Allows aliases to be used as source/destination of activities The approach followed for this is pretty straight-forward: instead of loading PK directly to our ActivityDefinition data struct, we allow it to be NodeId (in the same fashion we do for allowing aliases in node definitions). However, we internally identify activities using PK, so we do an additional mapping step once the data is loaded to go from NodeId{PK, Alias} to NodeId(PK), hence, once data is passed to the simulator it is always identified by NodeId(PK). Notice that, after `Simulation::validate_activity` it is safe to call `NodeId::get_pk::unwrap()`, given no NodeId::Alias should have passed validation. For activity destinations, we also need to make sure that if an alias has been provided we do control that node, otherwise the Alias->PK mapping cannot be done, in which case we fail with a `ValidationError`.I've performed the validation in main to avoid having to pass the reverse_pk_alias map to the simulator (given we will need an alias_pk map in the simulator later on to be able to log both PKs and aliases). This may break a bit the layer separation (we may not want to raise a `ValidationError` in main), so I'm happy to re-structure this in the next commit if we are happy with two maps (or any alternative approach you can come up with) --- sim-cli/src/main.rs | 88 ++++++++++++++++++++++++++++++++------------- sim-lib/src/cln.rs | 5 ++- sim-lib/src/lib.rs | 76 +++++++++++++++++++++++++++++---------- sim-lib/src/lnd.rs | 5 ++- 4 files changed, 128 insertions(+), 46 deletions(-) diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index 37822088..d4331013 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -6,7 +6,10 @@ use tokio::sync::Mutex; use clap::Parser; use log::LevelFilter; -use sim_lib::{cln::ClnNode, lnd::LndNode, Config, LightningNode, NodeConnection, Simulation}; +use sim_lib::{ + cln::ClnNode, lnd::LndNode, Config, LightningError, LightningNode, NodeConnection, NodeId, + Simulation, +}; use simple_logger::SimpleLogger; #[derive(Parser)] @@ -35,36 +38,71 @@ async fn main() -> anyhow::Result<()> { .unwrap(); let config_str = std::fs::read_to_string(cli.config)?; - let Config { nodes, activity } = serde_json::from_str(&config_str)?; + let Config { + nodes, + mut activity, + } = serde_json::from_str(&config_str)?; let mut clients: HashMap>> = HashMap::new(); + let mut r_alias_map = HashMap::new(); for connection in nodes { - // TODO: We should simplify this into two minimal branches plus shared logging and inserting into the list - match connection { - NodeConnection::LND(c) => { - let lnd = LndNode::new(c).await?; - let node_info = lnd.get_info(); - - log::info!( - "Connected to {} - Node ID: {}.", - node_info.alias, - node_info.pubkey - ); - - clients.insert(node_info.pubkey, Arc::new(Mutex::new(lnd))); - } - NodeConnection::CLN(c) => { - let cln = ClnNode::new(c).await?; - let node_info = cln.get_info(); + // TODO: Feels like there should be a better way of doing this without having to Arc> it at this time. + // Box sort of works, but we won't know the size of the dyn LightningNode at compile time so the compiler will + // scream at us when trying to create the Arc> later on while adding the node to the clients map + let node: Arc> = match connection { + NodeConnection::LND(c) => Arc::new(Mutex::new(LndNode::new(c).await?)), + NodeConnection::CLN(c) => Arc::new(Mutex::new(ClnNode::new(c).await?)), + }; + + let node_info = node.lock().await.get_info().clone(); + + log::info!( + "Connected to {} - Node ID: {}.", + node_info.alias, + node_info.pubkey + ); + + if clients.contains_key(&node_info.pubkey) { + anyhow::bail!(LightningError::ValidationError(format!( + "Duplicated node: {}", + node_info.pubkey + ))); + } - log::info!( - "Connected to {} - Node ID: {}.", - node_info.alias, - node_info.pubkey - ); + if r_alias_map.contains_key(&node_info.alias) { + anyhow::bail!(LightningError::ValidationError(format!( + "Duplicated node: {}", + node_info.alias + ))); + } + + r_alias_map.insert(node_info.alias.clone(), node_info.pubkey); + clients.insert(node_info.pubkey, node); + } - clients.insert(node_info.pubkey, Arc::new(Mutex::new(cln))); + // Make all the activities identifiable by PK internally + for act in activity.iter_mut() { + // We can only map aliases to nodes we control, so if either the source or destination alias + // is not in r_alias_map, we fail + if let NodeId::Alias(a) = &act.source { + if let Some(pk) = r_alias_map.get(a) { + act.source = NodeId::PublicKey(*pk); + } else { + anyhow::bail!(LightningError::ValidationError(format!( + "Unknown activity source: {}", + act.source + ))); + } + } + if let NodeId::Alias(a) = &act.destination { + if let Some(pk) = r_alias_map.get(a) { + act.destination = NodeId::PublicKey(*pk); + } else { + anyhow::bail!(LightningError::ValidationError(format!( + "Unknown activity destination: {}", + act.destination + ))); } } } diff --git a/sim-lib/src/cln.rs b/sim-lib/src/cln.rs index 0ffa22ed..81006100 100644 --- a/sim-lib/src/cln.rs +++ b/sim-lib/src/cln.rs @@ -171,7 +171,10 @@ impl LightningNode for ClnNode { } } - async fn get_node_features(&mut self, node: PublicKey) -> Result { + async fn get_node_features( + &mut self, + node: &PublicKey, + ) -> Result { let node_id = node.serialize().to_vec(); let nodes: Vec = self .client diff --git a/sim-lib/src/lib.rs b/sim-lib/src/lib.rs index 7b02a90d..c990931d 100644 --- a/sim-lib/src/lib.rs +++ b/sim-lib/src/lib.rs @@ -35,6 +35,29 @@ pub enum NodeId { Alias(String), } +impl NodeId { + pub fn get_pk(&self) -> Result<&PublicKey, String> { + if let NodeId::PublicKey(pk) = self { + Ok(pk) + } else { + panic!("NodeId is not a PublicKey. This should never happen, please report") + } + } +} + +impl std::fmt::Display for NodeId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + NodeId::PublicKey(pk) => pk.to_string(), + NodeId::Alias(a) => a.to_owned(), + } + ) + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct LndConnection { #[serde(with = "serializers::serde_node_id")] @@ -65,12 +88,14 @@ pub struct Config { pub activity: Vec, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ActivityDefinition { // The source of the action. - pub source: PublicKey, + #[serde(with = "serializers::serde_node_id")] + pub source: NodeId, // The destination of the action. - pub destination: PublicKey, + #[serde(with = "serializers::serde_node_id")] + pub destination: NodeId, // The interval of the action, as in every how many seconds the action is performed. #[serde(alias = "interval_secs")] pub interval: u16, @@ -136,7 +161,8 @@ pub trait LightningNode { shutdown: Listener, ) -> Result; /// Gets the list of features of a given node - async fn get_node_features(&mut self, node: PublicKey) -> Result; + async fn get_node_features(&mut self, node: &PublicKey) + -> Result; } #[derive(Clone, Copy)] @@ -247,27 +273,36 @@ impl Simulation { for payment_flow in self.activity.iter() { // We need every source node that is configured to execute some activity to be included in our set of // nodes so that we can execute actions on it. - let source_node = - self.nodes - .get(&payment_flow.source) - .ok_or(LightningError::ValidationError(format!( - "source node not found {}", - payment_flow.source, - )))?; + let src_id = payment_flow + .source + .get_pk() + .map_err(LightningError::ValidationError)?; + let src_node = self + .nodes + .get(src_id) + .ok_or(LightningError::ValidationError(format!( + "source node not found {}", + src_id, + )))?; + + let dest_id = payment_flow + .destination + .get_pk() + .map_err(LightningError::ValidationError)?; // Destinations must support keysend to be able to receive payments. // Note: validation should be update with a different check if an action is not a payment. - let features = source_node + let features = src_node .lock() .await - .get_node_features(payment_flow.destination) + .get_node_features(dest_id) .await .map_err(|err| LightningError::GetNodeInfoError(err.to_string()))?; if !features.supports_keysend() { return Err(LightningError::ValidationError(format!( "destination node does not support keysend {}", - payment_flow.destination, + dest_id, ))); } } @@ -383,8 +418,8 @@ impl Simulation { for (id, node) in self.nodes.iter().filter(|(pk, _)| { self.activity .iter() - .map(|a| a.source) - .collect::>() + .map(|a| a.source.get_pk().unwrap()) + .collect::>() .contains(pk) }) { // For each active node, we'll create a sender and receiver channel to produce and consumer @@ -405,9 +440,11 @@ impl Simulation { } for description in self.activity.iter() { - let sender_chan = producer_channels.get(&description.source).unwrap(); + let sender_chan = producer_channels + .get(&description.source.get_pk().unwrap()) + .unwrap(); tasks.spawn(produce_events( - *description, + description.clone(), sender_chan.clone(), shutdown.clone(), listener.clone(), @@ -486,7 +523,8 @@ async fn produce_events( shutdown: Trigger, listener: Listener, ) { - let e: NodeAction = NodeAction::SendPayment(act.destination, act.amount_msat); + let e: NodeAction = + NodeAction::SendPayment(*act.destination.get_pk().unwrap(), act.amount_msat); let interval = time::Duration::from_secs(act.interval as u64); log::debug!( diff --git a/sim-lib/src/lnd.rs b/sim-lib/src/lnd.rs index e0d4f659..a0500f6c 100644 --- a/sim-lib/src/lnd.rs +++ b/sim-lib/src/lnd.rs @@ -190,7 +190,10 @@ impl LightningNode for LndNode { } } - async fn get_node_features(&mut self, node: PublicKey) -> Result { + async fn get_node_features( + &mut self, + node: &PublicKey, + ) -> Result { let node_info = self .client .lightning()