Skip to content

Commit 95c8e73

Browse files
committed
Add end-to-end test for HRN resolution
Introduce a comprehensive test case to verify the full lifecycle of a payment initiated via a Human Readable Name (HRN). This test ensures that the integration between HRN parsing, BIP 353 resolution, and BOLT12 offer execution is functioning correctly within the node. By asserting that an encoded URI can be successfully resolved to a valid offer and subsequently paid, we validate the reliability of the resolution pipeline and ensure that recent architectural changes to the OnionMessenger and Node configuration work in unison.
1 parent 080b233 commit 95c8e73

File tree

6 files changed

+126
-17
lines changed

6 files changed

+126
-17
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ check-cfg = [
125125
"cfg(cln_test)",
126126
"cfg(lnd_test)",
127127
"cfg(cycle_tests)",
128+
"cfg(hrn_tests)",
128129
]
129130

130131
[[bench]]

src/ffi/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ impl std::fmt::Display for Offer {
298298
/// This struct can also be used for LN-Address recipients.
299299
///
300300
/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack
301+
#[derive(Eq, Hash, PartialEq)]
301302
pub struct HumanReadableName {
302303
pub(crate) inner: LdkHumanReadableName,
303304
}

src/payment/unified.rs

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ use bitcoin_payment_instructions::amount::Amount as BPIAmount;
2626
use bitcoin_payment_instructions::{PaymentInstructions, PaymentMethod};
2727
use lightning::ln::channelmanager::PaymentId;
2828
use lightning::offers::offer::Offer;
29-
use lightning::onion_message::dns_resolution::HumanReadableName;
3029
use lightning::routing::router::RouteParametersConfig;
3130
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
3231

@@ -40,6 +39,11 @@ use crate::Config;
4039

4140
type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;
4241

42+
#[cfg(not(feature = "uniffi"))]
43+
type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName;
44+
#[cfg(feature = "uniffi")]
45+
type HumanReadableName = crate::ffi::HumanReadableName;
46+
4347
#[derive(Debug, Clone)]
4448
struct Extras {
4549
bolt11_invoice: Option<Bolt11Invoice>,
@@ -159,15 +163,30 @@ impl UnifiedPayment {
159163
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki
160164
pub async fn send(
161165
&self, uri_str: &str, amount_msat: Option<u64>,
162-
route_parameters: Option<RouteParametersConfig>,
166+
route_parameters: Option<RouteParametersConfig>, #[cfg(hrn_tests)] test_offer: &Offer,
163167
) -> Result<UnifiedPaymentResult, Error> {
164168
let resolver = self.hrn_resolver.as_ref().clone().ok_or_else(|| {
165169
log_error!(self.logger, "No HRN resolver configured. Cannot resolve HRNs.");
166170
Error::HrnResolverNotConfigured
167171
})?;
168172

173+
let target_network;
174+
175+
target_network = if let Ok(_) = HumanReadableName::from_encoded(uri_str) {
176+
#[cfg(hrn_tests)]
177+
{
178+
bitcoin::Network::Bitcoin
179+
}
180+
#[cfg(not(hrn_tests))]
181+
{
182+
self.config.network
183+
}
184+
} else {
185+
self.config.network
186+
};
187+
169188
let parse_fut =
170-
PaymentInstructions::parse(uri_str, self.config.network, resolver.as_ref(), false);
189+
PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false);
171190

172191
let instructions =
173192
tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut)
@@ -231,20 +250,26 @@ impl UnifiedPayment {
231250
for method in sorted_payment_methods {
232251
match method {
233252
PaymentMethod::LightningBolt12(offer) => {
234-
let offer = maybe_wrap(offer.clone());
235-
236-
let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
237-
let hrn = maybe_wrap(hrn.clone());
238-
self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn))
239-
} else if let Some(amount_msat) = amount_msat {
240-
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
241-
} else {
242-
self.bolt12_payment.send(&offer, None, None, route_parameters)
243-
}
244-
.map_err(|e| {
245-
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
246-
e
247-
});
253+
#[cfg(not(hrn_tests))]
254+
let offer = maybe_wrap(offer);
255+
256+
#[cfg(hrn_tests)]
257+
let offer = maybe_wrap(test_offer.clone());
258+
259+
let payment_result = {
260+
if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
261+
let hrn = maybe_wrap(hrn.clone());
262+
self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn))
263+
} else if let Some(amount_msat) = amount_msat {
264+
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
265+
} else {
266+
self.bolt12_payment.send(&offer, None, None, route_parameters)
267+
}
268+
.map_err(|e| {
269+
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
270+
e
271+
})
272+
};
248273

