Skip to content

Commit 6cd5cef

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 e83f2cf commit 6cd5cef

File tree

4 files changed

+171
-14
lines changed

4 files changed

+171
-14
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ check-cfg = [
128128
"cfg(cln_test)",
129129
"cfg(lnd_test)",
130130
"cfg(cycle_tests)",
131+
"cfg(hrn_tests)",
131132
]
132133

133134
[[bench]]

src/payment/unified.rs

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

@@ -41,6 +40,16 @@ use crate::Config;
4140

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

43+
#[cfg(not(feature = "uniffi"))]
44+
type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName;
45+
#[cfg(feature = "uniffi")]
46+
type HumanReadableName = crate::ffi::HumanReadableName;
47+
48+
#[cfg(not(feature = "uniffi"))]
49+
type Offer = LdkOffer;
50+
#[cfg(feature = "uniffi")]
51+
type Offer = Arc<crate::ffi::Offer>;
52+
4453
#[derive(Debug, Clone)]
4554
struct Extras {
4655
bolt11_invoice: Option<Bolt11Invoice>,
@@ -67,6 +76,8 @@ pub struct UnifiedPayment {
6776
config: Arc<Config>,
6877
logger: Arc<Logger>,
6978
hrn_resolver: Arc<HRNResolver>,
79+
#[cfg(hrn_tests)]
80+
test_offer: std::sync::Mutex<Option<Offer>>,
7081
}
7182

7283
impl UnifiedPayment {
@@ -75,7 +86,16 @@ impl UnifiedPayment {
7586
bolt12_payment: Arc<Bolt12Payment>, config: Arc<Config>, logger: Arc<Logger>,
7687
hrn_resolver: Arc<HRNResolver>,
7788
) -> Self {
78-
Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger, hrn_resolver }
89+
Self {
90+
onchain_payment,
91+
bolt11_invoice,
92+
bolt12_payment,
93+
config,
94+
logger,
95+
hrn_resolver,
96+
#[cfg(hrn_tests)]
97+
test_offer: std::sync::Mutex::new(None),
98+
}
7999
}
80100
}
81101

