diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 661703ded..061aa8fb2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -85,6 +85,14 @@ jobs: if: "matrix.platform != 'windows-latest' && matrix.build-uniffi" run: | RUSTFLAGS="--cfg no_download" cargo test --features uniffi + - name: Test with HRN overrides (No UniFFI) on Rust ${{ matrix.toolchain }} + if: "matrix.platform == 'ubuntu-latest' && matrix.toolchain == 'stable'" + run: | + RUSTFLAGS="--cfg no_download" cargo test --features hrn_tests + - name: Test with UniFFI and HRN overrides on Rust ${{ matrix.toolchain }} + if: "matrix.platform != 'windows-latest' && matrix.build-uniffi" + run: | + RUSTFLAGS="--cfg no_download" cargo test --features uniffi,hrn_tests doc: name: Documentation diff --git a/Cargo.toml b/Cargo.toml index 4f8c0ed7b..291c6bafe 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ panic = 'abort' # Abort on panic [features] default = [] +hrn_tests = [] [dependencies] #lightning = { version = "0.2.0", features = ["std"] } @@ -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 = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0", features = ["std"] } lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0" } @@ -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 = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0", features = ["std"] } lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0" } +lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "1c730c8a16e28cc8e0c4817717ee63c97abcf4b0" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -140,6 +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" } @@ -152,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" } @@ -164,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" } 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 b59a38b04..2849e7fec 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 { @@ -348,6 +349,7 @@ enum NodeError { "InvalidBlindedPaths", "AsyncPaymentServicesDisabled", "HrnParsingFailed", + "HrnResolverNotConfigured", }; dictionary NodeStatus { @@ -382,6 +384,7 @@ enum BuildError { "LoggerSetupFailed", "NetworkMismatch", "AsyncPaymentsConfigMismatch", + "DNSResolverSetupFailed", }; [Trait] @@ -501,6 +504,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/builder.rs b/src/builder.rs index c1acf71d4..da01cbce8 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -25,6 +25,7 @@ use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArg use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler}; use lightning::log_trace; +use lightning::onion_message::dns_resolution::DNSResolverMessageHandler; use lightning::routing::gossip::NodeAlias; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ @@ -38,6 +39,7 @@ use lightning::util::persist::{ }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; +use lightning_dns_resolver::OMDomainResolver; use lightning_persister::fs_store::FilesystemStore; use vss_client::headers::VssHeaderProvider; @@ -72,8 +74,9 @@ use crate::peer_store::PeerStore; use crate::runtime::Runtime; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ - ChainMonitor, ChannelManager, DynStore, DynStoreWrapper, GossipSync, Graph, KeysManager, - MessageRouter, OnionMessenger, PaymentStore, PeerManager, Persister, SyncAndAsyncKVStore, + ChainMonitor, ChannelManager, DomainResolver, DynStore, DynStoreWrapper, GossipSync, Graph, + HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, Persister, + SyncAndAsyncKVStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -185,6 +188,8 @@ pub enum BuildError { NetworkMismatch, /// The role of the node in an asynchronous payments context is not compatible with the current configuration. AsyncPaymentsConfigMismatch, + /// An attempt to setup a DNS Resolver failed. + DNSResolverSetupFailed, } impl fmt::Display for BuildError { @@ -217,12 +222,21 @@ impl fmt::Display for BuildError { "The async payments role is not compatible with the current configuration." ) }, + Self::DNSResolverSetupFailed => { + write!(f, "An attempt to setup a DNS resolver has failed.") + }, } } } impl std::error::Error for BuildError {} +enum Resolver { + HRN(Arc), + DNS(Arc), + Ignore(Arc), +} + /// A builder for an [`Node`] instance, allowing to set some configuration and module choices from /// the getgo. /// @@ -1444,7 +1458,34 @@ fn build_with_store_internal( })?; } - let hrn_resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph))); + let resolver = if let Some(hrn_config) = &config.hrn_config { + if hrn_config.is_hrn_resolver { + let dns_addr = hrn_config.dns_server_address.as_str(); + + Resolver::DNS(Arc::new(OMDomainResolver::ignoring_incoming_proofs( + dns_addr.parse().map_err(|_| BuildError::DNSResolverSetupFailed)?, + ))) + } else { + Resolver::HRN(Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone( + &network_graph, + )))) + } + } else { + // hrn_config is None, default to the IgnoringMessaageHandler. + Resolver::Ignore(Arc::new(IgnoringMessageHandler {})) + }; + + let om_resolver = match resolver { + Resolver::DNS(ref dns_resolver) => { + Arc::clone(dns_resolver) as Arc + }, + Resolver::HRN(ref hrn_resolver) => { + Arc::clone(hrn_resolver) as Arc + }, + Resolver::Ignore(ref ignoring_handler) => { + Arc::clone(ignoring_handler) as Arc + }, + }; // Initialize the PeerManager let onion_messenger: Arc = @@ -1457,7 +1498,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - Arc::clone(&hrn_resolver), + Arc::clone(&om_resolver), IgnoringMessageHandler {}, )) } else { @@ -1469,7 +1510,7 @@ fn build_with_store_internal( message_router, Arc::clone(&channel_manager), Arc::clone(&channel_manager), - Arc::clone(&hrn_resolver), + Arc::clone(&om_resolver), IgnoringMessageHandler {}, )) }; @@ -1599,9 +1640,16 @@ fn build_with_store_internal( let peer_manager_clone = Arc::clone(&peer_manager); - hrn_resolver.register_post_queue_action(Box::new(move || { - peer_manager_clone.process_events(); - })); + let hrn_resolver = match resolver { + Resolver::DNS(_) => None, + Resolver::HRN(ref hrn_resolver) => { + hrn_resolver.register_post_queue_action(Box::new(move || { + peer_manager_clone.process_events(); + })); + Some(hrn_resolver) + }, + Resolver::Ignore(_) => None, + }; liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::clone(&peer_manager))); @@ -1716,7 +1764,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, - hrn_resolver, + hrn_resolver: hrn_resolver.cloned(), }) } diff --git a/src/config.rs b/src/config.rs index 1b71d0d4e..77c85c49b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -119,7 +119,8 @@ pub(crate) const HRN_RESOLUTION_TIMEOUT_SECS: u64 = 5; /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | /// | `anchor_channels_config` | Some(..) | -/// | `route_parameters` | None | +/// | `route_parameters` | None | +/// | `hrn_config` | Some(..) | /// /// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their /// respective default values. @@ -184,6 +185,10 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub route_parameters: Option, + /// Configuration options for Human-Readable Names ([BIP 353]). + /// + /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki + pub hrn_config: Option, } impl Default for Config { @@ -198,6 +203,34 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), route_parameters: None, node_alias: None, + hrn_config: Some(HumanReadableNamesConfig::default()), + } + } +} + +/// Configuration options for Human-Readable Names ([BIP 353]). +/// +/// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki +#[derive(Debug, Clone)] +pub struct HumanReadableNamesConfig { + /// The Default DNS resolvers to be used for resolving Human-Readable Names. + /// + /// If not empty, the values set will be used as DNS resolvers when sending to HRNs. + /// + /// **Note:** If empty, DNS resolvers would be selected from the network graph. + pub default_dns_resolvers: Vec, + /// This allows us to use our node as a DNS resolver for 3rd party HRN resolutions. + pub is_hrn_resolver: bool, + /// The DNS Server which will be used for resolving HRNs. + pub dns_server_address: String, +} + +impl Default for HumanReadableNamesConfig { + fn default() -> Self { + HumanReadableNamesConfig { + default_dns_resolvers: Vec::new(), + is_hrn_resolver: false, + dns_server_address: String::new(), } } } diff --git a/src/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 bed040fcd..4707a3451 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -46,7 +46,7 @@ pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; use crate::builder::sanitize_alias; pub use crate::config::{ default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, - EsploraSyncConfig, MaxDustHTLCExposure, + EsploraSyncConfig, HumanReadableNamesConfig, MaxDustHTLCExposure, }; pub use crate::entropy::{generate_entropy_mnemonic, EntropyError, NodeEntropy, WordCount}; use crate::error::Error; @@ -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 cf728c8bf..6c27d1df3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,7 +207,7 @@ pub struct Node { node_metrics: Arc>, om_mailbox: Option>, async_payments_role: Option, - hrn_resolver: Arc, + hrn_resolver: Option>, } impl Node { @@ -958,7 +958,7 @@ impl Node { self.bolt12_payment().into(), Arc::clone(&self.config), Arc::clone(&self.logger), - Arc::clone(&self.hrn_resolver), + Arc::new(self.hrn_resolver.clone()), ) } @@ -979,7 +979,7 @@ impl Node { self.bolt12_payment(), Arc::clone(&self.config), Arc::clone(&self.logger), - Arc::clone(&self.hrn_resolver), + Arc::new(self.hrn_resolver.clone()), )) } @@ -1859,3 +1859,62 @@ pub(crate) fn total_anchor_channels_reserve_sats( * anchor_channels_config.per_channel_reserve_sats }) } + +/// Testing utils for DNSSEC proof resolution of offers associated with the given Human-Readable Name. + +#[cfg(feature = "hrn_tests")] +pub mod dnssec_testing_utils { + use std::collections::HashMap; + #[cfg(feature = "uniffi")] + use std::sync::Arc; + use std::sync::{LazyLock, Mutex}; + + #[cfg(not(feature = "uniffi"))] + type Offer = lightning::offers::offer::Offer; + #[cfg(feature = "uniffi")] + type Offer = Arc; + + #[cfg(not(feature = "uniffi"))] + type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadableName; + #[cfg(feature = "uniffi")] + type HumanReadableName = Arc; + + static OFFER_OVERRIDE_MAP: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + + /// Sets a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Name. + pub fn set_testing_dnssec_proof_offer_resolution_override(hrn: &str, offer: Offer) { + let hrn_key = { + #[cfg(not(feature = "uniffi"))] + { + lightning::onion_message::dns_resolution::HumanReadableName::from_encoded(hrn) + .unwrap() + } + + #[cfg(feature = "uniffi")] + { + Arc::new(crate::ffi::HumanReadableName::from_encoded(hrn).unwrap()) + } + }; + + OFFER_OVERRIDE_MAP.lock().unwrap().insert(hrn_key, offer); + } + + /// Retrieves a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Names. + #[cfg(not(feature = "uniffi"))] + pub fn get_testing_offer_override(hrn: Option) -> Option { + OFFER_OVERRIDE_MAP.lock().unwrap().get(&hrn?).cloned() + } + + /// Retrieves a testing override for DNSSEC proof resolution of offers associated with the given Human-Readable Names. + #[cfg(feature = "uniffi")] + pub fn get_testing_offer_override(hrn: Option) -> Option { + let offer = OFFER_OVERRIDE_MAP.lock().unwrap().get(&hrn?).cloned().unwrap(); + Some(offer) + } + + /// Clears all testing overrides for DNSSEC proof resolution of offers. + pub fn clear_testing_overrides() { + OFFER_OVERRIDE_MAP.lock().unwrap().clear(); + } +} diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 98f1d21ef..04f2b037f 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -234,7 +234,26 @@ impl Bolt12Payment { return Err(Error::NotRunning); } - let offer = maybe_deref(offer); + let offer = if let Some(_hrn_ref) = &hrn { + #[cfg(feature = "hrn_tests")] + { + crate::dnssec_testing_utils::get_testing_offer_override(Some(_hrn_ref.clone())) + .map(|override_offer| { + log_info!(self.logger, "Using test-specific Offer override."); + override_offer + }) + .unwrap_or_else(|| offer.clone()) + } + + #[cfg(not(feature = "hrn_tests"))] + { + offer.clone() + } + } else { + offer.clone() + }; + + let offer = maybe_deref(&offer); let mut random_bytes = [0u8; 32]; rand::rng().fill_bytes(&mut random_bytes); diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 671af14ff..59880405d 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -26,7 +26,7 @@ use bitcoin_payment_instructions::amount::Amount as BPIAmount; use bitcoin_payment_instructions::{PaymentInstructions, PaymentMethod}; use lightning::ln::channelmanager::PaymentId; use lightning::offers::offer::Offer; -use lightning::onion_message::dns_resolution::HumanReadableName; +use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; use lightning::routing::router::RouteParametersConfig; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; @@ -40,6 +40,11 @@ use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; +#[cfg(not(feature = "uniffi"))] +type HumanReadableName = LdkHumanReadableName; +#[cfg(feature = "uniffi")] +type HumanReadableName = Arc; + #[derive(Debug, Clone)] struct Extras { bolt11_invoice: Option, @@ -64,14 +69,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 +166,33 @@ 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) = LdkHumanReadableName::from_encoded(uri_str) { + #[cfg(feature = "hrn_tests")] + { + let hrn_wrapped: HumanReadableName = maybe_wrap(hrn); + match crate::dnssec_testing_utils::get_testing_offer_override(Some(hrn_wrapped)) { + Some(_) => bitcoin::Network::Bitcoin, + _ => self.config.network, + } + } + #[cfg(not(feature = "hrn_tests"))] + { + let _ = hrn; + self.config.network + } + } else { + self.config.network + }; + + let parse_fut = + PaymentInstructions::parse(uri_str, target_network, resolver.as_ref(), false); let instructions = tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), parse_fut) @@ -192,7 +218,7 @@ impl UnifiedPayment { Error::InvalidAmount })?; - let fut = instr.set_amount(amt, self.hrn_resolver.as_ref()); + let fut = instr.set_amount(amt, resolver.as_ref()); tokio::time::timeout(Duration::from_secs(HRN_RESOLUTION_TIMEOUT_SECS), fut) .await @@ -232,18 +258,20 @@ impl UnifiedPayment { PaymentMethod::LightningBolt12(offer) => { let offer = maybe_wrap(offer.clone()); - let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) { - let hrn = maybe_wrap(hrn.clone()); - self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn)) - } else if let Some(amount_msat) = amount_msat { - self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters) - } else { - self.bolt12_payment.send(&offer, None, None, route_parameters) - } - .map_err(|e| { - log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e); - e - }); + let payment_result = { + if let Ok(hrn) = LdkHumanReadableName::from_encoded(uri_str) { + let hrn = maybe_wrap(hrn.clone()); + self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn)) + } else if let Some(amount_msat) = amount_msat { + self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters) + } else { + self.bolt12_payment.send(&offer, None, None, route_parameters) + } + .map_err(|e| { + log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified payment. Falling back to the BOLT11 invoice.", e); + e + }) + }; if let Ok(payment_id) = payment_result { return Ok(UnifiedPaymentResult::Bolt12 { payment_id }); diff --git a/src/types.rs b/src/types.rs index 5e9cd74c9..0db91b65a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -19,6 +19,7 @@ use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::types::ChannelId; +use lightning::onion_message::dns_resolution::DNSResolverMessageHandler; use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{CombinedScorer, ProbabilisticScoringFeeParameters}; @@ -27,6 +28,7 @@ use lightning::util::persist::{KVStore, KVStoreSync, MonitorUpdatingPersister}; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::GossipVerifier; +use lightning_dns_resolver::OMDomainResolver; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; @@ -277,12 +279,14 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, Arc, - Arc, + Arc, IgnoringMessageHandler, >; pub(crate) type HRNResolver = LDKOnionMessageDNSSECHrnResolver, Arc>; +pub(crate) type DomainResolver = OMDomainResolver; + pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, Arc, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 96f58297c..40abc6c90 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -26,7 +26,9 @@ use bitcoin::{ use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD}; use electrsd::{corepc_node, ElectrsD}; use electrum_client::ElectrumApi; -use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig}; +use ldk_node::config::{ + AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HumanReadableNamesConfig, +}; use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; @@ -319,7 +321,7 @@ pub(crate) use setup_builder; pub(crate) fn setup_two_nodes( chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, - anchors_trusted_no_reserve: bool, + anchors_trusted_no_reserve: bool, second_node_is_hrn_resolver: bool, ) -> (TestNode, TestNode) { setup_two_nodes_with_store( chain_source, @@ -327,12 +329,13 @@ pub(crate) fn setup_two_nodes( anchor_channels, anchors_trusted_no_reserve, TestStoreType::TestSyncStore, + second_node_is_hrn_resolver, ) } pub(crate) fn setup_two_nodes_with_store( chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, - anchors_trusted_no_reserve: bool, store_type: TestStoreType, + anchors_trusted_no_reserve: bool, store_type: TestStoreType, second_node_is_hrn_resolver: bool, ) -> (TestNode, TestNode) { println!("== Node A =="); let mut config_a = random_config(anchor_channels); @@ -342,6 +345,13 @@ pub(crate) fn setup_two_nodes_with_store( println!("\n== Node B =="); let mut config_b = random_config(anchor_channels); config_b.store_type = store_type; + if second_node_is_hrn_resolver { + config_b.node_config.hrn_config = Some(HumanReadableNamesConfig { + default_dns_resolvers: Vec::new(), + is_hrn_resolver: true, + dns_server_address: "8.8.8.8:53".to_string(), + }); + } if allow_0conf { config_b.node_config.trusted_peers_0conf.push(node_a.node_id()); } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 4b82d1f4f..ba4205ad3 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -27,12 +27,15 @@ use common::{ TestSyncStore, }; use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; +#[cfg(feature = "hrn_tests")] +use ldk_node::dnssec_testing_utils; use ldk_node::entropy::NodeEntropy; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, UnifiedPaymentResult, }; + use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; @@ -45,7 +48,7 @@ use log::LevelFilter; async fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; } @@ -54,7 +57,7 @@ async fn channel_full_cycle() { async fn channel_full_cycle_electrum() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Electrum(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; } @@ -63,7 +66,7 @@ async fn channel_full_cycle_electrum() { async fn channel_full_cycle_bitcoind_rpc_sync() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; } @@ -72,7 +75,7 @@ async fn channel_full_cycle_bitcoind_rpc_sync() { async fn channel_full_cycle_bitcoind_rest_sync() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::BitcoindRestSync(&bitcoind); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) .await; } @@ -81,7 +84,7 @@ async fn channel_full_cycle_bitcoind_rest_sync() { async fn channel_full_cycle_force_close() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) .await; } @@ -90,7 +93,7 @@ async fn channel_full_cycle_force_close() { async fn channel_full_cycle_force_close_trusted_no_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) .await; } @@ -99,7 +102,7 @@ async fn channel_full_cycle_force_close_trusted_no_reserve() { async fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true, false) .await; } @@ -108,7 +111,7 @@ async fn channel_full_cycle_0conf() { async fn channel_full_cycle_legacy_staticremotekey() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false, false) .await; } @@ -117,7 +120,7 @@ async fn channel_full_cycle_legacy_staticremotekey() { async fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -322,7 +325,7 @@ async fn start_stop_reinit() { async fn onchain_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -523,7 +526,7 @@ async fn onchain_send_receive() { async fn onchain_send_all_retains_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); // Setup nodes let addr_a = node_a.onchain_payment().new_address().unwrap(); @@ -838,7 +841,7 @@ async fn sign_verify_msg() { async fn connection_multi_listen() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false, false); let node_id_b = node_b.node_id(); @@ -858,7 +861,7 @@ async fn connection_restart_behavior() { async fn do_connection_restart_behavior(persist: bool) { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false, false); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -905,7 +908,7 @@ async fn do_connection_restart_behavior(persist: bool) { async fn concurrent_connections_succeed() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b); @@ -935,7 +938,7 @@ async fn run_splice_channel_test(bitcoind_chain_source: bool) { } else { TestChainSource::Esplora(&electrsd) }; - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let address_b = node_b.onchain_payment().new_address().unwrap(); @@ -1080,7 +1083,7 @@ async fn splice_channel() { async fn simple_bolt12_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_amount_sat = 5_000_000; @@ -1537,7 +1540,7 @@ async fn generate_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -1592,7 +1595,7 @@ async fn unified_send_receive_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -1699,6 +1702,72 @@ async fn unified_send_receive_bip21_uri() { assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } +#[cfg(feature = "hrn_tests")] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn unified_send_to_hrn() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, true); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premined_sats = 5_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premined_sats), + ) + .await; + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Sleep until we broadcast a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let test_offer = node_b.bolt12_payment().receive(1000000, "test offer", None, None).unwrap(); + + // Sleep one more sec to make sure the node announcement propagates. + std::thread::sleep(std::time::Duration::from_secs(1)); + + let hrn = "matt@mattcorallo.com"; + + dnssec_testing_utils::set_testing_dnssec_proof_offer_resolution_override( + hrn, + test_offer.clone(), + ); + + let offer_payment_id: PaymentId = + match node_a.unified_payment().send(&hrn, Some(1000000), None).await { + Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => { + println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected Bolt12 payment but got Bolt11"); + }, + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt12 payment but got On-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt12 payment but got error: {:?}", e); + }, + }; + + expect_payment_successful_event!(node_a, Some(offer_payment_id), None); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn lsps2_client_service_integration() { do_lsps2_client_service_integration(true).await; @@ -1948,7 +2017,7 @@ async fn facade_logging() { async fn spontaneous_send_with_custom_preimage() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_sat = 1_000_000;