]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Support initiating an async payment to a static invoice.
authorValentine Wallace <vwallace@protonmail.com>
Thu, 29 Aug 2024 18:37:45 +0000 (14:37 -0400)
committerValentine Wallace <vwallace@protonmail.com>
Fri, 13 Sep 2024 14:40:05 +0000 (10:40 -0400)
Supported when the sender is an always-online node. Here we send the initial
held_htlc_available onion message upon receipt of a static invoice, next we'll
need to actually send HTLCs upon getting a response to said OM.

lightning/src/ln/channelmanager.rs
lightning/src/ln/outbound_payment.rs

index 8120fb4017ca5ea662b733fd55aeb9f65953fbaa..82f47de270080573650bcda0bd12096e4fc06d6e 100644 (file)
@@ -71,6 +71,8 @@ use crate::offers::offer::{Offer, OfferBuilder};
 use crate::offers::parse::Bolt12SemanticError;
 use crate::offers::refund::{Refund, RefundBuilder};
 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::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions};
 use crate::onion_message::offers::{OffersMessage, OffersMessageHandler};
@@ -4318,6 +4320,61 @@ where
                        )
        }
 
+       #[cfg(async_payments)]
+       fn initiate_async_payment(
+               &self, invoice: &StaticInvoice, payment_id: PaymentId
+       ) -> Result<(), Bolt12PaymentError> {
+               let mut res = Ok(());
+               PersistenceNotifierGuard::optionally_notify(self, || {
+                       let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received(
+                               invoice, payment_id, &*self.entropy_source, &self.pending_events
+                       );
+                       let payment_release_secret = match outbound_pmts_res {
+                               Ok(secret) => secret,
+                               Err(Bolt12PaymentError::UnexpectedInvoice) | Err(Bolt12PaymentError::DuplicateInvoice) => {
+                                       res = outbound_pmts_res.map(|_| ());
+                                       return NotifyOption::SkipPersistNoEvents
+                               },
+                               Err(e) => {
+                                       res = Err(e);
+                                       return NotifyOption::DoPersist
+                               }
+                       };
+
+                       let reply_paths = match self.create_blinded_paths(
+                               MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundPayment { payment_id })
+                       ) {
+                               Ok(paths) => paths,
+                               Err(()) => {
+                                       self.abandon_payment_with_reason(payment_id, PaymentFailureReason::RouteNotFound);
+                                       res = Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::RouteNotFound));
+                                       return NotifyOption::DoPersist
+                               }
+                       };
+
+                       let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap();
+                       const HTLC_AVAILABLE_LIMIT: usize = 10;
+                       reply_paths
+                               .iter()
+                               .flat_map(|reply_path| invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path)))
+                               .take(HTLC_AVAILABLE_LIMIT)
+                               .for_each(|(invoice_path, reply_path)| {
+                                       let instructions = MessageSendInstructions::WithSpecifiedReplyPath {
+                                               destination: Destination::BlindedPath(invoice_path.clone()),
+                                               reply_path: reply_path.clone(),
+                                       };
+                                       let message = AsyncPaymentsMessage::HeldHtlcAvailable(
+                                               HeldHtlcAvailable { payment_release_secret }
+                                       );
+                                       pending_async_payments_messages.push((message, instructions));
+                               });
+
+                       NotifyOption::DoPersist
+               });
+
+               res
+       }
+
        /// Signals that no further attempts for the given payment should occur. Useful if you have a
        /// pending outbound payment with retries remaining, but wish to stop retrying the payment before
        /// retries are exhausted.
@@ -11040,14 +11097,39 @@ where
                                }
                        },
                        #[cfg(async_payments)]