249274
if let Ok(payment_id) = payment_result {
250275
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });

tests/common/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ pub(crate) fn setup_two_nodes_with_store(
406406
println!("\n== Node B ==");
407407
let mut config_b = random_config(anchor_channels);
408408
config_b.store_type = store_type;
409+
409410
if allow_0conf {
410411
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
411412
}

tests/integration_tests_hrn.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// This file is Copyright its original authors, visible in version control history.
2+
//
3+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5+
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6+
// accordance with one or both of these licenses.
7+
8+
#![cfg(hrn_tests)]
9+
10+
mod common;
11+
12+
use bitcoin::Amount;
13+
use common::{
14+
expect_channel_ready_event, expect_payment_successful_event, generate_blocks_and_wait,
15+
open_channel, premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_nodes,
16+
TestChainSource,
17+
};
18+
use ldk_node::payment::UnifiedPaymentResult;
19+
use ldk_node::Event;
20+
use lightning::ln::channelmanager::PaymentId;
21+
22+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
23+
async fn unified_send_to_hrn() {
24+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
25+
let chain_source = TestChainSource::Esplora(&electrsd);
26+
27+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
28+
29+
let address_a = node_a.onchain_payment().new_address().unwrap();
30+
let premined_sats = 5_000_000;
31+
32+
premine_and_distribute_funds(
33+
&bitcoind.client,
34+
&electrsd.client,
35+
vec![address_a],
36+
Amount::from_sat(premined_sats),
37+
)
38+
.await;
39+
40+
node_a.sync_wallets().unwrap();
41+
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await;
42+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
43+
44+
node_a.sync_wallets().unwrap();
45+
node_b.sync_wallets().unwrap();
46+
47+
expect_channel_ready_event!(node_a, node_b.node_id());
48+
expect_channel_ready_event!(node_b, node_a.node_id());
49+
50+
// Sleep until we broadcast a node announcement.
51+
while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() {
52+
std::thread::sleep(std::time::Duration::from_millis(10));
53+
}
54+
55+
let test_offer = node_b.bolt12_payment().receive(1000000, "test offer", None, None).unwrap();
56+
57+
// Sleep one more sec to make sure the node announcement propagates.
58+
std::thread::sleep(std::time::Duration::from_secs(1));
59+
60+
let hrn_str = "matt@mattcorallo.com";
61+
62+
let offer_payment_id: PaymentId =
63+
match node_a.unified_payment().send(&hrn_str, Some(1000000), None, &test_offer).await {
64+
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
65+
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
66+
payment_id
67+
},
68+
Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => {
69+
panic!("Expected Bolt12 payment but got Bolt11");
70+
},
71+
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
72+
panic!("Expected Bolt12 payment but got On-chain transaction");
73+
},
74+
Err(e) => {
75+
panic!("Expected Bolt12 payment but got error: {:?}", e);
76+
},
77+
};
78+
79+
expect_payment_successful_event!(node_a, Some(offer_payment_id), None);
80+
}

tests/integration_tests_rust.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use ldk_node::payment::{
3333
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
3434
UnifiedPaymentResult,
3535
};
36+
3637
use ldk_node::{Builder, Event, NodeError};
3738
use lightning::ln::channelmanager::PaymentId;
3839
use lightning::routing::gossip::{NodeAlias, NodeId};

0 commit comments

Comments
 (0)