From 9ba57d76574b4646aaafc9750bc76724db6c71dc Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 16 Oct 2025 08:33:00 +0100 Subject: [PATCH 1/4] Add configuration options for HRN settings Introduce new configuration parameters to manage Human-Readable Name (HRN) resolution and DNSSEC validation behavior. These settings allow users to define custom resolution preferences for BOLT12 offer lookups. Moving these parameters into the central configuration struct ensures that node behavior is customizable at runtime and consistent across different network environments. This abstraction is necessary to support diverse DNSSEC requirements without hard-coding resolution logic. --- bindings/ldk_node.udl | 7 +++++++ src/config.rs | 35 ++++++++++++++++++++++++++++++++++- src/ffi/types.rs | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b59a38b04..b999aeed9 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,6 +13,7 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; RouteParametersConfig? route_parameters; + HumanReadableNamesConfig? hrn_config; }; dictionary AnchorChannelsConfig { @@ -501,6 +502,12 @@ dictionary RouteParametersConfig { u8 max_channel_saturation_power_of_half; }; +dictionary HumanReadableNamesConfig { + sequence default_dns_resolvers; + boolean is_hrn_resolver; + string dns_server_address; +}; + dictionary CustomTlvRecord { u64 type_num; sequence value; diff --git a/src/config.rs b/src/config.rs index 1b71d0d4e..77c85c49b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -119,7 +119,8 @@ pub(crate) const HRN_RESOLUTION_TIMEOUT_SECS: u64 = 5; /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | /// | `anchor_channels_config` | Some(..) | -/// | `route_parameters` | None | +/// | `route_parameters` | None | +/// | `hrn_config` | Some(..) | /// /// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their /// respective default values. @@ -184,6 +185,10 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub route_parameters: Option, + /// Configuration options for Human-Readable Names ([BIP 353]). + /// + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki + pub hrn_config: Option, } impl Default for Config { @@ -198,6 +203,34 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), route_parameters: None, node_alias: None, + hrn_config: Some(HumanReadableNamesConfig::default()), + } + } +} + +/// Configuration options for Human-Readable Names ([BIP 353]). +/// +/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki +#[derive(Debug, Clone)] +pub struct HumanReadableNamesConfig { + /// The Default DNS resolvers to be used for resolving Human-Readable Names. + /// + /// If not empty, the values set will be used as DNS resolvers when sending to HRNs. + /// + /// **Note:** If empty, DNS resolvers would be selected from the network graph. + pub default_dns_resolvers: Vec, + /// This allows us to use our node as a DNS resolver for 3rd party HRN resolutions. + pub is_hrn_resolver: bool, + /// The DNS Server which will be used for resolving HRNs. + pub dns_server_address: String, +} + +impl Default for HumanReadableNamesConfig { + fn default() -> Self { + HumanReadableNamesConfig { + default_dns_resolvers: Vec::new(), + is_hrn_resolver: false, + dns_server_address: String::new(), } } } diff --git a/src/ffi/types.rs b/src/ffi/types.rs index bed040fcd..7f6b4e0e9 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -46,7 +46,7 @@ pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; use crate::builder::sanitize_alias; pub use crate::config::{ default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, - EsploraSyncConfig, MaxDustHTLCExposure, + EsploraSyncConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, }; pub use crate::entropy::{generate_entropy_mnemonic, EntropyError, NodeEntropy, WordCount}; use crate::error::Error; From 23d9b2f9536ede9a326db1cb3f9c66a151251d0a Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:10:51 +0100 Subject: [PATCH 2/4] Pass HRNResolver or DomainResolver into OnionMessenger Inject specialized resolution capabilities into OnionMessenger to support outbound payments and third-party resolution services. This change refines the previous resolution logic by allowing the node to act as a robust BIP 353 participant. If configured as a service provider, the node utilizes a Domain Resolver to handle requests for other participants. Otherwise, it uses an HRN Resolver specifically for initiating its own outbound payments. Providing these as optional parameters in the Node constructor ensures the logic matches the node's designated role in the ecosystem. --- Cargo.toml | 5 ++++ bindings/ldk_node.udl | 2 ++ src/builder.rs | 66 ++++++++++++++++++++++++++++++++++++------ src/error.rs | 5 ++++ src/lib.rs | 6 ++-- src/payment/unified.rs | 9 ++++-- src/types.rs | 6 +++- 7 files changed, 84 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4f8c0ed7b..d7a0dbc4b 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ default = [] #lightning-transaction-sync = { version = "0.2.0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } #lightning-liquidity = { version = "0.2.0", features = ["std"] } #lightning-macros = { version = "0.2.0" } +#lightning-dns-resolver = { version = "0.3.0" } lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0", features = ["std"] } lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0" } @@ -50,6 +51,7 @@ lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightnin lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0", features = ["std"] } lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0" } +lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -140,6 +142,7 @@ harness = false #lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" } #lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" } #lightning-macros = { path = "../rust-lightning/lightning-macros" } +#lightning-dns-resolver = { path = "../rust-lightning/lightning-dns-resolver" } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } @@ -152,6 +155,7 @@ harness = false #lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } #lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } @@ -164,6 +168,7 @@ harness = false #lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } #vss-client-ng = { path = "../vss-client" } #vss-client-ng = { git = "https://github.com/lightningdevkit/vss-client", branch = "main" } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b999aeed9..2849e7fec 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -349,6 +349,7 @@ enum NodeError { "InvalidBlindedPaths", "AsyncPaymentServicesDisabled", "HrnParsingFailed", + "HrnResolverNotConfigured", }; dictionary NodeStatus { @@ -383,6 +384,7 @@ enum BuildError { "LoggerSetupFailed", "NetworkMismatch", "AsyncPaymentsConfigMismatch", + "DNSResolverSetupFailed", }; [Trait] diff --git a/src/builder.rs b/src/builder.rs index c1acf71d4..da01cbce8 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -25,6 +25,7 @@ use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArg use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler}; use lightning::log_trace; +use lightning::onion_message::dns_resolution::DNSResolverMessageHandler; use lightning::routing::gossip::NodeAlias; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ @@ -38,6 +39,7 @@ use lightning::util::persist::{ }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; +use lightning_dns_resolver::OMDomainResolver; use lightning_persister::fs_store::FilesystemStore; use vss_client::headers::VssHeaderProvider; @@ -72,8 +74,9 @@ use crate::peer_store::PeerStore; use crate::runtime::Runtime; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ - ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, GossipSync, Graph, KeysManager, - MessageRouter, OnionMessenger, PaymentStore, PeerManager, Persister, SyncAndAsyncKVStore, + ChainMonitor, ChannelManager, DomainResolver, DynStore, DynStoreWrapper, GossipSync, Graph, + HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, Persister, + SyncAndAsyncKVStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -185,6 +188,8 @@ pub enum BuildError { NetworkMismatch, /// The role of the node in an asynchronous payments context is not compatible with the current configuration. AsyncPaymentsConfigMismatch, + /// An attempt to setup a DNS Resolver failed. + DNSResolverSetupFailed, } impl fmt::Display for BuildError { @@ -217,12 +222,21 @@ impl fmt::Display for BuildError { "The async payments role is not compatible with the current configuration." ) }, + Self::DNSResolverSetupFailed => { + write!(f, "An attempt to setup a DNS resolver has failed.") + }, } } } impl std::error::Error for BuildError {} +enum Resolver { + HRN(Arc), + DNS(Arc), + Ignore(Arc), +} + /// A builder for an [`Node`] instance, allowing to set some configuration and module choices from /// the getgo. /// @@ -1444,7 +1458,34 @@ fn build_with_store_internal( })?; } - let hrn_resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph))); + let resolver = if let Some(hrn_config) = &config.hrn_config { + if hrn_config.is_hrn_resolver { + let dns_addr = hrn_config.dns_server_address.as_str(); + + Resolver::DNS(Arc::new(OMDomainResolver::ignoring_incoming_proofs( + dns_addr.parse().map_err(|_| BuildError::DNSResolverSetupFailed)?, + ))) + } else { + Resolver::HRN(Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone( + &network_graph, + )))) + } + } else { + // hrn_config is None, default to the IgnoringMessaageHandler. + Resolver::Ignore(Arc::new(IgnoringMessageHandler {})) + }; + + let om_resolver = match resolver { + Resolver::DNS(ref dns_resolver) => { + Arc::clone(dns_resolver) as Arc + }, + Resolver::HRN(ref hrn_resolver) => { + Arc::clone(hrn_resolver) as Arc + }, + Resolver::Ignore(ref ignoring_handler) => { + Arc::clone(ignoring_handler) as Arc + }, + }; // Initialize the PeerManager let onion_messenger: Arc = @@ -1457,7 +1498,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - Arc::clone(&hrn_resolver), + Arc::clone(&om_resolver), IgnoringMessageHandler {}, )) } else { @@ -1469,7 +1510,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - Arc::clone(&hrn_resolver), + Arc::clone(&om_resolver), IgnoringMessageHandler {}, )) }; @@ -1599,9 +1640,16 @@ fn build_with_store_internal( let peer_manager_clone = Arc::clone(&peer_manager); - hrn_resolver.register_post_queue_action(Box::new(move || { - peer_manager_clone.process_events(); - })); + let hrn_resolver = match resolver { + Resolver::DNS(_) => None, + Resolver::HRN(ref hrn_resolver) => { + hrn_resolver.register_post_queue_action(Box::new(move || { + peer_manager_clone.process_events(); + })); + Some(hrn_resolver) + }, + Resolver::Ignore(_) => None, + }; liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::clone(&peer_manager))); @@ -1716,7 +1764,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, - hrn_resolver, + hrn_resolver: hrn_resolver.cloned(), }) } diff --git a/src/error.rs b/src/error.rs index ea0bcca3b..67a1b11c3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -131,6 +131,8 @@ pub enum Error { AsyncPaymentServicesDisabled, /// Parsing a Human-Readable Name has failed. HrnParsingFailed, + /// A HRN resolver was not configured + HrnResolverNotConfigured, } impl fmt::Display for Error { @@ -213,6 +215,9 @@ impl fmt::Display for Error { Self::HrnParsingFailed => { write!(f, "Failed to parse a human-readable name.") }, + Self::HrnResolverNotConfigured => { + write!(f, "A HRN resolver was not configured.") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index cf728c8bf..22e5fb721 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,7 +207,7 @@ pub struct Node { node_metrics: Arc>, om_mailbox: Option>, async_payments_role: Option, - hrn_resolver: Arc, + hrn_resolver: Option>, } impl Node { @@ -958,7 +958,7 @@ impl Node { self.bolt12_payment().into(), Arc::clone(&self.config), Arc::clone(&self.logger), - Arc::clone(&self.hrn_resolver), + Arc::new(self.hrn_resolver.clone()), ) } @@ -979,7 +979,7 @@ impl Node { self.bolt12_payment(), Arc::clone(&self.config), Arc::clone(&self.logger), - Arc::clone(&self.hrn_resolver), + Arc::new(self.hrn_resolver.clone()), )) } diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 671af14ff..03a1c9747 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -64,14 +64,14 @@ pub struct UnifiedPayment { bolt12_payment: Arc, config: Arc, logger: Arc, - hrn_resolver: Arc, + hrn_resolver: Arc>>, } impl UnifiedPayment { pub(crate) fn new( onchain_payment: Arc, bolt11_invoice: Arc, bolt12_payment: Arc, config: Arc, logger: Arc, - hrn_resolver: Arc, + hrn_resolver: Arc>>, ) -> Self { Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger, hrn_resolver } } @@ -161,6 +161,11 @@ impl UnifiedPayment { &self, uri_str: &str, amount_msat: Option, route_parameters: Option, ) -> Result { + let resolver = self.hrn_resolver.as_ref().clone().ok_or_else(|| { + log_error!(self.logger, "No HRN resolver configured. Cannot resolve HRNs."); + Error::HrnResolverNotConfigured + })?; + let parse_fut = PaymentInstructions::parse( uri_str, self.config.network, diff --git a/src/types.rs b/src/types.rs index 5e9cd74c9..0db91b65a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -19,6 +19,7 @@ use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::types::ChannelId; +use lightning::onion_message::dns_resolution::DNSResolverMessageHandler; use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{CombinedScorer, ProbabilisticScoringFeeParameters}; @@ -27,6 +28,7 @@ use lightning::util::persist::{KVStore, KVStoreSync, MonitorUpdatingPersister}; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::GossipVerifier; +use lightning_dns_resolver::OMDomainResolver; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; @@ -277,12 +279,14 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, Arc, - Arc, + Arc, IgnoringMessageHandler, >; pub(crate) type HRNResolver = LDKOnionMessageDNSSECHrnResolver, Arc>; +pub(crate) type DomainResolver = OMDomainResolver; + pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, Arc, From eb4949603c75f179c3bc420e08426b634fac5c10 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:22:27 +0100 Subject: [PATCH 3/4] 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. --- Cargo.toml | 1 + benches/payments.rs | 1 + src/ffi/types.rs | 1 + src/lib.rs | 59 ++++++++++++++++++ src/payment/bolt12.rs | 21 ++++++- src/payment/unified.rs | 63 +++++++++++++------ tests/common/mod.rs | 16 ++++- tests/integration_tests_rust.rs | 107 ++++++++++++++++++++++++++------ 8 files changed, 226 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d7a0dbc4b..291c6bafe 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ panic = 'abort' # Abort on panic [features] default = [] +hrn_tests = [] [dependencies] #lightning = { version = "0.2.0", features = ["std"] } diff --git a/benches/payments.rs b/benches/payments.rs index ba69e046d..0237aa049 100644 --- a/benches/payments.rs +++ b/benches/payments.rs @@ -127,6 +127,7 @@ fn payment_benchmark(c: &mut Criterion) { true, false, common::TestStoreType::Sqlite, + false, ); let runtime = diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 7f6b4e0e9..4707a3451 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -297,6 +297,7 @@ impl std::fmt::Display for Offer { /// This struct can also be used for LN-Address recipients. /// /// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack +#[derive(Eq, Hash, PartialEq)] pub struct HumanReadableName { pub(crate) inner: LdkHumanReadableName, } diff --git a/src/lib.rs b/src/lib.rs index 22e5fb721..6c27d1df3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1859,3 +1859,62 @@ pub(crate) fn total_anchor_channels_reserve_sats( * anchor_channels_config.per_channel_reserve_sats }) } + +/// Testing utils for DNSSEC proof resolution of offers associated with the given Human-Readable Name. + +#[cfg(feature = "hrn_tests")] +pub mod dnssec_testing_utils { + use std::collections::HashMap; + #[cfg(feature = "uniffi")] + use std::sync::Arc; + use std::sync::{LazyLock, Mutex}; + + #[cfg(not(feature = "uniffi"))] + type Offer = lightning::offers::offer::Offer; + #[cfg(feature = "uniffi")] + type Offer = Arc; + + #[cfg(not(feature = "uniffi"))] + type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName; + #[cfg(feature = "uniffi")] + type HumanReadableName = Arc; + + static OFFER_OVERRIDE_MAP: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + + /// Sets a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Name. + pub fn set_testing_dnssec_proof_offer_resolution_override(hrn: &str, offer: Offer) { + let hrn_key = { + #[cfg(not(feature = "uniffi"))] + { + lightning::onion_message::dns_resolution::HumanReadableName::from_encoded(hrn) + .unwrap() + } + + #[cfg(feature = "uniffi")] + { + Arc::new(crate::ffi::HumanReadableName::from_encoded(hrn).unwrap()) + } + }; + + OFFER_OVERRIDE_MAP.lock().unwrap().insert(hrn_key, offer); + } + + /// Retrieves a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Names. + #[cfg(not(feature = "uniffi"))] + pub fn get_testing_offer_override(hrn: Option) -> Option { + OFFER_OVERRIDE_MAP.lock().unwrap().get(&hrn?).cloned() + } + + /// Retrieves a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Names. + #[cfg(feature = "uniffi")] + pub fn get_testing_offer_override(hrn: Option) -> Option { + let offer = OFFER_OVERRIDE_MAP.lock().unwrap().get(&hrn?).cloned().unwrap(); + Some(offer) + } + + /// Clears all testing overrides for DNSSEC proof resolution of offers. + pub fn clear_testing_overrides() { + OFFER_OVERRIDE_MAP.lock().unwrap().clear(); + } +} diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 98f1d21ef..04f2b037f 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -234,7 +234,26 @@ impl Bolt12Payment { return Err(Error::NotRunning); } - let offer = maybe_deref(offer); + let offer = if let Some(_hrn_ref) = &hrn { + #[cfg(feature = "hrn_tests")] + { + crate::dnssec_testing_utils::get_testing_offer_override(Some(_hrn_ref.clone())) + .map(|override_offer| { + log_info!(self.logger, "Using test-specific Offer override."); + override_offer + }) + .unwrap_or_else(|| offer.clone()) + } + + #[cfg(not(feature = "hrn_tests"))] + { + offer.clone() + } + } else { + offer.clone() + }; + + let offer = maybe_deref(&offer); let mut random_bytes = [0u8; 32]; rand::rng().fill_bytes(&mut random_bytes); diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 03a1c9747..59880405d 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -26,7 +26,7 @@ use bitcoin_payment_instructions::amount::Amount as BPIAmount; use bitcoin_payment_instructions::{PaymentInstructions, PaymentMethod}; use lightning::ln::channelmanager::PaymentId; use lightning::offers::offer::Offer; -use lightning::onion_message::dns_resolution::HumanReadableName; +use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; use lightning::routing::router::RouteParametersConfig; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; @@ -40,6 +40,11 @@ use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; +#[cfg(not(feature = "uniffi"))] +type HumanReadableName = LdkHumanReadableName; +#[cfg(feature = "uniffi")] +type HumanReadableName = Arc; + #[derive(Debug, Clone)] struct Extras { bolt11_invoice: Option, @@ -166,12 +171,28 @@ impl UnifiedPayment { Error::HrnResolverNotConfigured })?; - let parse_fut = PaymentInstructions::parse( - uri_str, - self.config.network, - self.hrn_resolver.as_ref(), - false, - ); + let target_network; + + target_network = if let Ok(hrn) = LdkHumanReadableName::from_encoded(uri_str) { + #[cfg(feature = "hrn_tests")] + { + let hrn_wrapped: HumanReadableName = maybe_wrap(hrn); + match crate::dnssec_testing_utils::get_testing_offer_override(Some(hrn_wrapped)) { + Some(_) => bitcoin::Network::Bitcoin, + _ => self.config.network, + } + } + #[cfg(not(feature = "hrn_tests"))] + { + let _ = hrn; + self.config.network + } + } else { + self.config.network + }; + + let parse_fut = + PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false); let instructions = tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut) @@ -197,7 +218,7 @@ impl UnifiedPayment { Error::InvalidAmount })?; - let fut = instr.set_amount(amt, self.hrn_resolver.as_ref()); + let fut = instr.set_amount(amt, resolver.as_ref()); tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), fut) .await @@ -237,18 +258,20 @@ impl UnifiedPayment { PaymentMethod::LightningBolt12(offer) => { let offer = maybe_wrap(offer.clone()); - let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) { - let hrn = maybe_wrap(hrn.clone()); - self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn)) - } else if let Some(amount_msat) = amount_msat { - self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters) - } else { - self.bolt12_payment.send(&offer, None, None, route_parameters) - } - .map_err(|e| { - log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e); - e - }); + let payment_result = { + if let Ok(hrn) = LdkHumanReadableName::from_encoded(uri_str) { + let hrn = maybe_wrap(hrn.clone()); + self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn)) + } else if let Some(amount_msat) = amount_msat { + self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters) + } else { + self.bolt12_payment.send(&offer, None, None, route_parameters) + } + .map_err(|e| { + log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e); + e + }) + }; if let Ok(payment_id) = payment_result { return Ok(UnifiedPaymentResult::Bolt12 { payment_id }); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 96f58297c..40abc6c90 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -26,7 +26,9 @@ use bitcoin::{ use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD}; use electrsd::{corepc_node, ElectrsD}; use electrum_client::ElectrumApi; -use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig}; +use ldk_node::config::{ + AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HumanReadableNamesConfig, +}; use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; @@ -319,7 +321,7 @@ pub(crate) use setup_builder; pub(crate) fn setup_two_nodes( chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, - anchors_trusted_no_reserve: bool, + anchors_trusted_no_reserve: bool, second_node_is_hrn_resolver: bool, ) -> (TestNode, TestNode) { setup_two_nodes_with_store( chain_source, @@ -327,12 +329,13 @@ pub(crate) fn setup_two_nodes( anchor_channels, anchors_trusted_no_reserve, TestStoreType::TestSyncStore, + second_node_is_hrn_resolver, ) } pub(crate) fn setup_two_nodes_with_store( chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, - anchors_trusted_no_reserve: bool, store_type: TestStoreType, + anchors_trusted_no_reserve: bool, store_type: TestStoreType, second_node_is_hrn_resolver: bool, ) -> (TestNode, TestNode) { println!("== Node A =="); let mut config_a = random_config(anchor_channels); @@ -342,6 +345,13 @@ pub(crate) fn setup_two_nodes_with_store( println!("\n== Node B =="); let mut config_b = random_config(anchor_channels); config_b.store_type = store_type; + if second_node_is_hrn_resolver { + config_b.node_config.hrn_config = Some(HumanReadableNamesConfig { + default_dns_resolvers: Vec::new(), + is_hrn_resolver: true, + dns_server_address: "8.8.8.8:53".to_string(), + }); + } if allow_0conf { config_b.node_config.trusted_peers_0conf.push(node_a.node_id()); } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 4b82d1f4f..ba4205ad3 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -27,12 +27,15 @@ use common::{ TestSyncStore, }; use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; +#[cfg(feature = "hrn_tests")] +use ldk_node::dnssec_testing_utils; use ldk_node::entropy::NodeEntropy; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, UnifiedPaymentResult, }; + use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; @@ -45,7 +48,7 @@ use log::LevelFilter; async fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; } @@ -54,7 +57,7 @@ async fn channel_full_cycle() { async fn channel_full_cycle_electrum() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Electrum(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; } @@ -63,7 +66,7 @@ async fn channel_full_cycle_electrum() { async fn channel_full_cycle_bitcoind_rpc_sync() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; } @@ -72,7 +75,7 @@ async fn channel_full_cycle_bitcoind_rpc_sync() { async fn channel_full_cycle_bitcoind_rest_sync() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::BitcoindRestSync(&bitcoind); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; } @@ -81,7 +84,7 @@ async fn channel_full_cycle_bitcoind_rest_sync() { async fn channel_full_cycle_force_close() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) .await; } @@ -90,7 +93,7 @@ async fn channel_full_cycle_force_close() { async fn channel_full_cycle_force_close_trusted_no_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) .await; } @@ -99,7 +102,7 @@ async fn channel_full_cycle_force_close_trusted_no_reserve() { async fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true, false) .await; } @@ -108,7 +111,7 @@ async fn channel_full_cycle_0conf() { async fn channel_full_cycle_legacy_staticremotekey() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false, false) .await; } @@ -117,7 +120,7 @@ async fn channel_full_cycle_legacy_staticremotekey() { async fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -322,7 +325,7 @@ async fn start_stop_reinit() { async fn onchain_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -523,7 +526,7 @@ async fn onchain_send_receive() { async fn onchain_send_all_retains_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); // Setup nodes let addr_a = node_a.onchain_payment().new_address().unwrap(); @@ -838,7 +841,7 @@ async fn sign_verify_msg() { async fn connection_multi_listen() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false, false); let node_id_b = node_b.node_id(); @@ -858,7 +861,7 @@ async fn connection_restart_behavior() { async fn do_connection_restart_behavior(persist: bool) { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false, false); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -905,7 +908,7 @@ async fn do_connection_restart_behavior(persist: bool) { async fn concurrent_connections_succeed() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b); @@ -935,7 +938,7 @@ async fn run_splice_channel_test(bitcoind_chain_source: bool) { } else { TestChainSource::Esplora(&electrsd) }; - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let address_b = node_b.onchain_payment().new_address().unwrap(); @@ -1080,7 +1083,7 @@ async fn splice_channel() { async fn simple_bolt12_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_amount_sat = 5_000_000; @@ -1537,7 +1540,7 @@ async fn generate_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -1592,7 +1595,7 @@ async fn unified_send_receive_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -1699,6 +1702,72 @@ async fn unified_send_receive_bip21_uri() { assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } +#[cfg(feature = "hrn_tests")] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn unified_send_to_hrn() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, true); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premined_sats = 5_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premined_sats), + ) + .await; + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Sleep until we broadcast a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let test_offer = node_b.bolt12_payment().receive(1000000, "test offer", None, None).unwrap(); + + // Sleep one more sec to make sure the node announcement propagates. + std::thread::sleep(std::time::Duration::from_secs(1)); + + let hrn = "matt@mattcorallo.com"; + + dnssec_testing_utils::set_testing_dnssec_proof_offer_resolution_override( + hrn, + test_offer.clone(), + ); + + let offer_payment_id: PaymentId = + match node_a.unified_payment().send(&hrn, Some(1000000), None).await { + Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => { + println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected Bolt12 payment but got Bolt11"); + }, + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt12 payment but got On-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt12 payment but got error: {:?}", e); + }, + }; + + expect_payment_successful_event!(node_a, Some(offer_payment_id), None); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn lsps2_client_service_integration() { do_lsps2_client_service_integration(true).await; @@ -1948,7 +2017,7 @@ async fn facade_logging() { async fn spontaneous_send_with_custom_preimage() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_sat = 1_000_000; From d5bcd8a22fbea6f01c333ab2ef0d60e2ad5478b7 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Fri, 9 Jan 2026 18:54:19 +0100 Subject: [PATCH 4/4] Update CI workflow to include hrn_tests coverage Update the GitHub Actions workflow to include coverage for the new hrn_tests feature across multiple build configurations. This ensures that the DNSSEC override logic is validated in both standard Rust and UniFFI-enabled environments. Including these flags in CI prevents regressions where testing-specific code might break the primary build or fail to compile due to type mismatches between the LDK and FFI wrappers. Testing both feature combinations (with and without UniFFI) guarantees that the abstraction for HumanReadableName remains consistent across all supported platforms and integration layers. --- .github/workflows/rust.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 661703ded..061aa8fb2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -85,6 +85,14 @@ jobs: if: "matrix.platform != 'windows-latest' && matrix.build-uniffi" run: | RUSTFLAGS="--cfg no_download" cargo test --features uniffi + - name: Test with HRN overrides (No UniFFI) on Rust ${{ matrix.toolchain }} + if: "matrix.platform == 'ubuntu-latest' && matrix.toolchain == 'stable'" + run: | + RUSTFLAGS="--cfg no_download" cargo test --features hrn_tests + - name: Test with UniFFI and HRN overrides on Rust ${{ matrix.toolchain }} + if: "matrix.platform != 'windows-latest' && matrix.build-uniffi" + run: | + RUSTFLAGS="--cfg no_download" cargo test --features uniffi,hrn_tests doc: name: Documentation