Skip to content

Commit

Permalink
Adds most of CLN node implementation
Browse files Browse the repository at this point in the history
`track_payment` is missing
  • Loading branch information
sr-gi committed Aug 23, 2023
1 parent be16638 commit 11541c5
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 16 deletions.
12 changes: 11 additions & 1 deletion sim-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async fn main() -> anyhow::Result<()> {
let mut clients: HashMap<PublicKey, Arc<Mutex<dyn LightningNode + Send>>> = 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 node_id = c.id;
Expand All @@ -49,7 +50,16 @@ async fn main() -> anyhow::Result<()> {
clients.insert(node_id, Arc::new(Mutex::new(lnd)));
}
NodeConnection::CLN(c) => {
todo!();
let node_id = c.id;
let cln = ClnNode::new(c).await?;

log::info!(
"Connected to {} - Node ID: {}",
cln.get_info().alias,
cln.get_info().pubkey
);

clients.insert(node_id, Arc::new(Mutex::new(cln)));
}
}
}
Expand Down
145 changes: 145 additions & 0 deletions sim-lib/src/cln.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use async_trait::async_trait;
use bitcoin::secp256k1::PublicKey;
use cln_grpc::pb::{
node_client::NodeClient, Amount, GetinfoRequest, GetinfoResponse, KeysendRequest,
KeysendResponse, ListnodesRequest,
};
use lightning::ln::features::NodeFeatures;
use lightning::ln::PaymentHash;

use tokio::fs::File;
use tokio::io::{AsyncReadExt, Error};
use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
use triggered::Listener;

use crate::{ClnConnection, LightningError, LightningNode, NodeInfo, PaymentResult};

pub struct ClnNode {
pub client: NodeClient<Channel>,
info: NodeInfo,
}

impl ClnNode {
pub async fn new(connection: ClnConnection) -> Result<Self, LightningError> {
let ca_pem = reader(&connection.ca_cert).await.map_err(|_| {
LightningError::ConnectionError("Cannot loads CA certificate".to_string())
})?;
let client_pem = reader(&connection.client_cert).await.map_err(|_| {
LightningError::ConnectionError("Cannot loads client certificate".to_string())
})?;
let client_key = reader(&connection.client_key)
.await
.map_err(|_| LightningError::ConnectionError("Cannot loads client key".to_string()))?;

let ca = Certificate::from_pem(ca_pem);
let ident = Identity::from_pem(client_pem, client_key);

let tls = ClientTlsConfig::new()
.domain_name("cln")
.identity(ident)
.ca_certificate(ca);

let channel = Channel::from_shared(connection.address.to_string())
.map_err(|err| LightningError::ConnectionError(err.to_string()))?
.tls_config(tls)
.map_err(|_| {
LightningError::ConnectionError("Cannot establish tls connection".to_string())
})?
.connect()
.await
.map_err(|_| {
LightningError::ConnectionError("Cannot connect to gRPC server".to_string())
})?;
let mut client = NodeClient::new(channel);

let GetinfoResponse { id, alias, .. } = client
.getinfo(GetinfoRequest {})
.await
.map_err(|err| LightningError::GetInfoError(err.to_string()))?
.into_inner();

Ok(Self {
client,
info: NodeInfo {
pubkey: PublicKey::from_slice(&id)
.map_err(|err| LightningError::GetInfoError(err.to_string()))?,
features: vec![],
alias,
},
})
}
}

#[async_trait]
impl LightningNode for ClnNode {
fn get_info(&self) -> &NodeInfo {
&self.info
}

async fn send_payment(
&mut self,
dest: PublicKey,
amount_msat: u64,
) -> Result<PaymentHash, LightningError> {
let KeysendResponse { payment_hash, .. } = self
.client
.key_send(KeysendRequest {
destination: dest.serialize().to_vec(),
amount_msat: Some(Amount { msat: amount_msat }),
..Default::default()
})
.await
.map_err(|err| LightningError::SendPaymentError(err.to_string()))?
.into_inner();
let slice: [u8; 32] = payment_hash
.as_slice()
.try_into()
.map_err(|_| LightningError::InvalidPaymentHash)?;

Ok(PaymentHash(slice))
}

async fn track_payment(
&mut self,
_hash: PaymentHash,
_shutdown: Listener,
) -> Result<PaymentResult, LightningError> {
unimplemented!()
}

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
.list_nodes(ListnodesRequest {
id: Some(node_id.clone()),
})
.await
.map_err(|err| LightningError::GetNodeInfoError(err.to_string()))?
.into_inner()
.nodes;

// We are filtering `list_nodes` to a single node, so we should get either an empty vector or one with a single element
if let Some(node) = nodes.first() {
Ok(node
.features
.clone()
.map_or(NodeFeatures::empty(), |mut f| {
// We need to reverse this given it has the CLN wire encoding which is BE
f.reverse();
NodeFeatures::from_le_bytes(f)
}))
} else {
Err(LightningError::GetNodeInfoError(
"Node not found".to_string(),
))
}
}
}

