mirror of
https://github.com/lndk-org/lndk.git
synced 2026-08-13 12:33:05 +02:00
Merge pull request #235 from f3r10/2025-09-better-peer-selection-for-offers
Better peer selection for offers
This commit is contained in:
commit
f7c5a03af9
7 changed files with 696 additions and 28 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue