Merge pull request #235 from f3r10/2025-09-better-peer-selection-for-offers

Better peer selection for offers
This commit is contained in:
Maurice Poirrier 2025-09-25 23:31:22 +02:00 committed by GitHub
commit f7c5a03af9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 696 additions and 28 deletions

View file

@ -28,8 +28,8 @@ use tonic::{Code, Status};
use tonic_lnd::lnrpc::AddInvoiceResponse;
use tonic_lnd::lnrpc::PayReq;
use tonic_lnd::lnrpc::{
FeeLimit, GetInfoResponse, HtlcAttempt, ListPeersResponse, NodeInfo, Payment,
QueryRoutesResponse, Route,
FeeLimit, GetInfoResponse, HtlcAttempt, ListChannelsResponse, ListPeersResponse, NodeInfo,
Payment, QueryRoutesResponse, Route,
};
use tonic_lnd::signrpc::KeyLocator;
use tonic_lnd::tonic::Status as LndStatus;
@ -562,6 +562,7 @@ pub trait PeerConnector {
pub_key: String,
include_channels: bool,
) -> Result<NodeInfo, LndStatus>;
async fn list_active_public_channels(&mut self) -> Result<ListChannelsResponse, LndStatus>;
}
/// InvoicePayer provides a layer of abstraction over the LND API for paying for a BOLT 12 invoice.

View file

@ -181,6 +181,7 @@ mod tests {
async fn list_peers(&mut self) -> Result<tonic_lnd::lnrpc::ListPeersResponse, tonic_lnd::tonic::Status>;
async fn connect_peer(&mut self, node_id: String, addr: String) -> Result<(), tonic_lnd::tonic::Status>;
async fn get_node_info(&mut self, pub_key: String, include_channels: bool) -> Result<tonic_lnd::lnrpc::NodeInfo, tonic_lnd::tonic::Status>;
async fn list_active_public_channels(&mut self) -> Result<tonic_lnd::lnrpc::ListChannelsResponse, tonic_lnd::tonic::Status>;
}
}

View file

@ -12,7 +12,9 @@ use tonic_lnd::routerrpc::TrackPaymentRequest;
use tonic_lnd::tonic::Status;
use tonic_lnd::LightningClient;
use tonic_lnd::{
lnrpc::{ListPeersRequest, ListPeersResponse, NodeInfo},
lnrpc::{
ListChannelsRequest, ListChannelsResponse, ListPeersRequest, ListPeersResponse, NodeInfo,
},
signrpc::{KeyLocator, SignMessageReq},
Client,
};
@ -58,6 +60,19 @@ impl PeerConnector for LightningClient {
self.get_node_info(req).await.map(|resp| resp.into_inner())
}
async fn list_active_public_channels(&mut self) -> Result<ListChannelsResponse, Status> {
let list_req = ListChannelsRequest {
active_only: true,
inactive_only: false,
public_only: true,
private_only: false,
..Default::default()
};
self.list_channels(list_req)
.await
.map(|resp| resp.into_inner())
}
}
#[async_trait]
@ -264,6 +279,7 @@ pub(super) mod tests {
async fn list_peers(&mut self) -> Result<tonic_lnd::lnrpc::ListPeersResponse, 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>;
async fn list_active_public_channels(&mut self) -> Result<ListChannelsResponse, Status>;
}
}
@ -291,6 +307,7 @@ pub(super) mod tests {
async fn list_peers(&mut self) -> Result<tonic_lnd::lnrpc::ListPeersResponse, 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>;
async fn list_active_public_channels(&mut self) -> Result<ListChannelsResponse, Status>;
}
}

View file