async fn reader(filename: &str) -> Result<Vec<u8>, Error> {
let mut file = File::open(filename).await?;
let mut contents = vec![];
file.read_to_end(&mut contents).await?;
Ok(contents)
}
12 changes: 5 additions & 7 deletions sim-lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use async_trait::async_trait;
use bitcoin::secp256k1::PublicKey;
use csv::WriterBuilder;
use lightning::ln::features::NodeFeatures;
use lightning::ln::PaymentHash;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
Expand All @@ -19,8 +20,6 @@ pub mod cln;
pub mod lnd;
mod serializers;

const KEYSEND_OPTIONAL: u32 = 55;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum NodeConnection {
#[serde(alias = "lnd", alias = "Lnd")]
Expand Down Expand Up @@ -122,9 +121,8 @@ pub trait LightningNode {
hash: PaymentHash,
shutdown: Listener,
) -> Result<PaymentResult, LightningError>;
/// Looks up a node's announcement in the graph. This function currently only returns features, as they're all we
/// need, but may be updated to include any other node announcement fields if required.
async fn get_node_announcement(&self, node: PublicKey) -> Result<HashSet<u32>, LightningError>;
/// Gets the list of features of a given node
async fn get_node_features(&mut self, node: PublicKey) -> Result<NodeFeatures, LightningError>;
}

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -222,11 +220,11 @@ impl Simulation {
let features = source_node
.lock()
.await
.get_node_announcement(payment_flow.destination)
.get_node_features(payment_flow.destination)
.await
.map_err(|err| LightningError::GetNodeInfoError(err.to_string()))?;

if !features.contains(&KEYSEND_OPTIONAL) {
if !features.supports_keysend() {
return Err(LightningError::ValidationError(format!(
"destination node does not support keysend {}",
payment_flow.destination,
Expand Down
23 changes: 15 additions & 8 deletions sim-lib/src/lnd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ use crate::{
use async_trait::async_trait;
use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::PublicKey;
use lightning::ln::features::NodeFeatures;
use lightning::ln::{PaymentHash, PaymentPreimage};
use std::collections::HashSet;
use tonic_lnd::lnrpc::{payment::PaymentStatus, GetInfoRequest, GetInfoResponse};
use tonic_lnd::lnrpc::{NodeInfoRequest, PaymentFailureReason};
use tonic_lnd::routerrpc::TrackPaymentRequest;
use tonic_lnd::{routerrpc::SendPaymentRequest, Client};
use triggered::Listener;

const KEYSEND_OPTIONAL: u32 = 55;
const KEYSEND_KEY: u64 = 5482373484;
const SEND_PAYMENT_TIMEOUT_SECS: i32 = 300;

Expand Down Expand Up @@ -158,11 +159,10 @@ impl LightningNode for LndNode {
}
}

async fn get_node_announcement(&self, node: PublicKey) -> Result<HashSet<u32>, LightningError> {
let mut client = self.client.clone();
let lightning_client = client.lightning();

let node_info = lightning_client
async fn get_node_features(&mut self, node: PublicKey) -> Result<NodeFeatures, LightningError> {
let node_info = self
.client
.lightning()
.get_node_info(NodeInfoRequest {
pub_key: node.to_string(),
include_channels: false,
Expand All @@ -171,11 +171,18 @@ impl LightningNode for LndNode {
.map_err(|err| LightningError::GetNodeInfoError(err.to_string()))?
.into_inner();

let mut nf = NodeFeatures::empty();

if let Some(node_info) = node_info.node {
Ok(node_info.features.into_keys().collect())
// FIXME: We only care about the keysend feature now, but we should parse the whole feature vector
// into LDK's feature bitvector and properly construct NodeFeatures.
if node_info.features.contains_key(&KEYSEND_OPTIONAL) {
nf.set_keysend_optional()
}
Ok(nf)
} else {
Err(LightningError::GetNodeInfoError(
"node not found".to_string(),
"Node not found".to_string(),
))
}
}
Expand Down

0 comments on commit 11541c5

Please sign in to comment.