-                       OffersMessage::StaticInvoice(_invoice) => {
+                       OffersMessage::StaticInvoice(invoice) => {
+                               let payment_id = match context {
+                                       Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => {
+                                               if payment_id.verify(hmac, nonce, expanded_key).is_err() {
+                                                       return None
+                                               }
+                                               payment_id
+                                       },
+                                       _ => return None
+                               };
+                               // TODO: DRY this with the above regular invoice error handling
+                               let error = match self.initiate_async_payment(&invoice, payment_id) {
+                                       Err(Bolt12PaymentError::UnknownRequiredFeatures) => {
+                                               log_trace!(
+                                                       self.logger, "Invoice requires unknown features: {:?}",
+                                                       invoice.invoice_features()
+                                               );
+                                               InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures)
+                                       },
+                                       Err(Bolt12PaymentError::SendingFailed(e)) => {
+                                               log_trace!(self.logger, "Failed paying invoice: {:?}", e);
+                                               InvoiceError::from_string(format!("{:?}", e))
+                                       },
+                                       Err(Bolt12PaymentError::UnexpectedInvoice)
+                                               | Err(Bolt12PaymentError::DuplicateInvoice)
+                                               | Ok(()) => return None,
+                               };
                                match responder {
-                                       Some(responder) => {
-                                               return Some((OffersMessage::InvoiceError(
-                                                       InvoiceError::from_string("Static invoices not yet supported".to_string())
-                                               ), responder.respond()));
+                                       Some(responder) => Some((OffersMessage::InvoiceError(error), responder.respond())),
+                                       None => {
+                                               log_trace!(self.logger, "No reply path to send error: {:?}", error);
+                                               None
                                        },
-                                       None => return None,
                                }
                        },
                        OffersMessage::InvoiceError(invoice_error) => {
index 5849d56854caefc040fc0b4d902c0356aafb9894..db8ca6e909a24f19060f3350cf15234033272b78 100644 (file)
@@ -32,6 +32,12 @@ use crate::util::logger::Logger;
 use crate::util::time::Instant;
 use crate::util::ser::ReadableArgs;
 
+#[cfg(async_payments)]
+use {
+       crate::offers::invoice::{DerivedSigningPubkey, InvoiceBuilder},
+       crate::offers::static_invoice::StaticInvoice,
+};
+
 use core::fmt::{self, Display, Formatter};
 use core::ops::Deref;
 use core::sync::atomic::{AtomicBool, Ordering};
@@ -928,6 +934,70 @@ impl OutboundPayments {
                Ok(())
        }
 
+       #[cfg(async_payments)]
+       pub(super) fn static_invoice_received<ES: Deref>(
+               &self, invoice: &StaticInvoice, payment_id: PaymentId, entropy_source: ES,
+               pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>
+       ) -> Result<[u8; 32], Bolt12PaymentError> where ES::Target: EntropySource {
+               macro_rules! abandon_with_entry {
+                       ($payment: expr, $reason: expr) => {
+                               $payment.get_mut().mark_abandoned($reason);
+                               if let PendingOutboundPayment::Abandoned { reason, .. } = $payment.get() {
+                                       if $payment.get().remaining_parts() == 0 {
+                                               pending_events.lock().unwrap().push_back((events::Event::PaymentFailed {
+                                                       payment_id,
+                                                       payment_hash: None,
+                                                       reason: *reason,
+                                               }, None));
+                                               $payment.remove();
+                                       }
+                               }
+                       }
+               }
+
+               match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
+                       hash_map::Entry::Occupied(mut entry) => match entry.get() {
+                               PendingOutboundPayment::AwaitingInvoice {
+                                       retry_strategy, retryable_invoice_request, max_total_routing_fee_msat, ..
+                               } => {
+                                       let invreq = &retryable_invoice_request
+                                               .as_ref()
+                                               .ok_or(Bolt12PaymentError::UnexpectedInvoice)?
+                                               .invoice_request;
+                                       if !invoice.from_same_offer(invreq) {
+                                               return Err(Bolt12PaymentError::UnexpectedInvoice)
+                                       }
+                                       let amount_msat = match InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(invreq) {
+                                               Ok(amt) => amt,
+                                               Err(_) => {
+                                                       // We check this during invoice request parsing, when constructing the invreq's
+                                                       // contents from its TLV stream.
+                                                       debug_assert!(false, "LDK requires an msat amount in either the invreq or the invreq's underlying offer");
+                                                       abandon_with_entry!(entry, PaymentFailureReason::UnexpectedError);
+                                                       return Err(Bolt12PaymentError::UnknownRequiredFeatures)
+                                               }
+                                       };
+                                       let keysend_preimage = PaymentPreimage(entropy_source.get_secure_random_bytes());
+                                       let payment_hash = PaymentHash(Sha256::hash(&keysend_preimage.0).to_byte_array());
+                                       let payment_release_secret = entropy_source.get_secure_random_bytes();
+                                       let pay_params = PaymentParameters::from_static_invoice(invoice);
+                                       let mut route_params = RouteParameters::from_payment_params_and_value(pay_params, amount_msat);
+                                       route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat;
+                                       *entry.into_mut() = PendingOutboundPayment::StaticInvoiceReceived {
+                                               payment_hash,
+                                               keysend_preimage,
+                                               retry_strategy: *retry_strategy,
+                                               payment_release_secret,
+                                               route_params,
+                                       };
+                                       return Ok(payment_release_secret)
+                               },
+                               _ => return Err(Bolt12PaymentError::DuplicateInvoice),
+                       },
+                       hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice),
+               };
+       }
+
        pub(super) fn check_retry_payments<R: Deref, ES: Deref, NS: Deref, SP, IH, FH, L: Deref>(
                &self, router: &R, first_hops: FH, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS,
                best_block_height: u32,