Skip to content

Commit b910a8f

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 23d9b2f commit b910a8f

File tree

8 files changed

+231
-43
lines changed

8 files changed

+231
-43
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ panic = 'abort' # Abort on panic
2525

2626
[features]
2727
default = []
28+
hrn_tests = []
2829

2930
[dependencies]
3031
#lightning = { version = "0.2.0", features = ["std"] }

benches/payments.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ fn payment_benchmark(c: &mut Criterion) {
127127
true,
128128
false,
129129
common::TestStoreType::Sqlite,
130+
false,
130131
);
131132

132133
let runtime =

src/ffi/types.rs

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

src/lib.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,3 +1859,62 @@ pub(crate) fn total_anchor_channels_reserve_sats(
18591859
* anchor_channels_config.per_channel_reserve_sats
18601860
})
18611861
}
1862+
1863+
/// Testing utils for DNSSEC proof resolution of offers associated with the given Human-Readable Name.
1864+
1865+
#[cfg(feature = "hrn_tests")]
1866+
pub mod dnssec_testing_utils {
1867+
use std::collections::HashMap;
1868+
#[cfg(feature = "uniffi")]
1869+
use std::sync::Arc;
1870+
use std::sync::{LazyLock, Mutex};
1871+
1872+
#[cfg(not(feature = "uniffi"))]
1873+
type Offer = lightning::offers::offer::Offer;
1874+
#[cfg(feature = "uniffi")]
1875+
type Offer = Arc<crate::ffi::Offer>;
1876+
1877+
#[cfg(not(feature = "uniffi"))]
1878+
type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName;
1879+
#[cfg(feature = "uniffi")]
1880+
type HumanReadableName = Arc<crate::ffi::HumanReadableName>;
1881+
1882+
static OFFER_OVERRIDE_MAP: LazyLock<Mutex<HashMap<HumanReadableName, Offer>>> =
1883+
LazyLock::new(|| Mutex::new(HashMap::new()));
1884+
1885+
/// Sets a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Name.
1886+
pub fn set_testing_dnssec_proof_offer_resolution_override(hrn: &str, offer: Offer) {
1887+
let hrn_key = {
1888+
#[cfg(not(feature = "uniffi"))]
1889+
{
1890+
lightning::onion_message::dns_resolution::HumanReadableName::from_encoded(hrn)
1891+
.unwrap()
1892+
}
1893+
1894+
#[cfg(feature = "uniffi")]
1895+
{
1896+
Arc::new(crate::ffi::HumanReadableName::from_encoded(hrn).unwrap())
1897+
}
1898+
};
1899+
1900+
OFFER_OVERRIDE_MAP.lock().unwrap().insert(hrn_key, offer);
1901+
}
1902+
1903+
/// Retrieves a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Names.
1904+
#[cfg(not(feature = "uniffi"))]
1905+
pub fn get_testing_offer_override(hrn: Option<HumanReadableName>) -> Option<Offer> {
1906+
OFFER_OVERRIDE_MAP.lock().unwrap().get(&hrn?).cloned()
1907+
}
1908+
1909+
/// Retrieves a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Names.
1910+
#[cfg(feature = "uniffi")]
1911+
pub fn get_testing_offer_override(hrn: Option<HumanReadableName>) -> Option<Offer> {
1912+
let offer = OFFER_OVERRIDE_MAP.lock().unwrap().get(&hrn?).cloned().unwrap();
1913+
Some(offer)
1914+
}
1915+
1916+
/// Clears all testing overrides for DNSSEC proof resolution of offers.
1917+
pub fn clear_testing_overrides() {
1918+
OFFER_OVERRIDE_MAP.lock().unwrap().clear();
1919+
}
1920+
}

src/payment/bolt12.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,26 @@ impl Bolt12Payment {
234234
return Err(Error::NotRunning);
235235
}
236236

237-
let offer = maybe_deref(offer);
237+
let offer = if let Some(_hrn_ref) = &hrn {
238+
#[cfg(feature = "hrn_tests")]
239+
{
240+
crate::dnssec_testing_utils::get_testing_offer_override(Some(_hrn_ref.clone()))
241+
.map(|override_offer| {
242+
log_info!(self.logger, "Using test-specific Offer override.");
243+
override_offer
244+
})
245+
.unwrap_or_else(|| offer.clone())
246+
}
247+
248+
#[cfg(not(feature = "hrn_tests"))]
249+
{
250+
offer.clone()
251+
}
252+
} else {
253+
offer.clone()
254+
};
255+
256+
let offer = maybe_deref(&offer);
238257

239258
let mut random_bytes = [0u8; 32];
240259
rand::rng().fill_bytes(&mut random_bytes);

src/payment/unified.rs

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ 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;
29+
use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
3030
use lightning::routing::router::RouteParametersConfig;
3131
use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
3232

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

4141
type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>;
4242

