From 8b3e36fded0d5dd1cbddb4dd697a080d8d9d1711 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 16 Oct 2025 08:33:00 +0100 Subject: [PATCH 01/10] 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 6bd031379..e21133a5c 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 { @@ -514,6 +515,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 103b74657..5de10961e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -127,7 +127,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. @@ -192,6 +193,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 { @@ -206,6 +211,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 2a349a967..ebc38ba95 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, SyncTimeoutsConfig, + EsploraSyncConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, SyncTimeoutsConfig, }; pub use crate::entropy::{generate_entropy_mnemonic, EntropyError, NodeEntropy, WordCount}; use crate::error::Error; From 018a591e9f91ae0cf8abdd5f5bdd2cb79736c00f Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:10:51 +0100 Subject: [PATCH 02/10] 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 | 6 ++++ bindings/ldk_node.udl | 2 ++ src/builder.rs | 72 +++++++++++++++++++++++++++++++++++------- src/error.rs | 5 +++ src/lib.rs | 6 ++-- src/payment/unified.rs | 19 +++++------ src/types.rs | 6 +++- 7 files changed, 91 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3dcad31a5..678e64865 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 = "35ab03fbe0fe0927a9242754a0797553f6f7f099", features = ["std"] } lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "35ab03fbe0fe0927a9242754a0797553f6f7f099" } @@ -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 = "35ab03fbe0fe0927a9242754a0797553f6f7f099", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "35ab03fbe0fe0927a9242754a0797553f6f7f099", features = ["std"] } lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "35ab03fbe0fe0927a9242754a0797553f6f7f099" } +lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "35ab03fbe0fe0927a9242754a0797553f6f7f099" } 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"]} @@ -141,6 +143,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" } @@ -153,6 +156,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" } @@ -165,6 +169,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" } @@ -181,3 +186,4 @@ 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" } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index e21133a5c..e4eef8886 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -362,6 +362,7 @@ enum NodeError { "InvalidBlindedPaths", "AsyncPaymentServicesDisabled", "HrnParsingFailed", + "HrnResolverNotConfigured", }; dictionary NodeStatus { @@ -396,6 +397,7 @@ enum BuildError { "LoggerSetupFailed", "NetworkMismatch", "AsyncPaymentsConfigMismatch", + "DNSResolverSetupFailed", }; [Trait] diff --git a/src/builder.rs b/src/builder.rs index 5d8a5a7a9..997b8fea4 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::{ @@ -39,6 +40,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; @@ -75,9 +77,9 @@ use crate::peer_store::PeerStore; use crate::runtime::{Runtime, RuntimeSpawner}; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ - AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, GossipSync, Graph, - KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore, - Persister, SyncAndAsyncKVStore, + AsyncPersister, ChainMonitor, ChannelManager, DomainResolver, DynStore, DynStoreWrapper, + GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, + PeerManager, PendingPaymentStore, Persister, SyncAndAsyncKVStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -189,6 +191,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 { @@ -221,12 +225,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. /// @@ -1517,7 +1530,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 = @@ -1530,7 +1570,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 { @@ -1542,7 +1582,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 {}, )) }; @@ -1675,11 +1715,19 @@ fn build_with_store_internal( )); let peer_manager_clone = Arc::downgrade(&peer_manager); - hrn_resolver.register_post_queue_action(Box::new(move || { - if let Some(upgraded_pointer) = peer_manager_clone.upgrade() { - upgraded_pointer.process_events(); - } - })); + + let hrn_resolver = match resolver { + Resolver::DNS(_) => None, + Resolver::HRN(ref hrn_resolver) => { + hrn_resolver.register_post_queue_action(Box::new(move || { + if let Some(upgraded_pointer) = peer_manager_clone.upgrade() { + upgraded_pointer.process_events(); + } + })); + Some(hrn_resolver) + }, + Resolver::Ignore(_) => None, + }; liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::downgrade(&peer_manager))); @@ -1786,7 +1834,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, - hrn_resolver, + hrn_resolver: hrn_resolver.cloned(), #[cfg(cycle_tests)] _leak_checker, }) 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 d2222d949..ee5ada5c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,7 +226,7 @@ pub struct Node { node_metrics: Arc>, om_mailbox: Option>, async_payments_role: Option, - hrn_resolver: Arc, + hrn_resolver: Option>, #[cfg(cycle_tests)] _leak_checker: LeakChecker, } @@ -979,7 +979,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()), ) } @@ -1000,7 +1000,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..64b5f5fa1 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,12 +161,13 @@ impl UnifiedPayment { &self, uri_str: &str, amount_msat: Option, route_parameters: Option, ) -> Result { - let parse_fut = PaymentInstructions::parse( - uri_str, - self.config.network, - self.hrn_resolver.as_ref(), - false, - ); + 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, resolver.as_ref(), false); let instructions = tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut) @@ -192,7 +193,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 diff --git a/src/types.rs b/src/types.rs index b5b1ffed7..83652b893 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}; @@ -29,6 +30,7 @@ use lightning::util::persist::{ 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; @@ -289,12 +291,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 14d0971a4907405907f48cb609d71a0592bf9dc9 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:22:27 +0100 Subject: [PATCH 03/10] 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 | 59 +++++++++++++----- tests/common/mod.rs | 16 ++++- tests/integration_tests_rust.rs | 107 ++++++++++++++++++++++++++------ 8 files changed, 228 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 678e64865..a038c7d8b 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 ebc38ba95..456306c33 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 ee5ada5c0..f12f88e46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1878,3 +1878,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 ada4cd7e2..63cd8426e 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -235,7 +235,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 64b5f5fa1..c3f12c77d 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -26,7 +26,6 @@ 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::routing::router::RouteParametersConfig; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; @@ -40,6 +39,11 @@ use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; +#[cfg(not(feature = "uniffi"))] +type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName; +#[cfg(feature = "uniffi")] +type HumanReadableName = crate::ffi::HumanReadableName; + #[derive(Debug, Clone)] struct Extras { bolt11_invoice: Option, @@ -166,8 +170,33 @@ impl UnifiedPayment { Error::HrnResolverNotConfigured })?; + let target_network; + + target_network = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) { + #[cfg(feature = "hrn_tests")] + { + #[cfg(feature = "uniffi")] + let hrn_wrapped: Arc = maybe_wrap(hrn); + #[cfg(not(feature = "uniffi"))] + let hrn_wrapped: HumanReadableName = maybe_wrap(hrn); + match crate::dnssec_testing_utils::get_testing_offer_override(Some( + hrn_wrapped.into(), + )) { + 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, self.config.network, resolver.as_ref(), false); + PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false); let instructions = tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut) @@ -233,18 +262,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) = 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 + }) + }; 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 5f6657260..33ede4e98 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 605dd0613..2b1729e5a 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(); @@ -324,7 +327,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(); @@ -525,7 +528,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(); @@ -840,7 +843,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(); @@ -860,7 +863,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(); @@ -907,7 +910,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); @@ -937,7 +940,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(); @@ -1082,7 +1085,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; @@ -1539,7 +1542,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; @@ -1594,7 +1597,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; @@ -1701,6 +1704,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; @@ -1951,7 +2020,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 1cb0511afacb50d21d90c2c78a9640d621e0925c Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Fri, 9 Jan 2026 18:54:19 +0100 Subject: [PATCH 04/10] 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 1ccade444..2ddfa8731 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 --cfg cycle_tests" 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 From 59275e37977d9e01949e77b654076372ad63dc6e Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 2 Feb 2026 14:36:55 +0100 Subject: [PATCH 05/10] fixup! Add configuration options for HRN settings Add config options for bLIP-32 client (sending to HRNs) and make node a bLIP-32 service by default - by default, use LocalDNS resolution for sending to HRNs. --- bindings/ldk_node.udl | 11 ++++++++--- src/config.rs | 33 ++++++++++++++++++++------------- src/ffi/types.rs | 3 ++- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index e4eef8886..dfb12a4c0 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -517,10 +517,15 @@ dictionary RouteParametersConfig { u8 max_channel_saturation_power_of_half; }; +[Enum] +interface HRNResolverConfig { + Blip32Onion(); + LocalDns(string dns_server_address); +}; + dictionary HumanReadableNamesConfig { - sequence default_dns_resolvers; - boolean is_hrn_resolver; - string dns_server_address; + HRNResolverConfig client_resolution_config; + boolean disable_hrn_resolution_service; }; dictionary CustomTlvRecord { diff --git a/src/config.rs b/src/config.rs index 5de10961e..653f42c85 100644 --- a/src/config.rs +++ b/src/config.rs @@ -216,29 +216,36 @@ impl Default for Config { } } +/// Configuration options for how our node resolves Human-Readable Names (HRNs) when acting as a client. +#[derive(Debug, Clone)] +pub enum HRNResolverConfig { + /// Use bLIP-32 to ask other nodes to resolve names for us. + Blip32Onion, + /// Resolve names locally using a specific DNS server. + LocalDns { + /// The IP and port of the DNS server (e.g., "8.8.8.8:53"). + dns_server_address: String, + }, +} + /// 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, + /// This sets how our node resolves names when we want to send a payment. + pub client_resolution_config: HRNResolverConfig, + /// if set, this allows others to use our node for HRN resolutions. + pub disable_hrn_resolution_service: bool, } impl Default for HumanReadableNamesConfig { fn default() -> Self { HumanReadableNamesConfig { - default_dns_resolvers: Vec::new(), - is_hrn_resolver: false, - dns_server_address: String::new(), + client_resolution_config: HRNResolverConfig::LocalDns { + dns_server_address: "8.8.8.8:53".to_string(), + }, + disable_hrn_resolution_service: false, } } } diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 456306c33..7f1bd6425 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -46,7 +46,8 @@ pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; use crate::builder::sanitize_alias; pub use crate::config::{ default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, - EsploraSyncConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, SyncTimeoutsConfig, + EsploraSyncConfig, HRNResolverConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, + SyncTimeoutsConfig, }; pub use crate::entropy::{generate_entropy_mnemonic, EntropyError, NodeEntropy, WordCount}; use crate::error::Error; From 82a1dd9c34c6eaa09d3d4bb78d6165b96a7ee02d Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 2 Feb 2026 15:01:49 +0100 Subject: [PATCH 06/10] fixup! Pass HRNResolver or DomainResolver into OnionMessenger Use DNSHrnResolver for local resolution when node is a bLIP-32 service. node can be a bLIP-32 service only if it can be announced. --- src/builder.rs | 124 ++++++++++++++++++++++++----------------- src/payment/unified.rs | 5 +- src/types.rs | 44 +++++++++++++-- 3 files changed, 114 insertions(+), 59 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 997b8fea4..3949ac0ff 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use std::convert::TryInto; use std::default::Default; use std::path::PathBuf; -use std::sync::{Arc, Mutex, Once, RwLock}; +use std::sync::{Arc, Mutex, Once, RwLock, Weak}; use std::time::SystemTime; use std::{fmt, fs}; @@ -19,6 +19,7 @@ use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::key::Secp256k1; use bitcoin::secp256k1::PublicKey; use bitcoin::{BlockHash, Network}; +use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver; use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::{chainmonitor, BestBlock}; use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; @@ -47,7 +48,7 @@ use vss_client::headers::VssHeaderProvider; use crate::chain::ChainSource; use crate::config::{ default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, - BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, + BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, }; use crate::connection::ConnectionManager; @@ -77,9 +78,9 @@ use crate::peer_store::PeerStore; use crate::runtime::{Runtime, RuntimeSpawner}; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ - AsyncPersister, ChainMonitor, ChannelManager, DomainResolver, DynStore, DynStoreWrapper, - GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, - PeerManager, PendingPaymentStore, Persister, SyncAndAsyncKVStore, + AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, GossipSync, Graph, + HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, + PendingPaymentStore, Persister, SyncAndAsyncKVStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -234,12 +235,6 @@ impl fmt::Display for BuildError { 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. /// @@ -1530,32 +1525,66 @@ fn build_with_store_internal( })?; } - 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 peer_manager_hook: Arc>>> = Arc::new(Mutex::new(None)); + let mut hrn_resolver_out = None; + + let om_resolver = match &config.hrn_config { + None => Arc::new(IgnoringMessageHandler {}), + Some(hrn_config) => { + let client_resolver: Arc = + match &hrn_config.client_resolution_config { + HRNResolverConfig::Blip32Onion => { + let hrn_res = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone( + &network_graph, + ))); + hrn_resolver_out = Some(Arc::new(HRNResolver::Onion(Arc::clone(&hrn_res)))); + + let pm_hook_clone = Arc::clone(&peer_manager_hook); + hrn_res.register_post_queue_action(Box::new(move || { + if let Ok(guard) = pm_hook_clone.lock() { + if let Some(weak_pm) = &*guard { + if let Some(pm) = weak_pm.upgrade() { + pm.process_events(); + } + } + } + })); + + hrn_res + }, + HRNResolverConfig::LocalDns { dns_server_address } => { + let addr = dns_server_address + .parse() + .map_err(|_| BuildError::DNSResolverSetupFailed)?; + let hrn_res = Arc::new(DNSHrnResolver(addr)); + hrn_resolver_out = Some(Arc::new(HRNResolver::Local(hrn_res))); + Arc::new(OMDomainResolver::ignoring_incoming_proofs(addr)) + }, + }; + + let should_act_as_service = if hrn_config.disable_hrn_resolution_service { + false + } else if may_announce_channel(&config).is_ok() { + true + } else { + false + }; - 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 + if should_act_as_service { + if let HRNResolverConfig::LocalDns { dns_server_address } = + &hrn_config.client_resolution_config + { + let service_dns_addr = dns_server_address + .parse() + .map_err(|_| BuildError::DNSResolverSetupFailed)?; + Arc::new(OMDomainResolver::new(service_dns_addr, Some(client_resolver))) + } else { + log_error!(logger, "To act as an HRN resolution service, the DNS resolver must be configured to use a DNS server."); + return Err(BuildError::DNSResolverSetupFailed); + } + } else { + client_resolver + } }, }; @@ -1703,7 +1732,7 @@ fn build_with_store_internal( BuildError::InvalidSystemTime })?; - let peer_manager = Arc::new(PeerManager::new( + let peer_manager: Arc = Arc::new(PeerManager::new( msg_handler, cur_time.as_secs().try_into().map_err(|e| { log_error!(logger, "Failed to get current time: {}", e); @@ -1714,20 +1743,11 @@ fn build_with_store_internal( Arc::clone(&keys_manager), )); - let peer_manager_clone = Arc::downgrade(&peer_manager); - - let hrn_resolver = match resolver { - Resolver::DNS(_) => None, - Resolver::HRN(ref hrn_resolver) => { - hrn_resolver.register_post_queue_action(Box::new(move || { - if let Some(upgraded_pointer) = peer_manager_clone.upgrade() { - upgraded_pointer.process_events(); - } - })); - Some(hrn_resolver) - }, - Resolver::Ignore(_) => None, - }; + if let Ok(mut guard) = peer_manager_hook.lock() { + *guard = Some(Arc::downgrade(&peer_manager)); + } else { + return Err(BuildError::DNSResolverSetupFailed); + } liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::downgrade(&peer_manager))); @@ -1834,7 +1854,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, - hrn_resolver: hrn_resolver.cloned(), + hrn_resolver: hrn_resolver_out, #[cfg(cycle_tests)] _leak_checker, }) diff --git a/src/payment/unified.rs b/src/payment/unified.rs index c3f12c77d..0a5e9782c 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -195,8 +195,7 @@ impl UnifiedPayment { self.config.network }; - let parse_fut = - PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false); + let parse_fut = PaymentInstructions::parse(uri_str, target_network, &*resolver, false); let instructions = tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut) @@ -222,7 +221,7 @@ impl UnifiedPayment { Error::InvalidAmount })?; - let fut = instr.set_amount(amt, resolver.as_ref()); + let fut = instr.set_amount(amt, &*resolver); tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), fut) .await diff --git a/src/types.rs b/src/types.rs index 83652b893..c15e68759 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,9 +10,16 @@ use std::future::Future; use std::pin::Pin; use std::sync::{Arc, Mutex}; +use bitcoin_payment_instructions::amount::Amount; +use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; +use bitcoin_payment_instructions::hrn_resolution::{ + HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture, +}; + use bitcoin::secp256k1::PublicKey; use bitcoin::{OutPoint, ScriptBuf}; -use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; + use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; @@ -30,7 +37,6 @@ use lightning::util::persist::{ 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; @@ -295,9 +301,39 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse IgnoringMessageHandler, >; -pub(crate) type HRNResolver = LDKOnionMessageDNSSECHrnResolver, Arc>; +pub enum HRNResolver { + Onion(Arc, Arc>>), + Local(Arc), +} -pub(crate) type DomainResolver = OMDomainResolver; +impl HrnResolver for HRNResolver { + fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a> { + match self { + HRNResolver::Onion(inner) => inner.resolve_hrn(hrn), + HRNResolver::Local(inner) => inner.resolve_hrn(hrn), + } + } + + fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'a> { + match self { + HRNResolver::Onion(inner) => inner.resolve_lnurl(url), + HRNResolver::Local(inner) => inner.resolve_lnurl(url), + } + } + + fn resolve_lnurl_to_invoice<'a>( + &'a self, callback_url: String, amount: Amount, expected_description_hash: [u8; 32], + ) -> LNURLResolutionFuture<'a> { + match self { + HRNResolver::Onion(inner) => { + inner.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash) + }, + HRNResolver::Local(inner) => { + inner.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash) + }, + } + } +} pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, From 6a95c19ce9075030047f90d0f621c0330837791a Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 2 Feb 2026 15:07:33 +0100 Subject: [PATCH 07/10] fixup! Add end-to-end test for HRN resolution --- tests/common/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 33ede4e98..08ac8a29a 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -27,7 +27,8 @@ 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, HumanReadableNamesConfig, + AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig, + HumanReadableNamesConfig, }; use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; use ldk_node::io::sqlite_store::SqliteStore; @@ -347,9 +348,10 @@ pub(crate) fn setup_two_nodes_with_store( 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(), + client_resolution_config: HRNResolverConfig::LocalDns { + dns_server_address: "8.8.8.8:53".to_string(), + }, + disable_hrn_resolution_service: false, }); } if allow_0conf { From e0d5d495a23ee38e6c37308b0b9592a04a1134be Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Mon, 2 Feb 2026 15:13:43 +0100 Subject: [PATCH 08/10] fixup! Pass HRNResolver or DomainResolver into OnionMessenger --- src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index c15e68759..dad7cda0e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -12,10 +12,10 @@ use std::sync::{Arc, Mutex}; use bitcoin_payment_instructions::amount::Amount; use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver; -use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use bitcoin_payment_instructions::hrn_resolution::{ HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture, }; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use bitcoin::secp256k1::PublicKey; use bitcoin::{OutPoint, ScriptBuf}; From 4f534c3295d65e9ad796724d22a4e53907b8f474 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Tue, 3 Feb 2026 06:24:51 +0100 Subject: [PATCH 09/10] fixup! Pass HRNResolver or DomainResolver into OnionMessenger Instantiate OMDomainResolver with tokio runtime --- src/builder.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/builder.rs b/src/builder.rs index 3949ac0ff..3c97ca26f 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1577,7 +1577,11 @@ fn build_with_store_internal( let service_dns_addr = dns_server_address .parse() .map_err(|_| BuildError::DNSResolverSetupFailed)?; - Arc::new(OMDomainResolver::new(service_dns_addr, Some(client_resolver))) + Arc::new(OMDomainResolver::with_runtime( + service_dns_addr, + Some(client_resolver), + Some(tokio::runtime::Handle::current()), + )) } else { log_error!(logger, "To act as an HRN resolution service, the DNS resolver must be configured to use a DNS server."); return Err(BuildError::DNSResolverSetupFailed); From 254e03c3f4774132e64cc3b5e097f6d04f86678d Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Tue, 3 Feb 2026 11:37:47 +0100 Subject: [PATCH 10/10] fixup! Pass HRNResolver or DomainResolver into OnionMessenger --- src/builder.rs | 6 ++---- src/runtime.rs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 3c97ca26f..0c980e01d 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1564,10 +1564,8 @@ fn build_with_store_internal( let should_act_as_service = if hrn_config.disable_hrn_resolution_service { false - } else if may_announce_channel(&config).is_ok() { - true } else { - false + may_announce_channel(&config).is_ok() }; if should_act_as_service { @@ -1580,7 +1578,7 @@ fn build_with_store_internal( Arc::new(OMDomainResolver::with_runtime( service_dns_addr, Some(client_resolver), - Some(tokio::runtime::Handle::current()), + Some(runtime.handle().clone()), )) } else { log_error!(logger, "To act as an HRN resolution service, the DNS resolver must be configured to use a DNS server."); diff --git a/src/runtime.rs b/src/runtime.rs index 39a34ddfe..2c4f9c700 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -208,7 +208,7 @@ impl Runtime { ); } - fn handle(&self) -> &tokio::runtime::Handle { + pub(crate) fn handle(&self) -> &tokio::runtime::Handle { match &self.mode { RuntimeMode::Owned(rt) => rt.handle(), RuntimeMode::Handle(handle) => handle,