From 99d00930a44989ee7be47a6f5d596c40919a1dd0 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 7 Nov 2024 15:05:26 +0000 Subject: [PATCH] Support paying Human Readable Names directly from `ChannelManager` Now that we have the ability to resolve BIP 353 Human Readable Names directly and have tracking for outbound payments waiting on an offer resolution, we can implement full BIP 353 support in `ChannelManager`. Users will need one or more known nodes which offer DNS resolution service over onion messages using bLIP 32, which they pass to `ChannelManager::pay_for_offer_from_human_readable_name`, as well as the `HumanReadableName` itself. From there, `ChannelManager` asks the DNS resolver to provide a DNSSEC proof, which it verifies, parses into an `Offer`, and then pays. For those who wish to support on-chain fallbacks, sadly, this will not work, and they'll still have to use `OMNameResolver` directly in order to use their existing `bitcoin:` URI parsing. --- lightning/src/ln/channelmanager.rs | 184 +++++++++++++++++++++++++-- lightning/src/ln/outbound_payment.rs | 56 ++++++++ 2 files changed, 229 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b6b3f4fd7..8ddb2aa11 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -75,6 +75,7 @@ use crate::offers::signer; #[cfg(async_payments)] use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler}; +use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; @@ -87,6 +88,11 @@ use crate::util::ser::{BigSize, FixedLengthReader, Readable, ReadableArgs, Maybe use crate::util::logger::{Level, Logger, WithContext}; use crate::util::errors::APIError; +#[cfg(feature = "dnssec")] +use crate::blinded_path::message::DNSResolverContext; +#[cfg(feature = "dnssec")] +use crate::onion_message::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler, DNSSECQuery, DNSSECProof, OMNameResolver}; + #[cfg(not(c_bindings))] use { crate::offers::offer::DerivedMetadata, @@ -2564,6 +2570,11 @@ where /// [`ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee`] estimate. last_days_feerates: Mutex>, + #[cfg(feature = "dnssec")] + hrn_resolver: OMNameResolver, + #[cfg(feature = "dnssec")] + pending_dns_onion_messages: Mutex>, + entropy_source: ES, node_signer: NS, signer_provider: SP, @@ -3386,6 +3397,11 @@ where signer_provider, logger, + + #[cfg(feature = "dnssec")] + hrn_resolver: OMNameResolver::new(current_timestamp, params.best_block.height), + #[cfg(feature = "dnssec")] + pending_dns_onion_messages: Mutex::new(Vec::new()), } } @@ -9579,6 +9595,26 @@ where &self, offer: &Offer, quantity: Option, amount_msats: Option, payer_note: Option, payment_id: PaymentId, retry_strategy: Retry, max_total_routing_fee_msat: Option + ) -> Result<(), Bolt12SemanticError> { + self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, None, |invoice_request, nonce| { + let expiration = StaleExpiration::TimerTicks(1); + let retryable_invoice_request = RetryableInvoiceRequest { + invoice_request: invoice_request.clone(), + nonce, + }; + self.pending_outbound_payments + .add_new_awaiting_invoice( + payment_id, expiration, retry_strategy, max_total_routing_fee_msat, + Some(retryable_invoice_request) + ) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) + }) + } + + fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( + &self, offer: &Offer, quantity: Option, amount_msats: Option, + payer_note: Option, payment_id: PaymentId, + human_readable_name: Option, create_pending_payment: CPP, ) -> Result<(), Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; @@ -9602,6 +9638,10 @@ where None => builder, Some(payer_note) => builder.payer_note(payer_note), }; + let builder = match human_readable_name { + None => builder, + Some(hrn) => builder.sourced_from_human_readable_name(hrn), + }; let invoice_request = builder.build_and_sign()?; let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); @@ -9613,17 +9653,7 @@ where let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - let expiration = StaleExpiration::TimerTicks(1); - let retryable_invoice_request = RetryableInvoiceRequest { - invoice_request: invoice_request.clone(), - nonce, - }; - self.pending_outbound_payments - .add_new_awaiting_invoice( - payment_id, expiration, retry_strategy, max_total_routing_fee_msat, - Some(retryable_invoice_request) - ) - .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?; + create_pending_payment(&invoice_request, nonce)?; self.enqueue_invoice_request(invoice_request, reply_paths) } @@ -9764,6 +9794,73 @@ where } } + /// Pays for an [`Offer`] looked up using [BIP 353] Human Readable Names resolved by the DNS + /// resolver(s) at `dns_resolvers` which resolve names according to bLIP 32. + /// + /// If the wallet supports paying on-chain schemes, you should instead use + /// [`OMNameResolver::resolve_name`] and [`OMNameResolver::handle_dnssec_proof_for_uri`] (by + /// implementing [`DNSResolverMessageHandler`]) directly to look up a URI and then delegate to + /// your normal URI handling. + /// + /// If `max_total_routing_fee_msat` is not specified, the default from + /// [`RouteParameters::from_payment_params_and_value`] is applied. + /// + /// # Payment + /// + /// The provided `payment_id` is used to ensure that only one invoice is paid for the request + /// when received. See [Avoiding Duplicate Payments] for other requirements once the payment has + /// been sent. + /// + /// To revoke the request, use [`ChannelManager::abandon_payment`] prior to receiving the + /// invoice. If abandoned, or an invoice isn't received in a reasonable amount of time, the + /// payment will fail with an [`Event::InvoiceRequestFailed`]. + /// + /// # Privacy + /// + /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] + /// to construct a [`BlindedPath`] for the reply path. For further privacy implications, see the + /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. + /// + /// # Limitations + /// + /// Requires a direct connection to the given [`Destination`] as well as an introduction node in + /// [`Offer::paths`] or to [`Offer::signing_pubkey`], if empty. A similar restriction applies to + /// the responding [`Bolt12Invoice::payment_paths`]. + /// + /// # Errors + /// + /// Errors if: + /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, + /// + /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths + /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments + #[cfg(feature = "dnssec")] + pub fn pay_for_offer_from_human_readable_name( + &self, name: HumanReadableName, amount_msats: u64, payment_id: PaymentId, + retry_strategy: Retry, max_total_routing_fee_msat: Option, + dns_resolvers: Vec, + ) -> Result<(), ()> { + let (onion_message, context) = + self.hrn_resolver.resolve_name(payment_id, name, &*self.entropy_source)?; + let reply_paths = self.create_blinded_paths(MessageContext::DNSResolver(context))?; + let expiration = StaleExpiration::TimerTicks(1); + self.pending_outbound_payments.add_new_awaiting_offer(payment_id, expiration, retry_strategy, max_total_routing_fee_msat, amount_msats)?; + let message_params = dns_resolvers + .iter() + .flat_map(|destination| reply_paths.iter().map(move |path| (path, destination))) + .take(OFFERS_MESSAGE_REQUEST_LIMIT); + for (reply_path, destination) in message_params { + self.pending_dns_onion_messages.lock().unwrap().push(( + DNSResolverMessage::DNSSECQuery(onion_message.clone()), + MessageSendInstructions::WithSpecifiedReplyPath { + destination: destination.clone(), + reply_path: reply_path.clone(), + }, + )); + } + Ok(()) + } + /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing /// to pay us. /// @@ -10387,6 +10484,10 @@ where } } max_time!(self.highest_seen_timestamp); + #[cfg(feature = "dnssec")] { + let timestamp = self.highest_seen_timestamp.load(Ordering::Relaxed) as u32; + self.hrn_resolver.new_best_block(height, timestamp); + } } fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { @@ -11637,6 +11738,62 @@ where } } +#[cfg(feature = "dnssec")] +impl +DNSResolverMessageHandler for ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + fn handle_dnssec_query( + &self, _message: DNSSECQuery, _responder: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + None + } + + fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { + let offer_opt = self.hrn_resolver.handle_dnssec_proof_for_offer(message, context); + if let Some((completed_requests, offer)) = offer_opt { + for (name, payment_id) in completed_requests { + if let Ok(amt_msats) = self.pending_outbound_payments.amt_msats_for_payment_awaiting_offer(payment_id) { + let offer_pay_res = + self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), + |invoice_request, nonce| { + let retryable_invoice_request = RetryableInvoiceRequest { + invoice_request: invoice_request.clone(), + nonce, + }; + self.pending_outbound_payments + .received_offer(payment_id, Some(retryable_invoice_request)) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) + }); + if offer_pay_res.is_err() { + // The offer we tried to pay is the canonical current offer for the name we + // wanted to pay. If we can't pay it, there's no way to recover so fail the + // payment. + // Note that the PaymentFailureReason should be ignored for an + // AwaitingInvoice payment. + self.pending_outbound_payments.abandon_payment( + payment_id, PaymentFailureReason::RouteNotFound, &self.pending_events, + ); + } + } + } + } + } + + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap()) + } +} + impl NodeIdLookUp for ChannelManager where @@ -13321,6 +13478,11 @@ where logger: args.logger, default_configuration: args.default_config, + + #[cfg(feature = "dnssec")] + hrn_resolver: OMNameResolver::new(highest_seen_timestamp, best_block_height), + #[cfg(feature = "dnssec")] + pending_dns_onion_messages: Mutex::new(Vec::new()), }; for (_, monitor) in args.channel_monitors.iter() { diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 8cdeadeaa..a33fca9b1 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1639,6 +1639,62 @@ impl OutboundPayments { (payment, onion_session_privs) } + #[cfg(feature = "dnssec")] + pub(super) fn add_new_awaiting_offer( + &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, + max_total_routing_fee_msat: Option, amount_msats: u64, + ) -> Result<(), ()> { + let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); + match pending_outbounds.entry(payment_id) { + hash_map::Entry::Occupied(_) => Err(()), + hash_map::Entry::Vacant(entry) => { + entry.insert(PendingOutboundPayment::AwaitingOffer { + expiration, + retry_strategy, + max_total_routing_fee_msat, + amount_msats, + }); + + Ok(()) + }, + } + } + + #[cfg(feature = "dnssec")] + pub(super) fn amt_msats_for_payment_awaiting_offer(&self, payment_id: PaymentId) -> Result { + match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { + hash_map::Entry::Occupied(entry) => match entry.get() { + PendingOutboundPayment::AwaitingOffer { amount_msats, .. } => Ok(*amount_msats), + _ => Err(()), + }, + _ => Err(()), + } + } + + #[cfg(feature = "dnssec")] + pub(super) fn received_offer( + &self, payment_id: PaymentId, retryable_invoice_request: Option, + ) -> Result<(), ()> { + match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { + hash_map::Entry::Occupied(entry) => match entry.get() { + PendingOutboundPayment::AwaitingOffer { + expiration, retry_strategy, max_total_routing_fee_msat, .. + } => { + let mut new_val = PendingOutboundPayment::AwaitingInvoice { + expiration: *expiration, + retry_strategy: *retry_strategy, + max_total_routing_fee_msat: *max_total_routing_fee_msat, + retryable_invoice_request, + }; + core::mem::swap(&mut new_val, entry.into_mut()); + Ok(()) + }, + _ => Err(()), + }, + hash_map::Entry::Vacant(_) => Err(()), + } + } + pub(super) fn add_new_awaiting_invoice( &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, max_total_routing_fee_msat: Option, retryable_invoice_request: Option -- 2.39.5