43+
#[cfg(not(feature = "uniffi"))]
44+
type HumanReadableName = LdkHumanReadableName;
45+
#[cfg(feature = "uniffi")]
46+
type HumanReadableName = crate::ffi::HumanReadableName;
47+
4348
#[derive(Debug, Clone)]
4449
struct Extras {
4550
bolt11_invoice: Option<Bolt11Invoice>,
@@ -166,12 +171,33 @@ impl UnifiedPayment {
166171
Error::HrnResolverNotConfigured
167172
})?;
168173

169-
let parse_fut = PaymentInstructions::parse(
170-
uri_str,
171-
self.config.network,
172-
self.hrn_resolver.as_ref(),
173-
false,
174-
);
174+
let target_network;
175+
176+
target_network = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
177+
#[cfg(feature = "hrn_tests")]
178+
{
179+
#[cfg(feature = "uniffi")]
180+
let hrn_wrapped: Arc<HumanReadableName> = maybe_wrap(hrn);
181+
#[cfg(not(feature = "uniffi"))]
182+
let hrn_wrapped: HumanReadableName = maybe_wrap(hrn);
183+
match crate::dnssec_testing_utils::get_testing_offer_override(Some(
184+
hrn_wrapped.into(),
185+
)) {
186+
Some(_) => bitcoin::Network::Bitcoin,
187+
_ => self.config.network,
188+
}
189+
}
190+
#[cfg(not(feature = "hrn_tests"))]
191+
{
192+
let _ = hrn;
193+
self.config.network
194+
}
195+
} else {
196+
self.config.network
197+
};
198+
199+
let parse_fut =
200+
PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false);
175201

176202
let instructions =
177203
tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut)
@@ -197,7 +223,7 @@ impl UnifiedPayment {
197223
Error::InvalidAmount
198224
})?;
199225

200-
let fut = instr.set_amount(amt, self.hrn_resolver.as_ref());
226+
let fut = instr.set_amount(amt, resolver.as_ref());
201227

202228
tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), fut)
203229
.await
@@ -237,18 +263,20 @@ impl UnifiedPayment {
237263
PaymentMethod::LightningBolt12(offer) => {
238264
let offer = maybe_wrap(offer.clone());
239265

240-
let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
241-
let hrn = maybe_wrap(hrn.clone());
242-
self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn))
243-
} else if let Some(amount_msat) = amount_msat {
244-
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
245-
} else {
246-
self.bolt12_payment.send(&offer, None, None, route_parameters)
247-
}
248-
.map_err(|e| {
249-
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
250-
e
251-
});
266+
let payment_result = {
267+
if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
268+
let hrn = maybe_wrap(hrn.clone());
269+
self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn))
270+
} else if let Some(amount_msat) = amount_msat {
271+
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
272+
} else {
273+
self.bolt12_payment.send(&offer, None, None, route_parameters)
274+
}
275+
.map_err(|e| {
276+
log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e);
277+
e
278+
})
279+
};
252280

253281
if let Ok(payment_id) = payment_result {
254282
return Ok(UnifiedPaymentResult::Bolt12 { payment_id });

tests/common/mod.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ use bitcoin::{
2626
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2727
use electrsd::{corepc_node, ElectrsD};
2828
use electrum_client::ElectrumApi;
29-
use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig};
29+
use ldk_node::config::{
30+
AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HumanReadableNamesConfig,
31+
};
3032
use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy};
3133
use ldk_node::io::sqlite_store::SqliteStore;
3234
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
@@ -319,20 +321,21 @@ pub(crate) use setup_builder;
319321

320322
pub(crate) fn setup_two_nodes(
321323
chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool,
322-
anchors_trusted_no_reserve: bool,
324+
anchors_trusted_no_reserve: bool, second_node_is_hrn_resolver: bool,
323325
) -> (TestNode, TestNode) {
324326
setup_two_nodes_with_store(
325327
chain_source,
326328
allow_0conf,
327329
anchor_channels,
328330
anchors_trusted_no_reserve,
329331
TestStoreType::TestSyncStore,
332+
second_node_is_hrn_resolver,
330333
)
331334
}
332335

333336
pub(crate) fn setup_two_nodes_with_store(
334337
chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool,
335-
anchors_trusted_no_reserve: bool, store_type: TestStoreType,
338+
anchors_trusted_no_reserve: bool, store_type: TestStoreType, second_node_is_hrn_resolver: bool,
336339
) -> (TestNode, TestNode) {
337340
println!("== Node A ==");
338341
let mut config_a = random_config(anchor_channels);
@@ -342,6 +345,13 @@ pub(crate) fn setup_two_nodes_with_store(
342345
println!("\n== Node B ==");
343346
let mut config_b = random_config(anchor_channels);
344347
config_b.store_type = store_type;
348+
if second_node_is_hrn_resolver {
349+
config_b.node_config.hrn_config = Some(HumanReadableNamesConfig {
350+
default_dns_resolvers: Vec::new(),
351+
is_hrn_resolver: true,
352+
dns_server_address: "8.8.8.8:53".to_string(),
353+
});
354+
}
345355
if allow_0conf {
346356
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
347357
}

0 commit comments

Comments
 (0)