Skip to content

Commit

Permalink
offers: don't choose unadvertised node as introduction node
Browse files Browse the repository at this point in the history
  • Loading branch information
orbitalturtle committed Aug 27, 2024
1 parent 0a2ffb8 commit dd43ae2
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 30 deletions.
9 changes: 6 additions & 3 deletions src/lnd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ use std::fmt::Display;
use std::path::PathBuf;
use std::{fmt, fs};
use tonic_lnd::lnrpc::{
GetInfoResponse, HtlcAttempt, LightningNode, ListPeersResponse, Payment, QueryRoutesResponse,
Route,
GetInfoResponse, HtlcAttempt, ListPeersResponse, NodeInfo, Payment, QueryRoutesResponse, Route,
};
use tonic_lnd::signrpc::{KeyDescriptor, KeyLocator};
use tonic_lnd::tonic::Status;
Expand Down Expand Up @@ -404,7 +403,11 @@ pub trait MessageSigner {
pub trait PeerConnector {
async fn list_peers(&mut self) -> Result<ListPeersResponse, Status>;
async fn connect_peer(&mut self, node_id: String, addr: String) -> Result<(), Status>;
async fn get_node_info(&mut self, pub_key: String) -> Result<Option<LightningNode>, Status>;
async fn get_node_info(
&mut self,
pub_key: String,
include_channels: bool,
) -> Result<NodeInfo, Status>;
}

/// InvoicePayer provides a layer of abstraction over the LND API for paying for a BOLT 12 invoice.
Expand Down
179 changes: 157 additions & 22 deletions src/lndk_offers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ use std::fmt::Display;
use std::str::FromStr;
use tokio::task;
use tonic_lnd::lnrpc::{
ChanInfoRequest, GetInfoRequest, HtlcAttempt, LightningNode, ListPeersRequest,
ListPeersResponse, Payment, QueryRoutesResponse, Route,
ChanInfoRequest, GetInfoRequest, HtlcAttempt, ListPeersRequest, ListPeersResponse, NodeInfo,
Payment, QueryRoutesResponse, Route,
};
use tonic_lnd::routerrpc::TrackPaymentRequest;
use tonic_lnd::signrpc::{KeyDescriptor, KeyLocator, SignMessageReq};
Expand Down Expand Up @@ -253,8 +253,11 @@ impl OfferHandler {

/// create_reply_path creates a blinded path to provide to the offer maker when requesting an
/// invoice so they know where to send the invoice back to. We try to find a peer that we're
/// connected to with onion messaging support that we can use to form a blinded path,
/// otherwise we creae a blinded path directly to ourselves.
/// connected with the necessary requirements to form a blinded path. The peer needs two things:
/// 1) Onion messaging support.
/// 2) To be an advertised node with at least one public channel.
///
/// Otherwise we create a blinded path directly to ourselves.
pub async fn create_reply_path(
&self,
mut connector: impl PeerConnector + std::marker::Send + 'static,
Expand All @@ -271,7 +274,19 @@ impl OfferHandler {
let pubkey = PublicKey::from_str(&peer.pub_key).unwrap();
let onion_support = features_support_onion_messages(&peer.features);
if onion_support {
// We also need to check that the candidate introduction node is actually an
// advertised node. If it isn't, get_node_info will return a node with zero
// channels.
match connector.get_node_info(peer.pub_key, true).await {
Ok(node) => {
if node.channels.is_empty() {
continue;
}
}
Err(_) => continue,
};
intro_node = Some(pubkey);
break;
}
}

Expand Down Expand Up @@ -426,11 +441,11 @@ pub async fn connect_to_peer(
}

let node = connector
.get_node_info(node_id_str.clone())
.get_node_info(node_id_str.clone(), false)
.await
.map_err(OfferError::PeerConnectError)?;

let node = match node {
let node = match node.node {
Some(node) => node,
None => return Err(OfferError::NodeAddressNotFound),
};
Expand Down Expand Up @@ -477,16 +492,20 @@ impl PeerConnector for Client {
.map(|_| ())
}

async fn get_node_info(&mut self, pub_key: String) -> Result<Option<LightningNode>, Status> {
async fn get_node_info(
&mut self,
pub_key: String,
include_channels: bool,
) -> Result<NodeInfo, Status> {
let req = tonic_lnd::lnrpc::NodeInfoRequest {
pub_key,
include_channels: false,
include_channels,
};

self.lightning()
.get_node_info(req)
.await
.map(|resp| resp.into_inner().node)
.map(|resp| resp.into_inner())
}
}

Expand Down Expand Up @@ -693,10 +712,11 @@ mod tests {
use lightning::offers::merkle::SignError;
use lightning::offers::offer::{OfferBuilder, Quantity};
use mockall::mock;
use mockall::predicate::eq;
use std::collections::HashMap;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use tonic_lnd::lnrpc::{NodeAddress, Payment};
use tonic_lnd::lnrpc::{ChannelEdge, LightningNode, NodeAddress, Payment};

fn get_offer() -> String {
"lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqgn3qzsyvfkx26qkyypvr5hfx60h9w9k934lt8s2n6zc0wwtgqlulw7dythr83dqx8tzumg".to_string()
Expand Down Expand Up @@ -773,7 +793,7 @@ mod tests {
#[async_trait]
impl PeerConnector for TestPeerConnector {
async fn list_peers(&mut self) -> Result<ListPeersResponse, Status>;
async fn get_node_info(&mut self, pub_key: String) -> Result<Option<LightningNode>, Status>;
async fn get_node_info(&mut self, pub_key: String, include_channels: bool) -> Result<NodeInfo, Status>;
async fn connect_peer(&mut self, node_id: String, addr: String) -> Result<(), Status>;
}
}
Expand Down Expand Up @@ -923,17 +943,22 @@ mod tests {
})
});

connector_mock.expect_get_node_info().returning(|_| {
connector_mock.expect_get_node_info().returning(|_, _| {
let node_addr = NodeAddress {
network: String::from("regtest"),
addr: String::from("127.0.0.1"),
};
let node = LightningNode {
let node = Some(LightningNode {
addresses: vec![node_addr],
..Default::default()
});

let node_info = NodeInfo {
node,
..Default::default()
};

Ok(Some(node))
Ok(node_info)
});

connector_mock
Expand All @@ -959,17 +984,20 @@ mod tests {
})
});

connector_mock.expect_get_node_info().returning(|_| {
connector_mock.expect_get_node_info().returning(|_, _| {
let node_addr = NodeAddress {
network: String::from("regtest"),
addr: String::from("127.0.0.1"),
};
let node = LightningNode {
let node = Some(LightningNode {
addresses: vec![node_addr],
..Default::default()
};
});

Ok(Some(node))
Ok(NodeInfo {
node,
..Default::default()
})
});

let pubkey = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
Expand All @@ -985,17 +1013,20 @@ mod tests {
})
});

connector_mock.expect_get_node_info().returning(|_| {
connector_mock.expect_get_node_info().returning(|_, _| {
let node_addr = NodeAddress {
network: String::from("regtest"),
addr: String::from("127.0.0.1"),
};
let node = LightningNode {
let node = Some(LightningNode {
addresses: vec![node_addr],
..Default::default()
};
});

Ok(Some(node))
Ok(NodeInfo {
node,
..Default::default()
})
});

connector_mock
Expand Down Expand Up @@ -1025,6 +1056,17 @@ mod tests {
Ok(ListPeersResponse { peers: vec![peer] })
});

connector_mock.expect_get_node_info().returning(|_, _| {
let node = Some(LightningNode {
..Default::default()
});

Ok(NodeInfo {
node,
..Default::default()
})
});

let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let handler = OfferHandler::default();
assert!(handler
Expand Down Expand Up @@ -1066,6 +1108,99 @@ mod tests {
.is_err())
}

#[tokio::test]
async fn test_create_reply_path_not_advertised() {
// First lets test that if we're only connected to one peer. It has onion support, but the
// node isn't advertised, meaning we can't find it using get_node_info. This should return
// a blinded path with only one hop.
let mut connector_mock = MockTestPeerConnector::new();
connector_mock.expect_list_peers().returning(|| {
let feature = tonic_lnd::lnrpc::Feature {
..Default::default()
};
let mut feature_entry = HashMap::new();
feature_entry.insert(38, feature);

let peer = tonic_lnd::lnrpc::Peer {
pub_key: get_pubkeys()[0].clone(),
features: feature_entry,
..Default::default()
};
Ok(ListPeersResponse { peers: vec![peer] })
});

connector_mock
.expect_get_node_info()
.returning(|_, _| Err(Status::not_found("node was not found")));

let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let handler = OfferHandler::default();
let resp = handler
.create_reply_path(connector_mock, receiver_node_id)
.await;
assert!(resp.is_ok());
assert!(resp.unwrap().blinded_hops.len() == 1);

// Now let's test that we have two peers that both have onion support feature flags set.
// One isn't advertised (i.e. it isn't found in get_node_info). But the second is. This
// should succeed.
let mut connector_mock = MockTestPeerConnector::new();
connector_mock.expect_list_peers().returning(|| {
let feature = tonic_lnd::lnrpc::Feature {
..Default::default()
};
let mut feature_entry = HashMap::new();
feature_entry.insert(38, feature);

let keys = get_pubkeys();

let peer1 = tonic_lnd::lnrpc::Peer {
pub_key: keys[0].clone(),
features: feature_entry.clone(),
..Default::default()
};
let peer2 = tonic_lnd::lnrpc::Peer {
pub_key: keys[1].clone(),
features: feature_entry,
..Default::default()
};
Ok(ListPeersResponse {
peers: vec![peer1, peer2],
})
});

let keys = get_pubkeys();
connector_mock
.expect_get_node_info()
.with(eq(keys[0].clone()), eq(true))
.returning(|_, _| Err(Status::not_found("node was not found")));

connector_mock
.expect_get_node_info()
.with(eq(keys[1].clone()), eq(true))
.returning(|_, _| {
let node = Some(LightningNode {
..Default::default()
});

Ok(NodeInfo {
node,
channels: vec![ChannelEdge {
..Default::default()
}],
..Default::default()
})
});

let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let handler = OfferHandler::default();
let resp = handler
.create_reply_path(connector_mock, receiver_node_id)
.await;
assert!(resp.is_ok());
assert!(resp.unwrap().blinded_hops.len() == 2);
}

#[tokio::test]
async fn test_send_payment() {
let mut payer_mock = MockTestInvoicePayer::new();
Expand Down
3 changes: 2 additions & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub async fn setup_test_infrastructure(
pub async fn connect_network(
ldk1: &LdkNode,
ldk2: &LdkNode,
announce_channel: bool,
lnd: &mut LndNode,
bitcoind: &BitcoindNode,
) -> (PublicKey, PublicKey, PublicKey) {
Expand Down Expand Up @@ -136,7 +137,7 @@ pub async fn connect_network(
SocketAddr::from_str(&lnd_network_addr).unwrap(),
200000,
10000000,
true,
announce_channel,
)
.await
.unwrap();
Expand Down
Loading

0 comments on commit dd43ae2

Please sign in to comment.