@ -42,6 +42,7 @@ use crate::{
offers::handler::{CreateOfferParams, SendPaymentParams},
onion_messenger::MessengerUtilities,
};
use std::collections::HashSet;
use super::{validate_amount, OfferError};
@ -190,13 +191,33 @@ pub(super) async fn create_offer(
let secp_ctx = Secp256k1::new();
let message_context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce });
let path = create_reply_path(creator, node_id, message_context, entropy_source).await?;
let paths =
create_reply_path_for_offer_creation(creator, node_id, message_context, entropy_source)
.await?;
let mut builder =
OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, &secp_ctx)
.amount_msats(args.amount_msats)
.chain(args.chain)
.path(path);
.chain(args.chain);
builder = if let Some(path) = paths.first() {
builder.path(path.clone())
} else {
builder
};
builder = if let Some(path) = paths.get(1) {
builder.path(path.clone())
} else {
builder
};
builder = if let Some(path) = paths.get(2) {
builder.path(path.clone())
} else {
builder
};
builder = match args.description {
Some(description) => builder.description(description),
None => builder,
@ -219,6 +240,7 @@ pub(super) async fn create_offer(
}
None => builder,
};
let offer = builder.build().map_err(OfferError::CreateOfferFailure)?;
Ok(offer)
}
@ -253,15 +275,99 @@ pub(super) async fn create_invoice_info_from_request(
payment_paths,
})
}
/// create_reply_path creates a blinded path to provide to the offer node 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 the necessary requirements to form a blinded path. The peer needs two
/// create_reply_path_for_offer_creation creates blinded paths to provide to the offer.
/// We try to find at least one active public channel and at most three active public channels that
/// can act as blinded paths.
///
/// Otherwise we create a blinded path directly to ourselves.
pub async fn create_reply_path_for_offer_creation(
mut connector: impl PeerConnector + std::marker::Send + 'static,
node_id: PublicKey,
message_context: MessageContext,
messenger_utils: &MessengerUtilities,
) -> Result<Vec<BlindedMessagePath>, OfferError> {
// Find introduction channels for our blinded paths.
let current_channels = connector.list_active_public_channels().await.map_err(|e| {
error!("Could not lookup current channels: {e}.");
OfferError::ListChannelsFailure(e)
})?;
let mut intro_channels = HashSet::new();
for channel in current_channels.channels.iter() {
let pubkey = channel.remote_pubkey.clone();
if intro_channels.len() > 3 {
break;
}
match connector.get_node_info(pubkey, true).await {
Ok(node_info) => match node_info.node {
Some(node) => {
if node_info.channels.is_empty() {
continue;
}
let onion_support = features_support_onion_messages(&node.features);
let other_channels = node_info
.channels
.into_iter()
.filter(|peer_channel| peer_channel.channel_id != channel.chan_id)
.collect::<Vec<tonic_lnd::lnrpc::ChannelEdge>>();
if !other_channels.is_empty() && onion_support {
let pubkey = PublicKey::from_str(&channel.remote_pubkey).unwrap();
intro_channels.insert(pubkey);
}
}
None => continue,
},
Err(_) => continue,
}
}
let secp_ctx = Secp256k1::new();
if intro_channels.is_empty() {
debug!(
"Failed to create a blinded path for the offer.
No active public channels were found on which to build a path."
);
let path =
BlindedMessagePath::one_hop(node_id, message_context, messenger_utils, &secp_ctx)
.map_err(|_| {
error!("Could not create blinded path.");
OfferError::BuildBlindedPathFailure
})?;
Ok(vec![path])
} else {
let mut paths = vec![];
for node in intro_channels {
let nodes = vec![lightning::blinded_path::message::MessageForwardNode {
node_id: node,
short_channel_id: None,
}];
let path = BlindedMessagePath::new(
&nodes,
node_id,
message_context.clone(),
messenger_utils,
&secp_ctx,
)
.map_err(|_| {
error!("Could not create blinded path.");
OfferError::BuildBlindedPathFailure
})?;
paths.push(path)
}
Ok(paths)
}
}
/// create_reply_path_for_outgoing_payments creates a blinded path to provide to the offer node 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 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(
pub async fn create_reply_path_for_outgoing_payments(
mut connector: impl PeerConnector + std::marker::Send + 'static,
node_id: PublicKey,
message_context: MessageContext,
@ -349,7 +455,7 @@ pub async fn send_invoice_request(
let pubkey = PublicKey::from_str(&info.identity_pubkey).unwrap();
let message_context = MessageContext::Offers(offer_context);
let reply_path = create_reply_path(
let reply_path = create_reply_path_for_outgoing_payments(
client.lightning().clone(),
pubkey,
message_context,
@ -526,7 +632,8 @@ mod tests {
use tonic_lnd::{
lnrpc::{
AddInvoiceResponse, ChannelEdge, GetInfoResponse, HtlcAttempt, LightningNode,
ListPeersResponse, NodeAddress, NodeInfo, PayReq, QueryRoutesResponse, Route,
ListChannelsResponse, ListPeersResponse, NodeAddress, NodeInfo, PayReq,
QueryRoutesResponse, Route,
},
tonic::Status,
};
@ -687,7 +794,24 @@ mod tests {
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path(
let response = create_reply_path_for_outgoing_payments(
connector_mock,
receiver_node_id,
message_context,
&MessengerUtilities::new([42; 32]),
)
.await;
assert!(response.is_ok());
let mut connector_mock = MockTestPeerConnector::new();
connector_mock
.expect_list_active_public_channels()
.returning(|| Ok(ListChannelsResponse { channels: vec![] }));
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path_for_offer_creation(
connector_mock,
receiver_node_id,
message_context,
@ -698,7 +822,7 @@ mod tests {
}
#[tokio::test]
async fn test_create_reply_path() {
async fn test_reply_path_for_outgoing_payments() {
let mut connector_mock = MockTestPeerConnector::new();
connector_mock.expect_list_peers().returning(|| {
@ -729,7 +853,54 @@ mod tests {
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path(
let response = create_reply_path_for_outgoing_payments(
connector_mock,
receiver_node_id,
message_context,
&MessengerUtilities::new([42; 32]),
)
.await;
assert!(response.is_ok());
}
#[tokio::test]
async fn test_create_reply_path_for_offer_creation() {
let mut connector_mock = MockTestPeerConnector::new();
connector_mock
.expect_list_active_public_channels()
.returning(|| {
let channel = tonic_lnd::lnrpc::Channel {
remote_pubkey: get_pubkeys()[0].clone(),
..Default::default()
};
Ok(ListChannelsResponse {
channels: vec![channel],
})
});
connector_mock.expect_get_node_info().returning(|_, _| {
let node_addr = NodeAddress {
network: String::from("regtest"),
addr: String::from("127.0.0.1"),
};
let node = Some(LightningNode {
addresses: vec![node_addr],
..Default::default()
});
let node_info = NodeInfo {
node,
..Default::default()
};
Ok(node_info)
});
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path_for_offer_creation(
connector_mock,
receiver_node_id,
message_context,
@ -749,7 +920,24 @@ mod tests {
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path(
let response = create_reply_path_for_outgoing_payments(
connector_mock,
receiver_node_id,
message_context,
&MessengerUtilities::new([42; 32]),
)
.await;
assert!(response.is_err());
let mut connector_mock = MockTestPeerConnector::new();
connector_mock
.expect_list_active_public_channels()
.returning(|| Err(Status::unknown("unknown error")));
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path_for_offer_creation(
connector_mock,
receiver_node_id,
message_context,
@ -760,7 +948,7 @@ mod tests {
}
#[tokio::test]
async fn test_create_reply_path_not_advertised() {
async fn test_create_reply_path_for_outgoing_payments_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 it has no public channels. This should return
// a blinded path with only one hop.
@ -786,7 +974,7 @@ mod tests {
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path(
let response = create_reply_path_for_outgoing_payments(
connector_mock,
receiver_node_id,
message_context,
@ -851,7 +1039,7 @@ mod tests {
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path(
let response = create_reply_path_for_outgoing_payments(
connector_mock,
receiver_node_id,
message_context,
@ -864,6 +1052,124 @@ mod tests {
assert!(hops.len() == 2);
}
#[tokio::test]
async fn test_create_reply_path_for_offer_creation_not_advertised() {
// First lets test that the one that we are trying to connect to, does not have public
// channels This should return a blinded path with only one hop.
let mut connector_mock = MockTestPeerConnector::new();
connector_mock
.expect_list_active_public_channels()
.returning(|| Ok(ListChannelsResponse { channels: vec![] }));
connector_mock.expect_get_node_info().returning(|_, _| {
let node_addr = NodeAddress {
network: String::from("regtest"),
addr: String::from("127.0.0.1"),
};
let node = Some(LightningNode {
addresses: vec![node_addr],
..Default::default()
});
let node_info = NodeInfo {
node,
..Default::default()
};
Ok(node_info)
});
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path_for_offer_creation(
connector_mock,
receiver_node_id,
message_context,
&MessengerUtilities::new([42; 32]),
)
.await;
assert!(response.is_ok());
let reply_path = response.unwrap();
let hops = reply_path.first().unwrap().blinded_hops();
assert!(hops.len() == 1);
// Now let's test that we are connected to two channels.
// One channel isn't advertised (i.e. it has no public channels). But the second is. This
// should succeed.
let mut connector_mock = MockTestPeerConnector::new();
// Only second channel is advertised.
connector_mock
.expect_list_active_public_channels()
.returning(|| {
let keys = get_pubkeys();
let channel2 = tonic_lnd::lnrpc::Channel {
remote_pubkey: keys[1].clone(),
chan_id: 10,
..Default::default()
};
Ok(ListChannelsResponse {
channels: vec![channel2],
})
});
connector_mock.expect_get_node_info().returning(|_, _| {
let node_addr = NodeAddress {
network: String::from("regtest"),
addr: String::from("127.0.0.1"),
};
let mut features = std::collections::HashMap::new();
// Add onion message feature (feature bit 38/39)
features.insert(
38,
tonic_lnd::lnrpc::Feature {
name: "onion_messages_optional".to_string(),
is_required: false,
is_known: true,
},
);
let node = Some(LightningNode {
addresses: vec![node_addr],
features,
..Default::default()
});
let node_info = NodeInfo {
node,
channels: vec![
ChannelEdge {
channel_id: 10,
..Default::default()
},
ChannelEdge {
channel_id: 20,
..Default::default()
},
],
..Default::default()
};
Ok(node_info)
});
let receiver_node_id = PublicKey::from_str(&get_pubkeys()[0]).unwrap();
let message_context = get_message_context();
let response = create_reply_path_for_offer_creation(
connector_mock,
receiver_node_id,
message_context,
&MessengerUtilities::new([42; 32]),
)
.await;
assert!(response.is_ok());
let reply_path = response.unwrap();
let hops = reply_path.first().unwrap().blinded_hops();
assert!(hops.len() == 2);
}
fn get_blinded_payment_path() -> BlindedPaymentPath {
let entropy_source = MessengerUtilities::new([42; 32]);
let secp_ctx = Secp256k1::new();
@ -1080,6 +1386,36 @@ mod tests {
Ok(ListPeersResponse { peers: vec![peer] })
});
creator_mock
.expect_list_active_public_channels()
.returning(|| {
let channel = tonic_lnd::lnrpc::Channel {
remote_pubkey: get_pubkeys()[0].clone(),
..Default::default()
};
Ok(ListChannelsResponse {
channels: vec![channel],
})
});
creator_mock.expect_get_node_info().returning(|_, _| {
let node_addr = NodeAddress {
network: String::from("regtest"),
addr: String::from("127.0.0.1"),
};
let node = Some(LightningNode {
addresses: vec![node_addr],
..Default::default()
});
let node_info = NodeInfo {
node,
..Default::default()
};
Ok(node_info)
});
creator_mock
}

View file

@ -15,7 +15,8 @@ mod lnd_requests;
mod parse;
pub(crate) use lnd_requests::connect_to_peer_with_retry;
pub use lnd_requests::create_reply_path;
pub use lnd_requests::create_reply_path_for_offer_creation;
pub use lnd_requests::create_reply_path_for_outgoing_payments;
pub use parse::{decode, get_destination, validate_amount};
#[derive(Debug)]
@ -69,6 +70,8 @@ pub enum OfferError {
ParseInvoiceFailure(DecodeError),
/// Failed to encode invoice.
EncodeInvoiceFailure(BitcoinIoError),
/// Cannot list channels.
ListChannelsFailure(LndStatus),
}
impl OfferError {
@ -98,6 +101,7 @@ impl OfferError {
OfferError::InvoiceTimeout(_) => "INVOICE_TIMEOUT",
OfferError::IntroductionNodeNotFound => "INTRODUCTION_NODE_NOT_FOUND",
OfferError::GetChannelInfo(_) => "GET_CHANNEL_INFO",
OfferError::ListChannelsFailure(_) => "LIST_CHANNELS_FAILURE",
}
}
@ -173,6 +177,7 @@ impl Display for OfferError {
OfferError::EncodeInvoiceFailure(e) => {
write!(f, "Failed to encode invoice to hex format. Error: {e:?}")
}
OfferError::ListChannelsFailure(e) => write!(f, "Error listing channels: {e:?}"),
}
}
}

View file

@ -1135,6 +1135,7 @@ mod tests {
async fn list_peers(&mut self) -> Result<tonic_lnd::lnrpc::ListPeersResponse, Status>;
async fn get_node_info(&mut self, pub_key: String, include_channels: bool) -> Result<tonic_lnd::lnrpc::NodeInfo, Status>;
async fn connect_peer(&mut self, node_id: String, addr: String) -> Result<(), Status>;
async fn list_active_public_channels(&mut self) -> Result<tonic_lnd::lnrpc::ListChannelsResponse, Status>;
}
}

View file

@ -20,7 +20,7 @@ use lightning::offers::offer::Quantity;
use lightning::onion_message::messenger::Destination;
use lndk::lnd::validate_lnd_creds;
use lndk::offers::handler::{CreateOfferParams, OfferHandler, PayOfferParams};
use lndk::offers::{create_reply_path, OfferError};
use lndk::offers::{create_reply_path_for_outgoing_payments, OfferError};
use lndk::onion_messenger::MessengerUtilities;
use lndk::{setup_logger, LifecycleSignals};
use std::net::SocketAddr;
@ -498,7 +498,7 @@ async fn test_reply_path_unannounced_peers() {
// private channel and as such, is an unadvertised node. Because of that, create_reply_path
// should not use ldk2 as an introduction node and should return a reply path directly to
// itself.
let reply_path = create_reply_path(
let reply_path = create_reply_path_for_outgoing_payments(
lnd.client.clone().unwrap().lightning().clone(),
lnd_pubkey,
offer_context,
@ -541,7 +541,7 @@ async fn test_reply_path_announced_peers() {
// public channel and as such, is indeed an advertised node. Because of this, we make sure
// create_reply_path produces a path of length two with ldk2 as the introduction node, as we
// expected.
let reply_path = create_reply_path(
let reply_path = create_reply_path_for_outgoing_payments(
lnd.client.clone().unwrap().lightning().clone(),
lnd_pubkey,
offer_context,
@ -1056,10 +1056,21 @@ async fn pay_offer_and_wait_for_payment(
offer: Offer,
mut lnd_client: Client,
) -> Result<(), ()> {
let payment = ldk.pay_offer(offer, None).await;
assert!(payment.is_ok());
// Wait for the payment to complete on ldk side.
common::wait_for_ldk_payment_completion(ldk, Duration::from_secs(30)).await?;
let mut retries = 0;
let max_retries = 3;
let delay = Duration::from_secs(2);
while retries < max_retries {
tokio::time::sleep(delay).await;
let payment = ldk.pay_offer(offer.clone(), None).await;
assert!(payment.is_ok());
// Wait for the payment to complete on ldk side.
match common::wait_for_ldk_payment_completion(ldk, Duration::from_secs(10)).await {
Ok(_) => break,
_ => println!("Payment timedout, trying again"),
};
retries += 1;
}
// Wait for the payment to complete on lnd side.
common::wait_for_lnd_payment_completion(&mut lnd_client, Duration::from_secs(10)).await?;
Ok(())
@ -1074,6 +1085,88 @@ async fn test_receive_payment_from_offer() {
let (ldk1_pubkey, _ldk2_pubkey, _lnd_pubkey) =
common::connect_network(&ldk1, &ldk2, true, true, &mut lnd, &bitcoind).await;
// Network topology with 2 LDK nodes and 1 LND node and 3 open channels:
//
// LND
// /|\
// / | \
// LDK1────────LDK2 ─────────────────
let ldk2_fund_addr = ldk2.bitcoind_client.get_new_address().await;
let ldk2_addr_string = ldk2_fund_addr.to_string();
let ldk2_addr = bitcoincore_rpc::bitcoin::Address::from_str(&ldk2_addr_string)
.unwrap()
.require_network(bitcoincore_rpc::bitcoin::Network::Regtest)
.unwrap();
bitcoind
.node
.client
.generate_to_address(6, &ldk2_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
let lnd_addr = lnd
.address
.replace("localhost", "127.0.0.1")
.replace("https://", "");
ldk2.open_channel(
_lnd_pubkey,
SocketAddr::from_str(&lnd_addr).unwrap(),
200000,
10000000,
true,
)
.await
.unwrap();
lnd.wait_for_graph_sync().await;
bitcoind
.node
.client
.generate_to_address(20, &ldk2_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
let ldk2_fund_addr = ldk2.bitcoind_client.get_new_address().await;
let ldk2_addr_string = ldk2_fund_addr.to_string();
let ldk2_addr = bitcoincore_rpc::bitcoin::Address::from_str(&ldk2_addr_string)
.unwrap()
.require_network(bitcoincore_rpc::bitcoin::Network::Regtest)
.unwrap();
bitcoind
.node
.client
.generate_to_address(6, &ldk2_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
ldk2.open_channel(
_lnd_pubkey,
SocketAddr::from_str(&lnd_addr).unwrap(),
200000,
10000000,
true,
)
.await
.unwrap();
lnd.wait_for_graph_sync().await;
bitcoind
.node
.client
.generate_to_address(20, &ldk2_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
lnd.wait_for_nodes_addresses(&[&ldk1, &ldk2]).await;
let log_file = Some(lndk_dir.join(format!("lndk-logs.txt")));
setup_logger(None, log_file).unwrap();
@ -1116,16 +1209,230 @@ async fn test_receive_payment_from_offer() {
expiry: None,
};
lnd.wait_for_addresses_to_sync(ldk1_pubkey).await;
let offer = handler.create_offer(create_offer_params).await;
assert!(offer.is_ok());
let offer = offer.unwrap();
lnd.wait_for_addresses_to_sync(ldk1_pubkey).await;
// We check that according to the current topology even though there are 3 open channels, it is
// only one blinded path available.
assert!(offer.paths().len() == 1);
select! {
val = messenger.run(lndk_cfg, Arc::clone(&handler)) => {
panic!("lndk should not have completed first {:?}", val);
},
res = pay_offer_and_wait_for_payment(&ldk1, offer, lnd.client.clone().unwrap()) => {
log::info!("res: {:?}", res);
assert!(res.is_ok());
shutdown.trigger();
ldk1.stop().await;
ldk2.stop().await;
}
};
}
#[tokio::test(flavor = "multi_thread")]
// Test that we can receive a payment from an offer which constains multiple paths.
async fn test_receive_payment_from_offer_with_multiple_blinded_paths() {
let test_name = "test_receive_payment_from_offer_with_multiple_blinded_paths";
let (bitcoind, mut lnd, ldk1, ldk2, lndk_dir, ldk_test_dir) =
common::setup_test_infrastructure(test_name).await;
let (ldk1_pubkey, ldk2_pubkey, _lnd_pubkey) =
common::connect_network(&ldk1, &ldk2, true, true, &mut lnd, &bitcoind).await;
let log_file = Some(lndk_dir.join(format!("lndk-logs.txt")));
setup_logger(None, log_file).unwrap();
// Network topology with 4 LDK nodes and 1 LND node:
//
// LDK2 ────────────────────LND
// / | \ /|\
// / | \ / | \
// LDK1 | LDK3 ─────────────── | \
// | / | \
// | / | \
// LDK4 ────────────────────────────
// Second blinded path.
let ldk3 = common::setup_ldk_node(&bitcoind, 3, &ldk_test_dir, test_name).await;
// Third blinded path.
let ldk4 = common::setup_ldk_node(&bitcoind, 4, &ldk_test_dir, test_name).await;
let (ldk3_pubkey, addr_3) = ldk3.get_node_info();
let (ldk4_pubkey, addr_4) = ldk4.get_node_info();
let (_, addr_2) = ldk2.get_node_info();
let lnd_info = lnd.get_info().await;
let lnd_pubkey = PublicKey::from_str(&lnd_info.identity_pubkey).unwrap();
lnd.connect_to_peer(ldk3_pubkey, addr_3).await;
lnd.connect_to_peer(ldk4_pubkey, addr_4).await;
ldk2.connect_to_peer(ldk4_pubkey, addr_4).await.unwrap();
ldk2.connect_to_peer(ldk3_pubkey, addr_3).await.unwrap();
let ldk3_fund_addr = ldk3.bitcoind_client.get_new_address().await;
let ldk3_addr_string = ldk3_fund_addr.to_string();
let ldk3_addr = bitcoincore_rpc::bitcoin::Address::from_str(&ldk3_addr_string)
.unwrap()
.require_network(bitcoincore_rpc::bitcoin::Network::Regtest)
.unwrap();
let ldk4_fund_addr = ldk4.bitcoind_client.get_new_address().await;
let ldk4_addr_string = ldk4_fund_addr.to_string();
let ldk4_addr = bitcoincore_rpc::bitcoin::Address::from_str(&ldk4_addr_string)
.unwrap()
.require_network(bitcoincore_rpc::bitcoin::Network::Regtest)
.unwrap();
bitcoind
.node
.client
.generate_to_address(6, &ldk3_addr)
.unwrap();
bitcoind
.node
.client
.generate_to_address(6, &ldk4_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
ldk3.open_channel(ldk2_pubkey, addr_2, 200000, 10000000, true)
.await
.unwrap();
ldk4.open_channel(ldk2_pubkey, addr_2, 200000, 10000000, true)
.await
.unwrap();
lnd.wait_for_graph_sync().await;
bitcoind
.node
.client
.generate_to_address(6, &ldk3_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
let lnd_addr = lnd
.address
.replace("localhost", "127.0.0.1")
.replace("https://", "");
ldk3.open_channel(
lnd_pubkey,
SocketAddr::from_str(&lnd_addr).unwrap(),
200000,
10000000,
true,
)
.await
.unwrap();
lnd.wait_for_graph_sync().await;
bitcoind
.node
.client
.generate_to_address(6, &ldk3_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
ldk4.open_channel(
lnd_pubkey,
SocketAddr::from_str(&lnd_addr).unwrap(),
200000,
10000000,
true,
)
.await
.unwrap();
lnd.wait_for_graph_sync().await;
bitcoind
.node
.client
.generate_to_address(6, &ldk3_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
ldk3.open_channel(ldk4_pubkey, addr_4, 200000, 10000000, true)
.await
.unwrap();
lnd.wait_for_graph_sync().await;
bitcoind
.node
.client
.generate_to_address(20, &ldk3_addr)
.unwrap();
lnd.wait_for_chain_sync().await;
lnd.wait_for_nodes_addresses(&[&ldk1, &ldk2, &ldk3, &ldk4])
.await;
let (shutdown, listener) = triggered::trigger();
let creds = validate_lnd_creds(
Some(PathBuf::from_str(&lnd.cert_path).unwrap()),
None,
Some(PathBuf::from_str(&lnd.macaroon_path).unwrap()),
None,
)
.unwrap();
let lnd_cfg = lndk::lnd::LndCfg::new(lnd.address.clone(), creds);
let signals = LifecycleSignals {
shutdown: shutdown.clone(),
listener,
};
let lndk_cfg = lndk::Cfg {
lnd: lnd_cfg,
signals,
skip_version_check: false,
rate_limit_count: 10,
rate_limit_period_secs: 1,
};
let handler = Arc::new(OfferHandler::new(
None,
None,
Some(lnd.client.clone().unwrap()),
));
let messenger = lndk::LndkOnionMessenger::new();
let create_offer_params = CreateOfferParams {
client: lnd.client.clone().unwrap(),
amount_msats: 20_000,
chain: Network::Regtest,
description: None,
issuer: None,
quantity: None,
expiry: None,
};
lnd.wait_for_addresses_to_sync(ldk1_pubkey).await;
let offer = handler.create_offer(create_offer_params).await;
assert!(offer.is_ok());
let offer = offer.unwrap();
// we check that according the current topology, there are 3 blinded paths available.
assert!(offer.paths().len() == 3);
select! {
val = messenger.run(lndk_cfg, Arc::clone(&handler)) => {
panic!("lndk should not have completed first {:?}", val);
},
res = pay_offer_and_wait_for_payment(&ldk1, offer, lnd.client.clone().unwrap()) => {
log::info!("res: {:?}", res);
assert!(res.is_ok());
shutdown.trigger();
ldk1.stop().await;