Skip to content

Commit 3600e47

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 6af92d7 commit 3600e47

4 files changed

Lines changed: 187 additions & 18 deletions

File tree

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: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ use bitcoin::{Amount, Txid};
2525
use bitcoin_payment_instructions::amount::Amount as BPIAmount;
2626
use bitcoin_payment_instructions::{PaymentInstructions, PaymentMethod};
2727
use lightning::ln::channelmanager::PaymentId;
28-
use lightning::offers::offer::Offer;
29-
use lightning::onion_message::dns_resolution::HumanReadableName;
28+
use lightning::offers::offer::Offer as LdkOffer;
3029
use lightning::routing::router::RouteParametersConfig;
3130
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
3231

@@ -40,6 +39,16 @@ 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+
47+
#[cfg(not(feature = "uniffi"))]
48+
type Offer = LdkOffer;
49+
#[cfg(feature = "uniffi")]
50+
type Offer = Arc<crate::ffi::Offer>;
51+
4352
#[derive(Debug, Clone)]
4453
struct Extras {
4554
bolt11_invoice: Option<Bolt11Invoice>,
@@ -66,6 +75,8 @@ pub struct UnifiedPayment {
6675
config: Arc<Config>,
6776
logger: Arc<Logger>,
6877
hrn_resolver: Arc<HRNResolver>,
78+
#[cfg(hrn_tests)]
79+
test_offer: std::sync::Mutex<Option<Offer>>,
6980
}
7081

7182
impl UnifiedPayment {
@@ -74,7 +85,16 @@ impl UnifiedPayment {
7485
bolt12_payment: Arc<Bolt12Payment>, config: Arc<Config>, logger: Arc<Logger>,
7586
hrn_resolver: Arc<HRNResolver>,
7687
) -> Self {
77-
Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger, hrn_resolver }
88+
Self {
89+
onchain_payment,
90+
bolt11_invoice,
91+
bolt12_payment,
92+
config,
93+
logger,
94+
hrn_resolver,
95+
#[cfg(hrn_tests)]
96+
test_offer: std::sync::Mutex::new(None),
97+
}
7898
}
7999
}
80100

