From b6f44798fc5abec61a7cdaef7aae6b203d872e6a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 29 Aug 2024 14:37:45 -0400 Subject: [PATCH] Support initiating an async payment to a static invoice. 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 | 94 ++++++++++++++++++++++++++-- lightning/src/ln/outbound_payment.rs | 70 +++++++++++++++++++++ 2 files changed, 158 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8120fb401..82f47de27 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -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) => { diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 5849d5685..db8ca6e90 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -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( + &self, invoice: &StaticInvoice, payment_id: PaymentId, entropy_source: ES, + pending_events: &Mutex)>> + ) -> 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::::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( &self, router: &R, first_hops: FH, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, -- 2.39.5