@@ -116,7 +136,7 @@ impl UnifiedPayment {
116136

117137
let bolt12_offer =
118138
match self.bolt12_payment.receive_inner(amount_msats, description, None, None) {
119-
Ok(offer) => Some(offer),
139+
Ok(offer) => Some(maybe_wrap(offer)),
120140
Err(e) => {
121141
log_error!(self.logger, "Failed to create offer: {}", e);
122142
None
@@ -167,15 +187,26 @@ impl UnifiedPayment {
167187
route_parameters: Option<RouteParametersConfig>,
168188
) -> Result<UnifiedPaymentResult, Error> {
169189
let resolver;
190+
let target_network;
170191

171192
if let Ok(_) = HumanReadableName::from_encoded(uri_str) {
172193
resolver = Arc::clone(&self.hrn_resolver);
194+
195+
#[cfg(hrn_tests)]
196+
{
197+
target_network = bitcoin::Network::Bitcoin;
198+
}
199+
#[cfg(not(hrn_tests))]
200+
{
201+
target_network = self.config.network;
202+
}
173203
} else {
174204
resolver = Arc::new(HRNResolver::Dummy(DummyHrnResolver));
205+
target_network = self.config.network;
175206
}
176207

177208
let parse_fut =
178-
PaymentInstructions::parse(uri_str, self.config.network, resolver.as_ref(), false);
209+
PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false);
179210

180211
let instructions =
181212
tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut)
@@ -238,9 +269,30 @@ impl UnifiedPayment {
238269

239270
for method in sorted_payment_methods {
240271
match method {
241-
PaymentMethod::LightningBolt12(offer) => {
242-
let offer = maybe_wrap(offer.clone());
243-
272+
PaymentMethod::LightningBolt12(_offer) => {
273+
#[cfg(not(hrn_tests))]
274+
let offer = maybe_wrap(_offer.clone());
275+
276+
#[cfg(hrn_tests)]
277+
let offer = {
278+
let test_offer_guard = self.test_offer.lock().map_err(|e| {
279+
log_error!(
280+
self.logger,
281+
"Failed to lock test_offer due to poisoning: {:?}",
282+
e
283+
);
284+
Error::PaymentSendingFailed
285+
})?;
286+
287+
match &*test_offer_guard {
288+
Some(o) => o.clone(),
289+
None => {
290+
log_error!(self.logger, "test_offer was None during HRN test");
291+
return Err(Error::PaymentSendingFailed);
292+
},
293+
}
294+
};
295+
244296
let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
245297
let hrn = maybe_wrap(hrn.clone());
246298
self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn))
@@ -293,6 +345,16 @@ impl UnifiedPayment {
293345
log_error!(self.logger, "Payable methods not found in URI");
294346
Err(Error::PaymentSendingFailed)
295347
}
348+
349+
/// Sets a test offer to be used in the `send` method when the `hrn_tests` config flag is enabled.
350+
/// This is necessary to test sending Bolt12 payments via the unified payment handler in HRN tests,
351+
/// as we cannot rely on the offer being present in the parsed URI.
352+
#[cfg(hrn_tests)]
353+
pub fn set_test_offer(&self, _offer: Offer) {
354+
let _ = self.test_offer.lock().map(|mut guard| *guard = Some(_offer)).map_err(|e| {
355+
log_error!(self.logger, "Failed to set test offer due to poisoned lock: {:?}", e)
356+
});
357+
}
296358
}
297359

298360
/// Represents the result of a payment made using a [BIP 21] URI or a [BIP 353] Human-Readable Name.
@@ -400,9 +462,10 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
400462
"lno" => {
401463
let bolt12_value =
402464
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
403-
let offer =
404-
bolt12_value.parse::<Offer>().map_err(|_| Error::UriParameterParsingFailed)?;
405-
self.bolt12_offer = Some(offer);
465+
let offer = bolt12_value
466+
.parse::<LdkOffer>()
467+
.map_err(|_| Error::UriParameterParsingFailed)?;
468+
self.bolt12_offer = Some(maybe_wrap(offer));
406469
Ok(bip21::de::ParamKind::Known)
407470
},
408471
_ => Ok(bip21::de::ParamKind::Unknown),
@@ -425,7 +488,7 @@ mod tests {
425488
use bitcoin::address::NetworkUnchecked;
426489
use bitcoin::{Address, Network};
427490

428-
use super::{Amount, Bolt11Invoice, Extras, Offer};
491+
use super::{maybe_wrap, Amount, Bolt11Invoice, Extras, LdkOffer};
429492

430493
#[test]
431494
fn parse_uri() {
@@ -479,7 +542,7 @@ mod tests {
479542
}
480543

481544
if let Some(offer) = parsed_uri_with_offer.extras.bolt12_offer {
482-
assert_eq!(offer, Offer::from_str(expected_bolt12_offer_2).unwrap());
545+
assert_eq!(offer, maybe_wrap(LdkOffer::from_str(expected_bolt12_offer_2).unwrap()));
483546
} else {
484547
panic!("No offer found.");
485548
}

tests/common/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ use bitcoin::{
2727
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2828
use electrsd::{corepc_node, ElectrsD};
2929
use electrum_client::ElectrumApi;
30-
use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig};
30+
use ldk_node::config::{
31+
AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig,
32+
HumanReadableNamesConfig,
33+
};
3134
use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy};
3235
use ldk_node::io::sqlite_store::SqliteStore;
3336
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
@@ -400,11 +403,18 @@ pub(crate) fn setup_two_nodes_with_store(
400403
println!("== Node A ==");
401404
let mut config_a = random_config(anchor_channels);
402405
config_a.store_type = store_type;
406+
407+
if cfg!(hrn_tests) {
408+
config_a.node_config.hrn_config =
409+
HumanReadableNamesConfig { resolution_config: HRNResolverConfig::Blip32 };
410+
}
411+
403412
let node_a = setup_node(chain_source, config_a);
404413

405414
println!("\n== Node B ==");
406415
let mut config_b = random_config(anchor_channels);
407416
config_b.store_type = store_type;
417+
408418
if allow_0conf {
409419
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
410420
}

tests/integration_tests_hrn.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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, random_chain_source, setup_bitcoind_and_electrsd,
16+
setup_two_nodes, 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 = random_chain_source(&bitcoind, &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+
// Wait until node_b broadcasts a node announcement
51+
while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() {
52+
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
53+
}
54+
55+
// Sleep to make sure the node announcement propagates
56+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
57+
58+
let test_offer = node_b.bolt12_payment().receive(1000000, "test offer", None, None).unwrap();
59+
60+
let hrn_str = "matt@mattcorallo.com";
61+
62+
let unified_handler = node_a.unified_payment();
63+
unified_handler.set_test_offer(test_offer);
64+
65+
let offer_payment_id: PaymentId =
66+
match unified_handler.send(&hrn_str, Some(1000000), None).await {
67+
Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => {
68+
println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id);
69+
payment_id
70+
},
71+
Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => {
72+
panic!("Expected Bolt12 payment but got Bolt11");
73+
},
74+
Ok(UnifiedPaymentResult::Onchain { txid: _ }) => {
75+
panic!("Expected Bolt12 payment but got On-chain transaction");
76+
},
77+
Err(e) => {
78+
panic!("Expected Bolt12 payment but got error: {:?}", e);
79+
},
80+
};
81+
82+
expect_payment_successful_event!(node_a, Some(offer_payment_id), None);
83+
}

0 commit comments

Comments
 (0)