@@ -115,7 +135,7 @@ impl UnifiedPayment {
115135

116136
let bolt12_offer =
117137
match self.bolt12_payment.receive_inner(amount_msats, description, None, None) {
118-
Ok(offer) => Some(offer),
138+
Ok(offer) => Some(maybe_wrap(offer)),
119139
Err(e) => {
120140
log_error!(self.logger, "Failed to create offer: {}", e);
121141
None
@@ -165,12 +185,19 @@ impl UnifiedPayment {
165185
&self, uri_str: &str, amount_msat: Option<u64>,
166186
route_parameters: Option<RouteParametersConfig>,
167187
) -> Result<UnifiedPaymentResult, Error> {
168-
let parse_fut = PaymentInstructions::parse(
169-
uri_str,
170-
self.config.network,
171-
self.hrn_resolver.as_ref(),
172-
false,
173-
);
188+
let target_network;
189+
190+
#[cfg(hrn_tests)]
191+
{
192+
target_network = bitcoin::Network::Bitcoin;
193+
}
194+
#[cfg(not(hrn_tests))]
195+
{
196+
target_network = self.config.network;
197+
}
198+
199+
let parse_fut =
200+
PaymentInstructions::parse(uri_str, target_network, self.hrn_resolver.as_ref(), false);
174201

175202
let instructions =
176203
tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut)
@@ -233,8 +260,30 @@ impl UnifiedPayment {
233260

234261
for method in sorted_payment_methods {
235262
match method {
236-
PaymentMethod::LightningBolt12(offer) => {
237-
let offer = maybe_wrap(offer.clone());
263+
PaymentMethod::LightningBolt12(_offer) => {
264+
#[cfg(not(hrn_tests))]
265+
let offer = maybe_wrap(_offer.clone());
266+
267+
#[cfg(hrn_tests)]
268+
// We inject a test-only offer here because full DNSSEC validation is
269+
// currently infeasible in regtest environments. This allows us to
270+
// bypass the validation requirements that would otherwise fail
271+
// without a functional global DNSSEC root in the test runner.
272+
let offer = {
273+
let test_offer_guard = self.test_offer.lock().map_err(|e| {
274+
log_error!(
275+
self.logger,
276+
"Failed to lock test_offer due to poisoning: {:?}",
277+
e
278+
);
279+
Error::PaymentSendingFailed
280+
})?;
281+
282+
match &*test_offer_guard {
283+
Some(o) => o.clone(),
284+
None => maybe_wrap(_offer.clone()),
285+
}
286+
};
238287

239288
let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
240289
let hrn = maybe_wrap(hrn.clone());
@@ -288,6 +337,21 @@ impl UnifiedPayment {
288337
log_error!(self.logger, "Payable methods not found in URI");
289338
Err(Error::PaymentSendingFailed)
290339
}
340+
341+
/// Sets a test offer to be used in the `send` method when the `hrn_tests` config flag is enabled.
342+
///
343+
/// This is necessary for Bolt12 payments in HRN tests because we typically resolve offers
344+
/// via [BIP 353] DNS addresses. Since full DNSSEC validation is infeasible in regtest
345+
/// environments, the automated resolution of an offer from a URI will fail. Injected
346+
/// offers allow us to bypass this resolution step and test the subsequent payment flow.
347+
///
348+
/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki
349+
#[cfg(hrn_tests)]
350+
pub fn set_test_offer(&self, _offer: Offer) {
351+
let _ = self.test_offer.lock().map(|mut guard| *guard = Some(_offer)).map_err(|e| {
352+
log_error!(self.logger, "Failed to set test offer due to poisoned lock: {:?}", e)
353+
});
354+
}
291355
}
292356

293357
/// Represents the result of a payment made using a [BIP 21] URI or a [BIP 353] Human-Readable Name.
@@ -395,9 +459,10 @@ impl<'a> bip21::de::DeserializationState<'a> for DeserializationState {
395459
"lno" => {
396460
let bolt12_value =
397461
String::try_from(value).map_err(|_| Error::UriParameterParsingFailed)?;
398-
let offer =
399-
bolt12_value.parse::<Offer>().map_err(|_| Error::UriParameterParsingFailed)?;
400-
self.bolt12_offer = Some(offer);
462+
let offer = bolt12_value
463+
.parse::<LdkOffer>()
464+
.map_err(|_| Error::UriParameterParsingFailed)?;
465+
self.bolt12_offer = Some(maybe_wrap(offer));
401466
Ok(bip21::de::ParamKind::Known)
402467
},
403468
_ => Ok(bip21::de::ParamKind::Unknown),
@@ -420,7 +485,7 @@ mod tests {
420485
use bitcoin::address::NetworkUnchecked;
421486
use bitcoin::{Address, Network};
422487

423-
use super::{Amount, Bolt11Invoice, Extras, Offer};
488+
use super::{maybe_wrap, Amount, Bolt11Invoice, Extras, LdkOffer};
424489

425490
#[test]
426491
fn parse_uri() {
@@ -474,7 +539,7 @@ mod tests {
474539
}
475540

476541
if let Some(offer) = parsed_uri_with_offer.extras.bolt12_offer {
477-
assert_eq!(offer, Offer::from_str(expected_bolt12_offer_2).unwrap());
542+
assert_eq!(offer, maybe_wrap(LdkOffer::from_str(expected_bolt12_offer_2).unwrap()));
478543
} else {
479544
panic!("No offer found.");
480545
}

tests/common/mod.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::collections::{HashMap, HashSet};
1414
use std::env;
1515
use std::future::Future;
1616
use std::path::PathBuf;
17+
use std::str::FromStr;
1718
use std::sync::atomic::{AtomicU16, Ordering};
1819
use std::sync::{Arc, RwLock};
1920
use std::time::Duration;
@@ -27,7 +28,10 @@ use bitcoin::{
2728
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2829
use electrsd::{corepc_node, ElectrsD};
2930
use electrum_client::ElectrumApi;
30-
use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig};
31+
use ldk_node::config::{
32+
AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig,
33+
HumanReadableNamesConfig,
34+
};
3135
use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy};
3236
use ldk_node::io::sqlite_store::SqliteStore;
3337
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
@@ -400,11 +404,27 @@ pub(crate) fn setup_two_nodes_with_store(
400404
println!("== Node A ==");
401405
let mut config_a = random_config(anchor_channels);
402406
config_a.store_type = store_type;
407+
408+
if cfg!(hrn_tests) {
409+
config_a.node_config.hrn_config =
410+
HumanReadableNamesConfig { resolution_config: HRNResolverConfig::Blip32 };
411+
}
412+
403413
let node_a = setup_node(chain_source, config_a);
404414

405415
println!("\n== Node B ==");
406416
let mut config_b = random_config(anchor_channels);
407417
config_b.store_type = store_type;
418+
419+
if cfg!(hrn_tests) {
420+
config_b.node_config.hrn_config = HumanReadableNamesConfig {
421+
resolution_config: HRNResolverConfig::Dns {
422+
dns_server_address: SocketAddress::from_str("8.8.8.8:53").unwrap(),
423+
enable_hrn_resolution_service: true,
424+
},
425+
};
426+
}
427+
408428
if allow_0conf {
409429
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
410430
}

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)