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 diff --git a/Cargo.toml b/Cargo.toml index 3dcad31a5..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"] } @@ -38,6 +39,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 +52,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 +144,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 +157,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 +170,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 +187,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/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/bindings/ldk_node.udl b/bindings/ldk_node.udl index 6bd031379..dfb12a4c0 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 { @@ -361,6 +362,7 @@ enum NodeError { "InvalidBlindedPaths", "AsyncPaymentServicesDisabled", "HrnParsingFailed", + "HrnResolverNotConfigured", }; dictionary NodeStatus { @@ -395,6 +397,7 @@ enum BuildError { "LoggerSetupFailed", "NetworkMismatch", "AsyncPaymentsConfigMismatch", + "DNSResolverSetupFailed", }; [Trait] @@ -514,6 +517,17 @@ dictionary RouteParametersConfig { u8 max_channel_saturation_power_of_half; }; +[Enum] +interface HRNResolverConfig { + Blip32Onion(); + LocalDns(string dns_server_address); +}; + +dictionary HumanReadableNamesConfig { + HRNResolverConfig client_resolution_config; + boolean disable_hrn_resolution_service; +}; + dictionary CustomTlvRecord { u64 type_num; sequence value; diff --git a/src/builder.rs b/src/builder.rs index 5d8a5a7a9..0c980e01d 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,14 @@ 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 +41,14 @@ 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; 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,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,6 +226,9 @@ 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.") + }, } } } @@ -1517,7 +1525,70 @@ 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 = 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 { + may_announce_channel(&config).is_ok() + }; + + 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::with_runtime( + service_dns_addr, + Some(client_resolver), + 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."); + return Err(BuildError::DNSResolverSetupFailed); + } + } else { + client_resolver + } + }, + }; // Initialize the PeerManager let onion_messenger: Arc = @@ -1530,7 +1601,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 +1613,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 {}, )) }; @@ -1663,7 +1734,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); @@ -1674,12 +1745,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::DNSResolverSetupFailed); + } liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::downgrade(&peer_manager))); @@ -1786,7 +1856,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, - hrn_resolver, + hrn_resolver: hrn_resolver_out, #[cfg(cycle_tests)] _leak_checker, }) diff --git a/src/config.rs b/src/config.rs index 103b74657..653f42c85 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,41 @@ 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. + 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 { + /// 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 { + client_resolution_config: HRNResolverConfig::LocalDns { + dns_server_address: "8.8.8.8:53".to_string(), + }, + disable_hrn_resolution_service: false, } } } 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/ffi/types.rs b/src/ffi/types.rs index 2a349a967..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, MaxDustHTLCExposure, SyncTimeoutsConfig, + EsploraSyncConfig, HRNResolverConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, + SyncTimeoutsConfig, }; pub use crate::entropy::{generate_entropy_mnemonic, EntropyError, NodeEntropy, WordCount}; use crate::error::Error; @@ -297,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/lib.rs b/src/lib.rs index d2222d949..f12f88e46 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()), )) } @@ -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 671af14ff..0a5e9782c 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, @@ -64,14 +68,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 +165,37 @@ 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 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, target_network, &*resolver, false); let instructions = tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut) @@ -192,7 +221,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 @@ -232,18 +261,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/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, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 5f6657260..08ac8a29a 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}; @@ -319,7 +322,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 +330,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 +346,14 @@ 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 { + client_resolution_config: HRNResolverConfig::LocalDns { + dns_server_address: "8.8.8.8:53".to_string(), + }, + disable_hrn_resolution_service: false, + }); + } 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;