Skip to content

Commit

Permalink
Allows aliases to be used as source/destination of activities
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
sr-gi committed Sep 20, 2023
1 parent 345ce66 commit e2d5fb3
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 46 deletions.
88 changes: 63 additions & 25 deletions sim-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<PublicKey, Arc<Mutex<dyn LightningNode + Send>>> = 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<Mutex>> 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<Mutex>> later on while adding the node to the clients map
let node: Arc<Mutex<dyn LightningNode + Send>> = 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
)));
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion sim-lib/src/cln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ impl LightningNode for ClnNode {
}
}

async fn get_node_features(&mut self, node: PublicKey) -> Result<NodeFeatures, LightningError> {
async fn get_node_features(
&mut self,
node: &PublicKey,
) -> Result<NodeFeatures, LightningError> {
let node_id = node.serialize().to_vec();
let nodes: Vec<cln_grpc::pb::ListnodesNodes> = self
.client
Expand Down
76 changes: 57 additions & 19 deletions sim-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -65,12 +88,14 @@ pub struct Config {
pub activity: Vec<ActivityDefinition>,
}

#[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,
Expand Down Expand Up @@ -136,7 +161,8 @@ pub trait LightningNode {
shutdown: Listener,
) -> Result<PaymentResult, LightningError>;
/// Gets the list of features of a given node
async fn get_node_features(&mut self, node: PublicKey) -> Result<NodeFeatures, LightningError>;
async fn get_node_features(&mut self, node: &PublicKey)
-> Result<NodeFeatures, LightningError>;
}

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -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,
)));
}
}
Expand Down Expand Up @@ -383,8 +418,8 @@ impl Simulation {
for (id, node) in self.nodes.iter().filter(|(pk, _)| {
self.activity
.iter()
.map(|a| a.source)
.collect::<HashSet<PublicKey>>()
.map(|a| a.source.get_pk().unwrap())
.collect::<HashSet<&PublicKey>>()
.contains(pk)
}) {
// For each active node, we'll create a sender and receiver channel to produce and consumer
Expand All @@ -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(),
Expand Down Expand Up @@ -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!(
Expand Down
5 changes: 4 additions & 1 deletion sim-lib/src/lnd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@ impl LightningNode for LndNode {
}
}

async fn get_node_features(&mut self, node: PublicKey) -> Result<NodeFeatures, LightningError> {
async fn get_node_features(
&mut self,
node: &PublicKey,
) -> Result<NodeFeatures, LightningError> {
let node_info = self
.client
.lightning()
Expand Down

0 comments on commit e2d5fb3

Please sign in to comment.