From ab8fe4b366db13c525400c49bd972793e651ed6c Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 16 Oct 2025 08:33:00 +0100 Subject: [PATCH 1/5] 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 | 11 ++++++++ src/config.rs | 66 ++++++++++++++++++++++++++++++++++++++++++- src/ffi/types.rs | 3 +- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d40f72f4a..7d7bf4bb0 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 { @@ -517,6 +518,16 @@ dictionary RouteParametersConfig { u8 max_channel_saturation_power_of_half; }; +[Enum] +interface HRNResolverConfig { + Blip32(); + Dns(string dns_server_address, boolean enable_hrn_resolution_service); +}; + +dictionary HumanReadableNamesConfig { + HRNResolverConfig resolution_config; +}; + dictionary CustomTlvRecord { u64 type_num; sequence value; diff --git a/src/config.rs b/src/config.rs index 1dfa66176..cee86d7bb 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,18 @@ 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]). + /// + /// By default, this uses the `Dns` variant with the following settings: + /// * **DNS Server**: `8.8.8.8:53` (Google Public DNS) + /// * **Resolution Service**: Enabled (`true`) + /// + /// **Note:** Enabling `enable_hrn_resolution_service` is only one part of the + /// configuration. For resolution to function correctly, the local node must + /// also be configured as an **announceable node** within the network. + /// + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki + pub hrn_config: Option, } impl Default for Config { @@ -206,6 +219,57 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), route_parameters: None, node_alias: None, + hrn_config: Some(HumanReadableNamesConfig::default()), + } + } +} + +/// 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. + /// + /// [bLIP-32]: https://github.com/lightning/blips/blob/master/blip-0032.md + Blip32, + /// Resolve names locally using a specific DNS server. + Dns { + /// The IP and port of the DNS server. + /// **Default:** `8.8.8.8:53` (Google Public DNS) + dns_server_address: String, + /// If set to true, this allows others to use our node for HRN resolutions. + /// + /// **Note:** This feature requires the underlying node to be announceable. + enable_hrn_resolution_service: bool, + }, +} + +/// Configuration options for Human-Readable Names ([BIP 353]). +/// By default, this uses the `Dns` variant with the following settings: +/// * **DNS Server**: `8.8.8.8:53` (Google Public DNS) +/// * **Resolution Service**: Enabled (`true`) +/// +/// **Note:** Enabling `enable_hrn_resolution_service` is only one part of the +/// configuration. For resolution to function correctly, the local node must +/// also be configured as an **announceable node** within the network. +/// +/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki +#[derive(Debug, Clone)] +pub struct HumanReadableNamesConfig { + /// This sets how our node resolves names when we want to send a payment. + /// + /// By default, this uses the `Dns` variant with the following settings: + /// * **DNS Server**: `8.8.8.8:53` (Google Public DNS) + /// * **Resolution Service**: Enabled (`true`) + pub resolution_config: HRNResolverConfig, +} + +impl Default for HumanReadableNamesConfig { + fn default() -> Self { + HumanReadableNamesConfig { + resolution_config: HRNResolverConfig::Dns { + dns_server_address: "8.8.8.8:53".to_string(), + enable_hrn_resolution_service: true, + }, } } } diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 2a349a967..fe9d11437 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, MaxDustHTLCExposure, SyncTimeoutsConfig, + EsploraSyncConfig, HRNResolverConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, + SyncTimeoutsConfig, }; pub use crate::entropy::{generate_entropy_mnemonic, EntropyError, NodeEntropy, WordCount}; use crate::error::Error; From 91ccc403f38709bb753bb44a57532801a9ab0141 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:10:51 +0100 Subject: [PATCH 2/5] 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 | 3 ++ src/builder.rs | 111 +++++++++++++++++++++++++++++++++++------ src/error.rs | 5 ++ src/lib.rs | 2 +- src/payment/unified.rs | 19 +++---- src/runtime.rs | 2 +- src/types.rs | 46 +++++++++++++++-- 8 files changed, 164 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eda73f04f..0de3f5909 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 = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] } lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" } @@ -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 = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] } lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" } +lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" } 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 7d7bf4bb0..009cec5a0 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -365,6 +365,7 @@ enum NodeError { "InvalidBlindedPaths", "AsyncPaymentServicesDisabled", "HrnParsingFailed", + "HrnResolverNotConfigured", }; dictionary NodeStatus { @@ -399,6 +400,8 @@ enum BuildError { "LoggerSetupFailed", "NetworkMismatch", "AsyncPaymentsConfigMismatch", + "DNSResolverSetupFailed", + "PeerManagerSetupFailed", }; [Trait] diff --git a/src/builder.rs b/src/builder.rs index 7a285876f..b196a386b 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,12 +19,13 @@ 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}; 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,13 +40,15 @@ use lightning::util::persist::{ }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; +use lightning::{log_trace, log_warn}; +use lightning_dns_resolver::OMDomainResolver; use lightning_persister::fs_store::FilesystemStore; 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; @@ -76,8 +79,8 @@ 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, + HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, + PendingPaymentStore, Persister, SyncAndAsyncKVStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -189,6 +192,10 @@ 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, + /// Failed to set up the peer manager. + PeerManagerSetupFailed, } impl fmt::Display for BuildError { @@ -221,6 +228,12 @@ 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.") + }, + Self::PeerManagerSetupFailed => { + write!(f, "Failed to set up the peer manager.") + }, } } } @@ -1545,7 +1558,74 @@ fn build_with_store_internal( })?; } - let hrn_resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph))); + let peer_manager_hook: Arc>>> = Arc::new(Mutex::new(None)); + let mut hrn_resolver_out = None; + + let om_resolver: Arc = match &config.hrn_config { + None => Arc::new(IgnoringMessageHandler {}), + Some(hrn_config) => { + let runtime_handle = runtime.handle(); + + let client_resolver: Arc = + match &hrn_config.resolution_config { + HRNResolverConfig::Blip32 => { + 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(pm) = guard.as_ref().and_then(|weak| weak.upgrade()) { + pm.process_events(); + } + } + })); + hrn_res as Arc + }, + HRNResolverConfig::Dns { 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))); + + let resolver = + Arc::new(OMDomainResolver::::with_runtime( + addr, + None, + Some(runtime_handle.clone()), + )); + resolver as Arc + }, + }; + + if let HRNResolverConfig::Dns { + enable_hrn_resolution_service: true, + ref dns_server_address, + .. + } = hrn_config.resolution_config + { + if may_announce_channel(&config).is_ok() { + let service_dns_addr = dns_server_address + .parse() + .map_err(|_| BuildError::DNSResolverSetupFailed)?; + + Arc::new(OMDomainResolver::with_runtime( + service_dns_addr, + Some(client_resolver), + Some(runtime_handle.clone()), + )) as Arc + } else { + log_warn!(logger, "Unable to act as an HRN resolution service. To act as an HRN resolution service, the node must be configured to announce channels."); + client_resolver + } + } else { + client_resolver + } + }, + }; // Initialize the PeerManager let onion_messenger: Arc = @@ -1558,7 +1638,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 { @@ -1570,7 +1650,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 {}, )) }; @@ -1691,7 +1771,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); @@ -1702,12 +1782,11 @@ fn build_with_store_internal( Arc::clone(&keys_manager), )); - 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(); - } - })); + if let Ok(mut guard) = peer_manager_hook.lock() { + *guard = Some(Arc::downgrade(&peer_manager)); + } else { + return Err(BuildError::PeerManagerSetupFailed); + } liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::downgrade(&peer_manager))); @@ -1814,7 +1893,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, - hrn_resolver, + hrn_resolver: Arc::new(hrn_resolver_out), #[cfg(cycle_tests)] _leak_checker, }) diff --git a/src/error.rs b/src/error.rs index ea0bcca3b..9c0f1c6c1 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 2b60307b0..b5fbd46aa 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: Arc>>, #[cfg(cycle_tests)] _leak_checker: LeakChecker, } diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 671af14ff..3502cdf94 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); tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), fut) .await 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, diff --git a/src/types.rs b/src/types.rs index b5b1ffed7..dad7cda0e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,15 +10,23 @@ 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::hrn_resolution::{ + HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture, +}; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; + 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; 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}; @@ -289,11 +297,43 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, Arc, - Arc, + Arc, IgnoringMessageHandler, >; -pub(crate) type HRNResolver = LDKOnionMessageDNSSECHrnResolver, Arc>; +pub enum HRNResolver { + Onion(Arc, Arc>>), + Local(Arc), +} + +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 817c14c767dd1f16a5b386aeffba72ca63039185 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Thu, 4 Sep 2025 08:22:27 +0100 Subject: [PATCH 3/5] 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 + src/ffi/types.rs | 1 + src/payment/unified.rs | 61 +++++++++++++++++-------- tests/common/mod.rs | 12 ++++- tests/integration_tests_hrn.rs | 80 +++++++++++++++++++++++++++++++++ tests/integration_tests_rust.rs | 1 + 6 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 tests/integration_tests_hrn.rs diff --git a/Cargo.toml b/Cargo.toml index 0de3f5909..72a7a8b12 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,7 @@ check-cfg = [ "cfg(cln_test)", "cfg(lnd_test)", "cfg(cycle_tests)", + "cfg(hrn_tests)", ] [[bench]] diff --git a/src/ffi/types.rs b/src/ffi/types.rs index fe9d11437..7f1bd6425 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -298,6 +298,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/payment/unified.rs b/src/payment/unified.rs index 3502cdf94..87d6cd829 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, @@ -159,15 +163,30 @@ impl UnifiedPayment { /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki pub async fn send( &self, uri_str: &str, amount_msat: Option, - route_parameters: Option, + route_parameters: Option, #[cfg(hrn_tests)] test_offer: &Offer, ) -> 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 target_network; + + target_network = if let Ok(_) = HumanReadableName::from_encoded(uri_str) { + #[cfg(hrn_tests)] + { + bitcoin::Network::Bitcoin + } + #[cfg(not(hrn_tests))] + { + 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) @@ -230,21 +249,27 @@ impl UnifiedPayment { for method in sorted_payment_methods { match method { - 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 - }); + PaymentMethod::LightningBolt12(_offer) => { + #[cfg(not(hrn_tests))] + let offer = maybe_wrap(_offer); + + #[cfg(hrn_tests)] + let offer = maybe_wrap(test_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 + }) + }; 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 0413b4552..90778a955 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -26,7 +26,10 @@ 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, HRNResolverConfig, + HumanReadableNamesConfig, +}; use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; @@ -401,11 +404,18 @@ pub(crate) fn setup_two_nodes_with_store( println!("== Node A =="); let mut config_a = random_config(anchor_channels); config_a.store_type = store_type; + + if cfg!(hrn_tests) { + config_a.node_config.hrn_config = + Some(HumanReadableNamesConfig { resolution_config: HRNResolverConfig::Blip32 }); + } + let node_a = setup_node(chain_source, config_a); println!("\n== Node B =="); let mut config_b = random_config(anchor_channels); config_b.store_type = store_type; + if allow_0conf { config_b.node_config.trusted_peers_0conf.push(node_a.node_id()); } diff --git a/tests/integration_tests_hrn.rs b/tests/integration_tests_hrn.rs new file mode 100644 index 000000000..d927bac2e --- /dev/null +++ b/tests/integration_tests_hrn.rs @@ -0,0 +1,80 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +#![cfg(hrn_tests)] + +mod common; + +use bitcoin::Amount; +use common::{ + expect_channel_ready_event, expect_payment_successful_event, generate_blocks_and_wait, + open_channel, premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_nodes, + TestChainSource, +}; +use ldk_node::payment::UnifiedPaymentResult; +use ldk_node::Event; +use lightning::ln::channelmanager::PaymentId; + +#[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); + + 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_str = "matt@mattcorallo.com"; + + let offer_payment_id: PaymentId = + match node_a.unified_payment().send(&hrn_str, Some(1000000), None, &test_offer).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); +} diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index a16a395a6..8e9841291 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -33,6 +33,7 @@ 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}; From 8a4307d005d9735857e7b99e7566706606f24c8b Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Fri, 9 Jan 2026 18:54:19 +0100 Subject: [PATCH 4/5] 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/hrn-integration.yml | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/hrn-integration.yml diff --git a/.github/workflows/hrn-integration.yml b/.github/workflows/hrn-integration.yml new file mode 100644 index 000000000..041c595ca --- /dev/null +++ b/.github/workflows/hrn-integration.yml @@ -0,0 +1,46 @@ +name: CI Checks - HRN Integration Tests + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + hrn_integration: + name: HRN Tests - Ubuntu Stable + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install Rust stable + run: | + rustup update stable + rustup default stable + + - name: Enable caching for bitcoind/electrs + id: cache-deps + uses: actions/cache@v4 + with: + path: bin/ + key: hrn-deps-${{ runner.os }}-${{ hashFiles('scripts/download_bitcoind_electrs.sh') }} + + - name: Download bitcoind/electrs + if: steps.cache-deps.outputs.cache-hit != 'true' + run: | + source ./scripts/download_bitcoind_electrs.sh + mkdir -p bin + mv "$BITCOIND_EXE" bin/bitcoind + mv "$ELECTRS_EXE" bin/electrs + + - name: Set environment variables + run: | + echo "BITCOIND_EXE=$(pwd)/bin/bitcoind" >> "$GITHUB_ENV" + echo "ELECTRS_EXE=$(pwd)/bin/electrs" >> "$GITHUB_ENV" + + - name: Run HRN Integration Tests + env: + RUSTFLAGS: "--cfg no_download --cfg hrn_tests" + run: cargo test --test integration_tests_hrn \ No newline at end of file From 4e0bd6d47943fff86e6693731cf0b470b28339d8 Mon Sep 17 00:00:00 2001 From: Chuks Agbakuru Date: Sun, 22 Feb 2026 17:17:56 +0100 Subject: [PATCH 5/5] Implement bLIP-32 resolver discovery via network graph Adds logic to scan the network graph for nodes that support both DNS resolution and Onion Messaging. - Iterates through the network graph to find up to 5 candidate nodes. - Validates node features for DNS and Onion Message support. - Adds error handling and logging for missing BIP-353 requirements. --- bindings/ldk_node.udl | 1 + src/error.rs | 5 +++++ src/lib.rs | 2 ++ src/payment/unified.rs | 39 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 009cec5a0..f411b871e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -366,6 +366,7 @@ enum NodeError { "AsyncPaymentServicesDisabled", "HrnParsingFailed", "HrnResolverNotConfigured", + "NoDnsResolvers", }; dictionary NodeStatus { diff --git a/src/error.rs b/src/error.rs index 9c0f1c6c1..ddbcf5320 100644 --- a/src/error.rs +++ b/src/error.rs @@ -133,6 +133,8 @@ pub enum Error { HrnParsingFailed, /// A HRN resolver was not configured. HrnResolverNotConfigured, + /// No nodes in the network graph support both DNS resolution and onion messaging, which are required for bLIP-32. + NoDnsResolvers, } impl fmt::Display for Error { @@ -218,6 +220,9 @@ impl fmt::Display for Error { Self::HrnResolverNotConfigured => { write!(f, "A HRN resolver was not configured.") }, + Self::NoDnsResolvers => { + write!(f, "No nodes in the network graph support both DNS resolution and onion messaging, which are required for bLIP-32.") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index b5fbd46aa..9aa373e20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -980,6 +980,7 @@ impl Node { Arc::clone(&self.config), Arc::clone(&self.logger), Arc::clone(&self.hrn_resolver), + Arc::clone(&self.network_graph), ) } @@ -1001,6 +1002,7 @@ impl Node { Arc::clone(&self.config), Arc::clone(&self.logger), Arc::clone(&self.hrn_resolver), + Arc::clone(&self.network_graph), )) } diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 87d6cd829..b66e5e4d9 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -34,7 +34,7 @@ use crate::error::Error; use crate::ffi::maybe_wrap; use crate::logger::{log_error, LdkLogger, Logger}; use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; -use crate::types::HRNResolver; +use crate::types::{Graph, HRNResolver}; use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; @@ -69,15 +69,24 @@ pub struct UnifiedPayment { config: Arc, logger: Arc, hrn_resolver: Arc>>, + network_graph: 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>>, network_graph: Arc, ) -> Self { - Self { onchain_payment, bolt11_invoice, bolt12_payment, config, logger, hrn_resolver } + Self { + onchain_payment, + bolt11_invoice, + bolt12_payment, + config, + logger, + hrn_resolver, + network_graph, + } } /// Generates a URI with an on-chain address, [BOLT 11] invoice and [BOLT 12] offer. @@ -165,6 +174,30 @@ impl UnifiedPayment { &self, uri_str: &str, amount_msat: Option, route_parameters: Option, #[cfg(hrn_tests)] test_offer: &Offer, ) -> Result { + let dns_resolvers = { + let mut resolvers = Vec::new(); + let network_graph = self.network_graph.read_only(); + + for (node_id, node) in network_graph.nodes().unordered_iter() { + if let Some(info) = &node.announcement_info { + let supports_dns = info.features().supports_dns_resolution(); + let supports_om = info.features().supports_onion_messages(); + if supports_dns && supports_om { + resolvers.push(node_id.clone()); + } + } + if resolvers.len() > 5 { + break; + } + } + resolvers + }; + + if dns_resolvers.is_empty() { + log_error!(self.logger, "No nodes in the network graph support both DNS resolution and onion messaging, which are required for bLIP-32. Please ensure you have at least one such node in your network graph before making BIP-353 payments."); + return Err(Error::NoDnsResolvers); + } + let resolver = self.hrn_resolver.as_ref().clone().ok_or_else(|| { log_error!(self.logger, "No HRN resolver configured. Cannot resolve HRNs."); Error::HrnResolverNotConfigured