From 732ee149bb4fb7b0e93a758e4e1c9b930ba9060b Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 12 Jul 2024 16:19:53 -0400 Subject: [PATCH 01/24] Tweak debug_assert message for parsed onion messages. More specific. --- lightning/src/onion_message/messenger.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 7445c3c4c0b..f44e3cb018d 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1588,7 +1588,7 @@ where None => None, Some(MessageContext::Offers(context)) => Some(context), Some(MessageContext::Custom(_)) => { - debug_assert!(false, "Shouldn't have triggered this case."); + debug_assert!(false, "Checked in peel_onion_message"); return } }; @@ -1615,7 +1615,7 @@ where None => None, Some(MessageContext::Custom(data)) => Some(data), Some(MessageContext::Offers(_)) => { - debug_assert!(false, "Shouldn't have triggered this case."); + debug_assert!(false, "Checked in peel_onion_message"); return } }; From a3216acb7d5c36f7f7993d76612ff383992e4c14 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 10 Jul 2024 13:54:05 -0400 Subject: [PATCH 02/24] Add MessageContext for async payments. This context will be used in reply paths for outbound held_htlc_available messages, so we can authenticate the corresponding release_held_htlc messages. --- lightning/src/blinded_path/message.rs | 31 ++++++++++++++++++++++++ lightning/src/onion_message/messenger.rs | 8 ++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 256483fec01..e6aa4e340ce 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -280,6 +280,10 @@ pub enum MessageContext { /// /// [`OffersMessage`]: crate::onion_message::offers::OffersMessage Offers(OffersContext), + /// Context specific to an [`AsyncPaymentsMessage`]. + /// + /// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage + AsyncPayments(AsyncPaymentsContext), /// Context specific to a [`CustomOnionMessageHandler::CustomMessage`]. /// /// [`CustomOnionMessageHandler::CustomMessage`]: crate::onion_message::messenger::CustomOnionMessageHandler::CustomMessage @@ -363,9 +367,30 @@ pub enum OffersContext { }, } +/// Contains data specific to an [`AsyncPaymentsMessage`]. +/// +/// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage +#[derive(Clone, Debug)] +pub enum AsyncPaymentsContext { + /// Context contained within the reply [`BlindedMessagePath`] we put in outbound + /// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`] + /// messages. + /// + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc + OutboundPayment { + /// ID used when payment to the originating [`Offer`] was initiated. Useful for us to identify + /// which of our pending outbound payments should be released to its often-offline payee. + /// + /// [`Offer`]: crate::offers::offer::Offer + payment_id: PaymentId + }, +} + impl_writeable_tlv_based_enum!(MessageContext, {0, Offers} => (), {1, Custom} => (), + {2, AsyncPayments} => (), ); impl_writeable_tlv_based_enum!(OffersContext, @@ -384,6 +409,12 @@ impl_writeable_tlv_based_enum!(OffersContext, }, ); +impl_writeable_tlv_based_enum!(AsyncPaymentsContext, + (0, OutboundPayment) => { + (0, payment_id, required), + }, +); + /// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`. pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[MessageForwardNode], diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index f44e3cb018d..18522c0f308 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -979,6 +979,10 @@ where (ParsedOnionMessageContents::Offers(_), Some(MessageContext::Offers(_))) => { Ok(PeeledOnion::Receive(message, context, reply_path)) } + #[cfg(async_payments)] + (ParsedOnionMessageContents::AsyncPayments(_), Some(MessageContext::AsyncPayments(_))) => { + Ok(PeeledOnion::Receive(message, context, reply_path)) + } (ParsedOnionMessageContents::Custom(_), Some(MessageContext::Custom(_))) => { Ok(PeeledOnion::Receive(message, context, reply_path)) } @@ -1587,7 +1591,7 @@ where let context = match context { None => None, Some(MessageContext::Offers(context)) => Some(context), - Some(MessageContext::Custom(_)) => { + _ => { debug_assert!(false, "Checked in peel_onion_message"); return } @@ -1614,7 +1618,7 @@ where let context = match context { None => None, Some(MessageContext::Custom(data)) => Some(data), - Some(MessageContext::Offers(_)) => { + _ => { debug_assert!(false, "Checked in peel_onion_message"); return } From e162278bc8ad3ac8b7132016fa36f96d0afbb896 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 10 Jul 2024 13:58:53 -0400 Subject: [PATCH 03/24] Pass context into held_htlc_available message handling. Useful for using the payment_id within to look up the corresponding outbound async payment so we know we can safely release the HTLCs to the now-onlinen recipient. --- fuzz/src/onion_message.rs | 6 ++++-- lightning/src/ln/channelmanager.rs | 4 ++-- lightning/src/ln/peer_handler.rs | 4 ++-- lightning/src/onion_message/async_payments.rs | 3 ++- lightning/src/onion_message/functional_tests.rs | 4 ++-- lightning/src/onion_message/messenger.rs | 10 +++++++++- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 19666847a23..26bb0bc0ee4 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -5,7 +5,9 @@ use bitcoin::secp256k1::ecdsa::RecoverableSignature; use bitcoin::secp256k1::schnorr; use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1, SecretKey}; -use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, OffersContext}; +use lightning::blinded_path::message::{ + AsyncPaymentsContext, BlindedMessagePath, MessageContext, OffersContext, +}; use lightning::blinded_path::EmptyNodeIdLookUp; use lightning::ln::features::InitFeatures; use lightning::ln::msgs::{self, DecodeError, OnionMessageHandler}; @@ -129,7 +131,7 @@ impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { responder.respond(), )) } - fn release_held_htlc(&self, _message: ReleaseHeldHtlc) {} + fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {} } #[derive(Debug)] diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 5f8dc1e5541..f21a0a20be5 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -33,7 +33,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{secp256k1, Sequence}; use crate::events::FundingInfo; -use crate::blinded_path::message::{MessageContext, OffersContext}; +use crate::blinded_path::message::{AsyncPaymentsContext, MessageContext, OffersContext}; use crate::blinded_path::NodeIdLookUp; use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode}; use crate::blinded_path::payment::{BlindedPaymentPath, Bolt12OfferContext, Bolt12RefundContext, PaymentConstraints, PaymentContext, ReceiveTlvs}; @@ -11094,7 +11094,7 @@ where None } - fn release_held_htlc(&self, _message: ReleaseHeldHtlc) {} + fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {} fn release_pending_messages(&self) -> Vec<(AsyncPaymentsMessage, MessageSendInstructions)> { Vec::new() diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index 3c0d724ad94..626eabfebd6 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -18,7 +18,7 @@ use bitcoin::constants::ChainHash; use bitcoin::secp256k1::{self, Secp256k1, SecretKey, PublicKey}; -use crate::blinded_path::message::OffersContext; +use crate::blinded_path::message::{AsyncPaymentsContext, OffersContext}; use crate::sign::{NodeSigner, Recipient}; use crate::events::{MessageSendEvent, MessageSendEventsProvider}; use crate::ln::types::ChannelId; @@ -152,7 +152,7 @@ impl AsyncPaymentsMessageHandler for IgnoringMessageHandler { ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { None } - fn release_held_htlc(&self, _message: ReleaseHeldHtlc) {} + fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {} } impl CustomOnionMessageHandler for IgnoringMessageHandler { type CustomMessage = Infallible; diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 89756d9f1f3..e2a7f7bf74e 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -9,6 +9,7 @@ //! Message handling for async payments. +use crate::blinded_path::message::AsyncPaymentsContext; use crate::io; use crate::ln::msgs::DecodeError; use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; @@ -32,7 +33,7 @@ pub trait AsyncPaymentsMessageHandler { /// Handle a [`ReleaseHeldHtlc`] message. If authentication of the message succeeds, an HTLC /// should be released to the corresponding payee. - fn release_held_htlc(&self, message: ReleaseHeldHtlc); + fn release_held_htlc(&self, message: ReleaseHeldHtlc, context: AsyncPaymentsContext); /// Release any [`AsyncPaymentsMessage`]s that need to be sent. /// diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 7966e2a40ab..ef733b0893c 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -10,7 +10,7 @@ //! Onion message testing and test utilities live here. use crate::blinded_path::EmptyNodeIdLookUp; -use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode, MessageContext, OffersContext}; +use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, MessageContext, OffersContext}; use crate::events::{Event, EventsProvider}; use crate::ln::features::{ChannelFeatures, InitFeatures}; use crate::ln::msgs::{self, DecodeError, OnionMessageHandler}; @@ -87,7 +87,7 @@ impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { None } - fn release_held_htlc(&self, _message: ReleaseHeldHtlc) {} + fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {} } #[derive(Clone, Debug, PartialEq)] diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 18522c0f308..6883b02c145 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1612,7 +1612,15 @@ where }, #[cfg(async_payments)] ParsedOnionMessageContents::AsyncPayments(AsyncPaymentsMessage::ReleaseHeldHtlc(msg)) => { - self.async_payments_handler.release_held_htlc(msg); + let context = match context { + Some(MessageContext::AsyncPayments(context)) => context, + Some(_) => { + debug_assert!(false, "Checked in peel_onion_message"); + return + }, + None => return, + }; + self.async_payments_handler.release_held_htlc(msg, context); }, ParsedOnionMessageContents::Custom(msg) => { let context = match context { From 3d5d64a2c82795ed6353890a7a8a5ae530984eb3 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 13 Jun 2024 11:25:20 -0400 Subject: [PATCH 04/24] Store AsyncPaymentsMessages for later sending Upcoming commits will support sending and receiving held_htlc_available and release_held_htlc messages. These messages need to be enqueued so that they can be released in ChannelManager's implementation of AsyncPaymentsMessageHandler to OnionMessenger for sending. --- lightning/src/ln/channelmanager.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f21a0a20be5..730332d2ec0 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2060,6 +2060,8 @@ where // // `pending_offers_messages` // +// `pending_async_payments_messages` +// // `total_consistency_lock` // | // |__`forward_htlcs` @@ -2315,6 +2317,7 @@ where pending_offers_messages: Mutex>, #[cfg(any(test, feature = "_test_utils"))] pub(crate) pending_offers_messages: Mutex>, + pending_async_payments_messages: Mutex>, /// Tracks the message events that are to be broadcasted when we are connected to some peer. pending_broadcast_messages: Mutex>, @@ -3135,6 +3138,7 @@ where funding_batch_states: Mutex::new(BTreeMap::new()), pending_offers_messages: Mutex::new(Vec::new()), + pending_async_payments_messages: Mutex::new(Vec::new()), pending_broadcast_messages: Mutex::new(Vec::new()), last_days_feerates: Mutex::new(VecDeque::new()), @@ -12827,6 +12831,7 @@ where funding_batch_states: Mutex::new(BTreeMap::new()), pending_offers_messages: Mutex::new(Vec::new()), + pending_async_payments_messages: Mutex::new(Vec::new()), pending_broadcast_messages: Mutex::new(Vec::new()), From cff6e344f558fe97570b50cbaa359fd6f504c586 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 13 Jun 2024 16:16:47 -0400 Subject: [PATCH 05/24] Support checking that a static invoice matches an outbound invreq. Useful for ensuring that an inbound static invoice matches one of our outbound invreqs, otherwise it is an unexpected invoice and should be ignored and not paid. --- lightning/src/offers/invoice_request.rs | 5 +++++ lightning/src/offers/merkle.rs | 1 + lightning/src/offers/static_invoice.rs | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 32d05249cfa..dc2fd4bf1df 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -840,6 +840,11 @@ impl InvoiceRequest { invoice_request_accessors!(self, self.contents); invoice_request_respond_with_explicit_signing_pubkey_methods!(self, self, InvoiceBuilder); invoice_request_verify_method!(self, Self); + + #[cfg(async_payments)] + pub(super) fn bytes(&self) -> &Vec { + &self.bytes + } } #[cfg(c_bindings)] diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 90bfc859e50..e2fed2e800b 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -249,6 +249,7 @@ impl<'a> TlvStream<'a> { } /// A slice into a [`TlvStream`] for a record. +#[derive(Eq, PartialEq)] pub(super) struct TlvRecord<'a> { pub(super) r#type: u64, type_bytes: &'a [u8], diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 33706f928d8..4910c57c5af 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -20,12 +20,13 @@ use crate::offers::invoice::{ InvoiceTlvStream, InvoiceTlvStreamRef, }; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; +use crate::offers::invoice_request::InvoiceRequest; use crate::offers::merkle::{ - self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, + self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, }; use crate::offers::nonce::Nonce; use crate::offers::offer::{ - Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, + Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, OFFER_TYPES, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::util::ser::{CursorReadable, Iterable, WithoutLength, Writeable, Writer}; @@ -312,6 +313,16 @@ impl StaticInvoice { pub fn signature(&self) -> Signature { self.signature } + + pub(crate) fn from_same_offer(&self, invreq: &InvoiceRequest) -> bool { + let invoice_offer_tlv_stream = TlvStream::new(&self.bytes) + .range(OFFER_TYPES) + .map(|tlv_record| tlv_record.record_bytes); + let invreq_offer_tlv_stream = TlvStream::new(invreq.bytes()) + .range(OFFER_TYPES) + .map(|tlv_record| tlv_record.record_bytes); + invoice_offer_tlv_stream.eq(invreq_offer_tlv_stream) + } } impl InvoiceContents { From ad63a706f2d0849f16a1848d02807750dd958b03 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 13 Jun 2024 17:12:03 -0400 Subject: [PATCH 06/24] Support creating PaymentParameters from static invoices. --- lightning/src/routing/router.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index a25609c89bb..5311cda630f 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -20,6 +20,8 @@ use crate::ln::channelmanager::{PaymentId, MIN_FINAL_CLTV_EXPIRY_DELTA, Recipien use crate::ln::features::{BlindedHopFeatures, Bolt11InvoiceFeatures, Bolt12InvoiceFeatures, ChannelFeatures, NodeFeatures}; use crate::ln::msgs::{DecodeError, ErrorAction, LightningError, MAX_VALUE_MSAT}; use crate::ln::onion_utils; +#[cfg(async_payments)] +use crate::offers::static_invoice::StaticInvoice; use crate::offers::invoice::Bolt12Invoice; use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageRouter, OnionMessagePath}; use crate::routing::gossip::{DirectedChannelInfo, EffectiveCapacity, ReadOnlyNetworkGraph, NetworkGraph, NodeId}; @@ -877,6 +879,16 @@ impl PaymentParameters { .with_expiry_time(invoice.created_at().as_secs().saturating_add(invoice.relative_expiry().as_secs())) } + /// Creates parameters for paying to a blinded payee from the provided invoice. Sets + /// [`Payee::Blinded::route_hints`], [`Payee::Blinded::features`], and + /// [`PaymentParameters::expiry_time`]. + #[cfg(async_payments)] + pub fn from_static_invoice(invoice: &StaticInvoice) -> Self { + Self::blinded(invoice.payment_paths().to_vec()) + .with_bolt12_features(invoice.invoice_features().clone()).unwrap() + .with_expiry_time(invoice.created_at().as_secs().saturating_add(invoice.relative_expiry().as_secs())) + } + /// Creates parameters for paying to a blinded payee from the provided blinded route hints. pub fn blinded(blinded_route_hints: Vec) -> Self { Self { From c3ed4a2687a0dee2dea0939982128a271dde587e Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 10 Jul 2024 14:55:34 -0400 Subject: [PATCH 07/24] Store async payment data in PendingOutboundPayment. Adds a pending outbound payment variant for async payments, which indicates that we have received a static invoice to pay and have generated a keysend preimage for the eventual payment. When the recipient comes back online, we'll transition from this new state to Retryable and actually forward the HTLCs. --- lightning/src/ln/channelmanager.rs | 4 ++++ lightning/src/ln/outbound_payment.rs | 32 +++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 730332d2ec0..d88bf2c4cf2 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3374,6 +3374,9 @@ where PendingOutboundPayment::InvoiceReceived { .. } => { Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id }) }, + PendingOutboundPayment::StaticInvoiceReceived { .. } => { + Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id }) + }, PendingOutboundPayment::Retryable { payment_hash, total_msat, .. } => { Some(RecentPaymentDetails::Pending { payment_id: *payment_id, @@ -11722,6 +11725,7 @@ where } PendingOutboundPayment::AwaitingInvoice { .. } => {}, PendingOutboundPayment::InvoiceReceived { .. } => {}, + PendingOutboundPayment::StaticInvoiceReceived { .. } => {}, PendingOutboundPayment::Fulfilled { .. } => {}, PendingOutboundPayment::Abandoned { .. } => {}, } diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index ca0d7c17d99..5849d56854c 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -65,6 +65,13 @@ pub(crate) enum PendingOutboundPayment { // used anywhere. max_total_routing_fee_msat: Option, }, + StaticInvoiceReceived { + payment_hash: PaymentHash, + keysend_preimage: PaymentPreimage, + retry_strategy: Retry, + payment_release_secret: [u8; 32], + route_params: RouteParameters, + }, Retryable { retry_strategy: Option, attempts: PaymentAttempts, @@ -182,6 +189,7 @@ impl PendingOutboundPayment { PendingOutboundPayment::Legacy { .. } => None, PendingOutboundPayment::AwaitingInvoice { .. } => None, PendingOutboundPayment::InvoiceReceived { payment_hash, .. } => Some(*payment_hash), + PendingOutboundPayment::StaticInvoiceReceived { payment_hash, .. } => Some(*payment_hash), PendingOutboundPayment::Retryable { payment_hash, .. } => Some(*payment_hash), PendingOutboundPayment::Fulfilled { payment_hash, .. } => *payment_hash, PendingOutboundPayment::Abandoned { payment_hash, .. } => Some(*payment_hash), @@ -196,7 +204,8 @@ impl PendingOutboundPayment { PendingOutboundPayment::Fulfilled { session_privs, .. } | PendingOutboundPayment::Abandoned { session_privs, .. } => session_privs, PendingOutboundPayment::AwaitingInvoice { .. } | - PendingOutboundPayment::InvoiceReceived { .. } => { debug_assert!(false); return; }, + PendingOutboundPayment::InvoiceReceived { .. } | + PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); return; }, }); let payment_hash = self.payment_hash(); *self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash, timer_ticks_without_htlcs: 0 }; @@ -230,7 +239,8 @@ impl PendingOutboundPayment { session_privs.remove(session_priv) }, PendingOutboundPayment::AwaitingInvoice { .. } | - PendingOutboundPayment::InvoiceReceived { .. } => { debug_assert!(false); false }, + PendingOutboundPayment::InvoiceReceived { .. } | + PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); false }, }; if remove_res { if let PendingOutboundPayment::Retryable { @@ -259,7 +269,8 @@ impl PendingOutboundPayment { session_privs.insert(session_priv) }, PendingOutboundPayment::AwaitingInvoice { .. } | - PendingOutboundPayment::InvoiceReceived { .. } => { debug_assert!(false); false }, + PendingOutboundPayment::InvoiceReceived { .. } | + PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); false }, PendingOutboundPayment::Fulfilled { .. } => false, PendingOutboundPayment::Abandoned { .. } => false, }; @@ -292,6 +303,7 @@ impl PendingOutboundPayment { }, PendingOutboundPayment::AwaitingInvoice { .. } => 0, PendingOutboundPayment::InvoiceReceived { .. } => 0, + PendingOutboundPayment::StaticInvoiceReceived { .. } => 0, } } } @@ -1195,6 +1207,11 @@ impl OutboundPayments { debug_assert!(false); return }, + PendingOutboundPayment::StaticInvoiceReceived { .. } => { + log_error!(logger, "Payment already initiating"); + debug_assert!(false); + return + }, PendingOutboundPayment::Fulfilled { .. } => { log_error!(logger, "Payment already completed"); return @@ -1985,6 +2002,15 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (2, retry_strategy, required), (4, max_total_routing_fee_msat, option), }, + // Added in 0.0.125. Prior versions will drop these outbounds on downgrade, which is safe because + // no HTLCs are in-flight. + (9, StaticInvoiceReceived) => { + (0, payment_hash, required), + (2, keysend_preimage, required), + (4, retry_strategy, required), + (6, payment_release_secret, required), + (8, route_params, required), + }, ); #[cfg(test)] From 7fb16ea20c53613f14378a57e4e552fb4ed78a1f Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 10 Jul 2024 14:58:11 -0400 Subject: [PATCH 08/24] Pass full message context into ChanMan blinded path util. Allows us to repurpose this util to create blinded paths in an async payments message context while maintaining support for the offers context. --- lightning/src/ln/channelmanager.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d88bf2c4cf2..c6cffaf1023 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9131,7 +9131,9 @@ where let invoice_request = builder.build_and_sign()?; let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); - let context = OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }; + let context = MessageContext::Offers( + OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) } + ); let reply_paths = self.create_blinded_paths(context) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -9251,9 +9253,9 @@ where let nonce = Nonce::from_entropy_source(entropy); let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); - let context = OffersContext::InboundPayment { + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash: invoice.payment_hash(), nonce, hmac - }; + }); let reply_paths = self.create_blinded_paths(context) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -9401,7 +9403,7 @@ where if absolute_expiry.unwrap_or(Duration::MAX) <= max_short_lived_absolute_expiry { self.create_compact_blinded_paths(context) } else { - self.create_blinded_paths(context) + self.create_blinded_paths(MessageContext::Offers(context)) } } @@ -9422,7 +9424,7 @@ where /// [`MessageRouter::create_blinded_paths`]. /// /// Errors if the `MessageRouter` errors. - fn create_blinded_paths(&self, context: OffersContext) -> Result, ()> { + fn create_blinded_paths(&self, context: MessageContext) -> Result, ()> { let recipient = self.get_our_node_id(); let secp_ctx = &self.secp_ctx; @@ -9435,7 +9437,7 @@ where .collect::>(); self.router - .create_blinded_paths(recipient, MessageContext::Offers(context), peers, secp_ctx) + .create_blinded_paths(recipient, context, peers, secp_ctx) .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) } @@ -10839,11 +10841,11 @@ where { let RetryableInvoiceRequest { invoice_request, nonce } = retryable_invoice_request; let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); - let context = OffersContext::OutboundPayment { + let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) - }; + }); match self.create_blinded_paths(context) { Ok(reply_paths) => match self.enqueue_invoice_request(invoice_request, reply_paths) { Ok(_) => {} From c976e4cd1481c069ffc4b5497e3b54c6511fd1df Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 29 Aug 2024 14:25:40 -0400 Subject: [PATCH 09/24] Release pending async payments to PeerManager. --- lightning/src/ln/channelmanager.rs | 2 +- lightning/src/onion_message/messenger.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c6cffaf1023..8120fb4017c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11106,7 +11106,7 @@ where fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {} fn release_pending_messages(&self) -> Vec<(AsyncPaymentsMessage, MessageSendInstructions)> { - Vec::new() + core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap()) } } diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 6883b02c145..ab7ccbdab38 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1765,6 +1765,14 @@ where ); } + #[cfg(async_payments)] { + for (message, instructions) in self.async_payments_handler.release_pending_messages() { + let _ = self.send_onion_message_internal( + message, instructions, format_args!("when sending AsyncPaymentsMessage") + ); + } + } + // Enqueue any initiating `CustomMessage`s to send. for (message, instructions) in self.custom_handler.release_pending_custom_messages() { let _ = self.send_onion_message_internal( From b6f44798fc5abec61a7cdaef7aae6b203d872e6a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 29 Aug 2024 14:37:45 -0400 Subject: [PATCH 10/24] 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 8120fb4017c..82f47de2700 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 5849d56854c..db8ca6e909a 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, From 28269a7879f0777d6fadaa262e43bc72d1a73cca Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 29 Aug 2024 14:53:12 -0400 Subject: [PATCH 11/24] DRY handling when initiating payment to BOLT 12 invoice. --- lightning/src/ln/channelmanager.rs | 83 ++++++++++++------------------ 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 82f47de2700..0fe7bef69cf 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10943,6 +10943,35 @@ where let secp_ctx = &self.secp_ctx; let expanded_key = &self.inbound_payment_key; + macro_rules! handle_pay_invoice_res { + ($res: expr, $invoice: expr, $logger: expr) => {{ + let error = match $res { + Err(Bolt12PaymentError::UnknownRequiredFeatures) => { + log_trace!( + $logger, "Invoice requires unknown features: {:?}", + $invoice.invoice_features() + ); + InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures) + }, + Err(Bolt12PaymentError::SendingFailed(e)) => { + log_trace!($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(error), responder.respond())), + None => { + log_trace!($logger, "No reply path to send error: {:?}", error); + return None + }, + } + }} + } + match message { OffersMessage::InvoiceRequest(invoice_request) => { let responder = match responder { @@ -11069,32 +11098,8 @@ where return None; } - let error = match self.send_payment_for_verified_bolt12_invoice( - &invoice, payment_id, - ) { - Err(Bolt12PaymentError::UnknownRequiredFeatures) => { - log_trace!( - logger, "Invoice requires unknown features: {:?}", - invoice.invoice_features() - ); - InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures) - }, - Err(Bolt12PaymentError::SendingFailed(e)) => { - log_trace!(logger, "Failed paying invoice: {:?}", e); - InvoiceError::from_string(format!("{:?}", e)) - }, - Err(Bolt12PaymentError::UnexpectedInvoice) - | Err(Bolt12PaymentError::DuplicateInvoice) - | Ok(()) => return None, - }; - - match responder { - Some(responder) => Some((OffersMessage::InvoiceError(error), responder.respond())), - None => { - log_trace!(logger, "No reply path to send error: {:?}", error); - None - }, - } + let res = self.send_payment_for_verified_bolt12_invoice(&invoice, payment_id); + handle_pay_invoice_res!(res, invoice, logger); }, #[cfg(async_payments)] OffersMessage::StaticInvoice(invoice) => { @@ -11107,30 +11112,8 @@ where }, _ => 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) => Some((OffersMessage::InvoiceError(error), responder.respond())), - None => { - log_trace!(self.logger, "No reply path to send error: {:?}", error); - None - }, - } + let res = self.initiate_async_payment(&invoice, payment_id); + handle_pay_invoice_res!(res, invoice, self.logger); }, OffersMessage::InvoiceError(invoice_error) => { let payment_hash = match context { From e4d7681cba45da748c2863100ac036f84e96f343 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 29 Aug 2024 15:00:08 -0400 Subject: [PATCH 12/24] Error on static invoice with unknown required features. --- lightning/src/ln/channelmanager.rs | 3 ++- lightning/src/ln/outbound_payment.rs | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 0fe7bef69cf..dbd44ec0db7 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4326,8 +4326,9 @@ where ) -> Result<(), Bolt12PaymentError> { let mut res = Ok(()); PersistenceNotifierGuard::optionally_notify(self, || { + let features = self.bolt12_invoice_features(); let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received( - invoice, payment_id, &*self.entropy_source, &self.pending_events + invoice, payment_id, features, &*self.entropy_source, &self.pending_events ); let payment_release_secret = match outbound_pmts_res { Ok(secret) => secret, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index db8ca6e909a..f6ba4d9f859 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -936,7 +936,8 @@ impl OutboundPayments { #[cfg(async_payments)] pub(super) fn static_invoice_received( - &self, invoice: &StaticInvoice, payment_id: PaymentId, entropy_source: ES, + &self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures, + entropy_source: ES, pending_events: &Mutex)>> ) -> Result<[u8; 32], Bolt12PaymentError> where ES::Target: EntropySource { macro_rules! abandon_with_entry { @@ -967,6 +968,10 @@ impl OutboundPayments { if !invoice.from_same_offer(invreq) { return Err(Bolt12PaymentError::UnexpectedInvoice) } + if invoice.invoice_features().requires_unknown_bits_from(&features) { + abandon_with_entry!(entry, PaymentFailureReason::UnknownRequiredFeatures); + return Err(Bolt12PaymentError::UnknownRequiredFeatures) + } let amount_msat = match InvoiceBuilder::::amount_msats(invreq) { Ok(amt) => amt, Err(_) => { From 8569830eb09338bd4deed80f2617ceaeadc6ec4e Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 29 Aug 2024 15:12:41 -0400 Subject: [PATCH 13/24] Set max path len on receipt of static invoice. Because we may receive a static invoice to pay days before the recipient actually comes back online to receive the payment, it's good to do as many checks as we can up-front. Here we ensure that the blinded paths provided in the invoice won't cause us to exceed the maximum onion packet size. --- lightning/src/ln/channelmanager.rs | 4 +++- lightning/src/ln/outbound_payment.rs | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index dbd44ec0db7..f5254285516 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4326,9 +4326,11 @@ where ) -> Result<(), Bolt12PaymentError> { let mut res = Ok(()); PersistenceNotifierGuard::optionally_notify(self, || { + let best_block_height = self.best_block.read().unwrap().height; let features = self.bolt12_invoice_features(); let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received( - invoice, payment_id, features, &*self.entropy_source, &self.pending_events + invoice, payment_id, features, best_block_height, &*self.entropy_source, + &self.pending_events ); let payment_release_secret = match outbound_pmts_res { Ok(secret) => secret, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index f6ba4d9f859..e616c445faf 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -937,7 +937,7 @@ impl OutboundPayments { #[cfg(async_payments)] pub(super) fn static_invoice_received( &self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures, - entropy_source: ES, + best_block_height: u32, entropy_source: ES, pending_events: &Mutex)>> ) -> Result<[u8; 32], Bolt12PaymentError> where ES::Target: EntropySource { macro_rules! abandon_with_entry { @@ -988,6 +988,15 @@ impl OutboundPayments { 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; + + if let Err(()) = onion_utils::set_max_path_length( + &mut route_params, &RecipientOnionFields::spontaneous_empty(), Some(keysend_preimage), + best_block_height + ) { + abandon_with_entry!(entry, PaymentFailureReason::RouteNotFound); + return Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::OnionPacketSizeExceeded)) + } + *entry.into_mut() = PendingOutboundPayment::StaticInvoiceReceived { payment_hash, keysend_preimage, From 69356e7686f58625822b36f7e1fe07cac68201a1 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 4 Sep 2024 15:00:38 -0400 Subject: [PATCH 14/24] Split off send_payment_for_bolt12_invoice_internal util. This new util will be able to send to both static and non-static BOLT 12 invoices. --- lightning/src/ln/outbound_payment.rs | 49 ++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index e616c445faf..bc1123ee2e4 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -829,7 +829,7 @@ impl OutboundPayments { PendingOutboundPayment::AwaitingInvoice { retry_strategy: retry, max_total_routing_fee_msat: max_total_fee, .. } => { - retry_strategy = Some(*retry); + retry_strategy = *retry; max_total_routing_fee_msat = *max_total_fee; *entry.into_mut() = PendingOutboundPayment::InvoiceReceived { payment_hash, @@ -849,11 +849,41 @@ impl OutboundPayments { return Err(Bolt12PaymentError::UnknownRequiredFeatures); } - let mut payment_params = PaymentParameters::from_bolt12_invoice(&invoice); + let mut route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_bolt12_invoice(&invoice), invoice.amount_msats() + ); + if let Some(max_fee_msat) = max_total_routing_fee_msat { + route_params.max_total_routing_fee_msat = Some(max_fee_msat); + } + self.send_payment_for_bolt12_invoice_internal( + payment_id, payment_hash, route_params, retry_strategy, router, first_hops, inflight_htlcs, + entropy_source, node_signer, node_id_lookup, secp_ctx, best_block_height, logger, + pending_events, send_payment_along_path + ) + } + fn send_payment_for_bolt12_invoice_internal< + R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP, L: Deref + >( + &self, payment_id: PaymentId, payment_hash: PaymentHash, mut route_params: RouteParameters, + retry_strategy: Retry, router: &R, first_hops: Vec, inflight_htlcs: IH, + entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, + secp_ctx: &Secp256k1, best_block_height: u32, logger: &L, + pending_events: &Mutex)>>, + send_payment_along_path: SP, + ) -> Result<(), Bolt12PaymentError> + where + R::Target: Router, + ES::Target: EntropySource, + NS::Target: NodeSigner, + NL::Target: NodeIdLookUp, + L::Target: Logger, + IH: Fn() -> InFlightHtlcs, + SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + { // Advance any blinded path where the introduction node is our node. if let Ok(our_node_id) = node_signer.get_node_id(Recipient::Node) { - for path in payment_params.payee.blinded_route_hints_mut().iter_mut() { + for path in route_params.payment_params.payee.blinded_route_hints_mut().iter_mut() { let introduction_node_id = match path.introduction_node() { IntroductionNode::NodeId(pubkey) => *pubkey, IntroductionNode::DirectedShortChannelId(direction, scid) => { @@ -869,15 +899,6 @@ impl OutboundPayments { } } - let amount_msat = invoice.amount_msats(); - let mut route_params = RouteParameters::from_payment_params_and_value( - payment_params, amount_msat - ); - - if let Some(max_fee_msat) = max_total_routing_fee_msat { - route_params.max_total_routing_fee_msat = Some(max_fee_msat); - } - let recipient_onion = RecipientOnionFields { payment_secret: None, payment_metadata: None, @@ -902,8 +923,8 @@ impl OutboundPayments { let payment_params = Some(route_params.payment_params.clone()); let (retryable_payment, onion_session_privs) = self.create_pending_payment( - payment_hash, recipient_onion.clone(), None, &route, - retry_strategy, payment_params, entropy_source, best_block_height + payment_hash, recipient_onion.clone(), None, &route, Some(retry_strategy), payment_params, + entropy_source, best_block_height ); match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { hash_map::Entry::Occupied(entry) => match entry.get() { From 0297a1e4ee2fb577cdfc635eceb0a7f122be33f9 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 4 Sep 2024 15:24:26 -0400 Subject: [PATCH 15/24] Support sending async payments as an always-online sender. Async receive is not yet supported. Here we process inbound release_htlc onion messages, check that they actually correspond to one of our outbound payments, and actually forward the HTLCs. Valid release_htlc receipt indicates that the recipient has now come online to receive. --- lightning/src/ln/channelmanager.rs | 27 +++++++++++- lightning/src/ln/outbound_payment.rs | 61 ++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f5254285516..fe5008eac5f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4378,6 +4378,21 @@ where res } + #[cfg(async_payments)] + fn send_payment_for_static_invoice( + &self, payment_id: PaymentId, payment_release_secret: [u8; 32] + ) -> Result<(), Bolt12PaymentError> { + let best_block_height = self.best_block.read().unwrap().height; + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + self.pending_outbound_payments + .send_payment_for_static_invoice( + payment_id, payment_release_secret, &self.router, self.list_usable_channels(), + || self.compute_inflight_htlcs(), &self.entropy_source, &self.node_signer, &self, + &self.secp_ctx, best_block_height, &self.logger, &self.pending_events, + |args| self.send_payment_along_path(args) + ) + } + /// 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. @@ -11171,7 +11186,17 @@ where None } - fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {} + fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { + #[cfg(async_payments)] { + let AsyncPaymentsContext::OutboundPayment { payment_id } = _context; + if let Err(e) = self.send_payment_for_static_invoice(payment_id, _message.payment_release_secret) { + log_trace!( + self.logger, "Failed to release held HTLC with payment id {} and release secret {:02x?}: {:?}", + payment_id, _message.payment_release_secret, e + ); + } + } + } fn release_pending_messages(&self) -> Vec<(AsyncPaymentsMessage, MessageSendInstructions)> { core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap()) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index bc1123ee2e4..c86aab51142 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -856,16 +856,17 @@ impl OutboundPayments { route_params.max_total_routing_fee_msat = Some(max_fee_msat); } self.send_payment_for_bolt12_invoice_internal( - payment_id, payment_hash, route_params, retry_strategy, router, first_hops, inflight_htlcs, - entropy_source, node_signer, node_id_lookup, secp_ctx, best_block_height, logger, - pending_events, send_payment_along_path + payment_id, payment_hash, None, route_params, retry_strategy, router, first_hops, + inflight_htlcs, entropy_source, node_signer, node_id_lookup, secp_ctx, best_block_height, + logger, pending_events, send_payment_along_path ) } fn send_payment_for_bolt12_invoice_internal< R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP, L: Deref >( - &self, payment_id: PaymentId, payment_hash: PaymentHash, mut route_params: RouteParameters, + &self, payment_id: PaymentId, payment_hash: PaymentHash, + keysend_preimage: Option, mut route_params: RouteParameters, retry_strategy: Retry, router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, logger: &L, @@ -923,12 +924,13 @@ impl OutboundPayments { let payment_params = Some(route_params.payment_params.clone()); let (retryable_payment, onion_session_privs) = self.create_pending_payment( - payment_hash, recipient_onion.clone(), None, &route, Some(retry_strategy), payment_params, - entropy_source, best_block_height + payment_hash, recipient_onion.clone(), keysend_preimage, &route, Some(retry_strategy), + payment_params, entropy_source, best_block_height ); match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { hash_map::Entry::Occupied(entry) => match entry.get() { - PendingOutboundPayment::InvoiceReceived { .. } => { + PendingOutboundPayment::InvoiceReceived { .. } + | PendingOutboundPayment::StaticInvoiceReceived { .. } => { *entry.into_mut() = retryable_payment; }, _ => return Err(Bolt12PaymentError::DuplicateInvoice), @@ -937,7 +939,7 @@ impl OutboundPayments { } let result = self.pay_route_internal( - &route, payment_hash, &recipient_onion, None, payment_id, + &route, payment_hash, &recipient_onion, keysend_preimage, payment_id, Some(route_params.final_value_msat), onion_session_privs, node_signer, best_block_height, &send_payment_along_path ); @@ -1033,6 +1035,49 @@ impl OutboundPayments { }; } + #[cfg(async_payments)] + pub(super) fn send_payment_for_static_invoice< + R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP, L: Deref + >( + &self, payment_id: PaymentId, payment_release_secret: [u8; 32], router: &R, + first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, + node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, logger: &L, + pending_events: &Mutex)>>, + send_payment_along_path: SP, + ) -> Result<(), Bolt12PaymentError> + where + R::Target: Router, + ES::Target: EntropySource, + NS::Target: NodeSigner, + NL::Target: NodeIdLookUp, + L::Target: Logger, + IH: Fn() -> InFlightHtlcs, + SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + { + let (payment_hash, keysend_preimage, route_params, retry_strategy) = + match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { + hash_map::Entry::Occupied(entry) => match entry.get() { + PendingOutboundPayment::StaticInvoiceReceived { + payment_hash, payment_release_secret: release_secret, route_params, retry_strategy, + keysend_preimage, .. + } => { + if payment_release_secret != *release_secret { + return Err(Bolt12PaymentError::UnexpectedInvoice) + } + (*payment_hash, *keysend_preimage, route_params.clone(), *retry_strategy) + }, + _ => return Err(Bolt12PaymentError::DuplicateInvoice), + }, + hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice), + }; + + self.send_payment_for_bolt12_invoice_internal( + payment_id, payment_hash, Some(keysend_preimage), route_params, retry_strategy, router, + first_hops, inflight_htlcs, entropy_source, node_signer, node_id_lookup, secp_ctx, + best_block_height, logger, pending_events, send_payment_along_path + ) + } + 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, From 985e6ac14abf5e363f0b57eaa8145f4319bba99c Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 20 Jun 2024 15:21:20 -0400 Subject: [PATCH 16/24] Timeout expired outbound async payments. --- lightning/src/ln/outbound_payment.rs | 67 +++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index c86aab51142..476201a8e67 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1892,6 +1892,22 @@ impl OutboundPayments { true } }, + PendingOutboundPayment::StaticInvoiceReceived { route_params, payment_hash, .. } => { + let is_stale = + route_params.payment_params.expiry_time.unwrap_or(u64::MAX) < + duration_since_epoch.as_secs(); + if is_stale { + let fail_ev = events::Event::PaymentFailed { + payment_id: *payment_id, + payment_hash: Some(*payment_hash), + reason: Some(PaymentFailureReason::PaymentExpired) + }; + pending_events.push_back((fail_ev, None)); + false + } else { + true + } + }, _ => true, }); } @@ -2172,11 +2188,11 @@ mod tests { use crate::blinded_path::EmptyNodeIdLookUp; use crate::events::{Event, PathFailure, PaymentFailureReason}; - use crate::ln::types::PaymentHash; + use crate::ln::types::{PaymentHash, PaymentPreimage}; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; use crate::ln::features::{Bolt12InvoiceFeatures, ChannelFeatures, NodeFeatures}; use crate::ln::msgs::{ErrorAction, LightningError}; - use crate::ln::outbound_payment::{Bolt12PaymentError, OutboundPayments, Retry, RetryableSendFailure, StaleExpiration}; + use crate::ln::outbound_payment::{Bolt12PaymentError, OutboundPayments, PendingOutboundPayment, Retry, RetryableSendFailure, StaleExpiration}; #[cfg(feature = "std")] use crate::offers::invoice::DEFAULT_RELATIVE_EXPIRY; use crate::offers::offer::OfferBuilder; @@ -2726,4 +2742,51 @@ mod tests { assert!(outbound_payments.has_pending_payments()); assert!(pending_events.lock().unwrap().is_empty()); } + + #[test] + fn time_out_unreleased_async_payments() { + let pending_events = Mutex::new(VecDeque::new()); + let outbound_payments = OutboundPayments::new(new_hash_map()); + let payment_id = PaymentId([0; 32]); + let absolute_expiry = 60; + + let mut outbounds = outbound_payments.pending_outbound_payments.lock().unwrap(); + let payment_params = PaymentParameters::from_node_id(test_utils::pubkey(42), 0) + .with_expiry_time(absolute_expiry); + let route_params = RouteParameters { + payment_params, + final_value_msat: 0, + max_total_routing_fee_msat: None, + }; + let payment_hash = PaymentHash([0; 32]); + let outbound = PendingOutboundPayment::StaticInvoiceReceived { + payment_hash, + keysend_preimage: PaymentPreimage([0; 32]), + retry_strategy: Retry::Attempts(0), + payment_release_secret: [0; 32], + route_params, + }; + outbounds.insert(payment_id, outbound); + core::mem::drop(outbounds); + + // The payment will not be removed if it isn't expired yet. + outbound_payments.remove_stale_payments(Duration::from_secs(absolute_expiry), &pending_events); + let outbounds = outbound_payments.pending_outbound_payments.lock().unwrap(); + assert_eq!(outbounds.len(), 1); + let events = pending_events.lock().unwrap(); + assert_eq!(events.len(), 0); + core::mem::drop(outbounds); + core::mem::drop(events); + + outbound_payments.remove_stale_payments(Duration::from_secs(absolute_expiry + 1), &pending_events); + let outbounds = outbound_payments.pending_outbound_payments.lock().unwrap(); + assert_eq!(outbounds.len(), 0); + let events = pending_events.lock().unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0], (Event::PaymentFailed { + payment_hash: Some(payment_hash), + payment_id, + reason: Some(PaymentFailureReason::PaymentExpired), + }, None)); + } } From 6d415b15c24b4805c73de208fd389fb9a9dc1679 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 20 Jun 2024 16:28:36 -0400 Subject: [PATCH 17/24] Support abandoning pending outbound async payments. Async payments may have very high expires because we may be waiting for days for the recipient to come online, so it's important that users be able to abandon these payments early if needed. --- lightning/src/ln/outbound_payment.rs | 74 ++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 476201a8e67..65563325c8f 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -218,20 +218,26 @@ impl PendingOutboundPayment { } fn mark_abandoned(&mut self, reason: PaymentFailureReason) { - if let PendingOutboundPayment::Retryable { session_privs, payment_hash, .. } = self { - let mut our_session_privs = new_hash_set(); - core::mem::swap(&mut our_session_privs, session_privs); - *self = PendingOutboundPayment::Abandoned { - session_privs: our_session_privs, - payment_hash: *payment_hash, - reason: Some(reason) - }; - } else if let PendingOutboundPayment::InvoiceReceived { payment_hash, .. } = self { - *self = PendingOutboundPayment::Abandoned { - session_privs: new_hash_set(), - payment_hash: *payment_hash, - reason: Some(reason) - }; + let session_privs = match self { + PendingOutboundPayment::Retryable { session_privs, .. } => { + let mut our_session_privs = new_hash_set(); + core::mem::swap(&mut our_session_privs, session_privs); + our_session_privs + }, + _ => new_hash_set(), + }; + match self { + Self::Retryable { payment_hash, .. } | + Self::InvoiceReceived { payment_hash, .. } | + Self::StaticInvoiceReceived { payment_hash, .. } => + { + *self = Self::Abandoned { + session_privs, + payment_hash: *payment_hash, + reason: Some(reason), + }; + }, + _ => {} } } @@ -2789,4 +2795,44 @@ mod tests { reason: Some(PaymentFailureReason::PaymentExpired), }, None)); } + + #[test] + fn abandon_unreleased_async_payment() { + let pending_events = Mutex::new(VecDeque::new()); + let outbound_payments = OutboundPayments::new(new_hash_map()); + let payment_id = PaymentId([0; 32]); + let absolute_expiry = 60; + + let mut outbounds = outbound_payments.pending_outbound_payments.lock().unwrap(); + let payment_params = PaymentParameters::from_node_id(test_utils::pubkey(42), 0) + .with_expiry_time(absolute_expiry); + let route_params = RouteParameters { + payment_params, + final_value_msat: 0, + max_total_routing_fee_msat: None, + }; + let payment_hash = PaymentHash([0; 32]); + let outbound = PendingOutboundPayment::StaticInvoiceReceived { + payment_hash, + keysend_preimage: PaymentPreimage([0; 32]), + retry_strategy: Retry::Attempts(0), + payment_release_secret: [0; 32], + route_params, + }; + outbounds.insert(payment_id, outbound); + core::mem::drop(outbounds); + + outbound_payments.abandon_payment( + payment_id, PaymentFailureReason::UserAbandoned, &pending_events + ); + let outbounds = outbound_payments.pending_outbound_payments.lock().unwrap(); + assert_eq!(outbounds.len(), 0); + let events = pending_events.lock().unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0], (Event::PaymentFailed { + payment_hash: Some(payment_hash), + payment_id, + reason: Some(PaymentFailureReason::UserAbandoned), + }, None)); + } } From 7dd178738d6932466df52eb143a801fe041588ba Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 20 Aug 2024 15:45:24 -0400 Subject: [PATCH 18/24] Correct docs on payment id in RecentPaymentDetails. --- lightning/src/ln/channelmanager.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fe5008eac5f..3278b8ec2a0 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2548,14 +2548,17 @@ pub const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 6 pub enum RecentPaymentDetails { /// When an invoice was requested and thus a payment has not yet been sent. AwaitingInvoice { - /// A user-provided identifier in [`ChannelManager::send_payment`] used to uniquely identify - /// a payment and ensure idempotency in LDK. + /// A user-provided identifier in [`ChannelManager::pay_for_offer`] used to uniquely identify a + /// payment and ensure idempotency in LDK. payment_id: PaymentId, }, /// When a payment is still being sent and awaiting successful delivery. Pending { - /// A user-provided identifier in [`ChannelManager::send_payment`] used to uniquely identify - /// a payment and ensure idempotency in LDK. + /// A user-provided identifier in [`send_payment`] or [`pay_for_offer`] used to uniquely + /// identify a payment and ensure idempotency in LDK. + /// + /// [`send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment + /// [`pay_for_offer`]: crate::ln::channelmanager::ChannelManager::pay_for_offer payment_id: PaymentId, /// Hash of the payment that is currently being sent but has yet to be fulfilled or /// abandoned. @@ -2568,8 +2571,11 @@ pub enum RecentPaymentDetails { /// been resolved. Upon receiving [`Event::PaymentSent`], we delay for a few minutes before the /// payment is removed from tracking. Fulfilled { - /// A user-provided identifier in [`ChannelManager::send_payment`] used to uniquely identify - /// a payment and ensure idempotency in LDK. + /// A user-provided identifier in [`send_payment`] or [`pay_for_offer`] used to uniquely + /// identify a payment and ensure idempotency in LDK. + /// + /// [`send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment + /// [`pay_for_offer`]: crate::ln::channelmanager::ChannelManager::pay_for_offer payment_id: PaymentId, /// Hash of the payment that was claimed. `None` for serializations of [`ChannelManager`] /// made before LDK version 0.0.104. @@ -2579,8 +2585,11 @@ pub enum RecentPaymentDetails { /// abandoned via [`ChannelManager::abandon_payment`], it is marked as abandoned until all /// pending HTLCs for this payment resolve and an [`Event::PaymentFailed`] is generated. Abandoned { - /// A user-provided identifier in [`ChannelManager::send_payment`] used to uniquely identify - /// a payment and ensure idempotency in LDK. + /// A user-provided identifier in [`send_payment`] or [`pay_for_offer`] used to uniquely + /// identify a payment and ensure idempotency in LDK. + /// + /// [`send_payment`]: crate::ln::channelmanager::ChannelManager::send_payment + /// [`pay_for_offer`]: crate::ln::channelmanager::ChannelManager::pay_for_offer payment_id: PaymentId, /// Hash of the payment that we have given up trying to send. payment_hash: PaymentHash, From c4f3e2567bfe9d1793a29610d21be020dca348aa Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 29 Aug 2024 15:37:24 -0400 Subject: [PATCH 19/24] Don't trigger manager persistence on unexpected release_htlc message. If someone sends us an unexpected or duplicate release_held_htlc onion message, we should simply ignore it and not persist the entire ChannelManager in response. --- lightning/src/ln/channelmanager.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 3278b8ec2a0..90d896eddf0 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4392,14 +4392,26 @@ where &self, payment_id: PaymentId, payment_release_secret: [u8; 32] ) -> Result<(), Bolt12PaymentError> { let best_block_height = self.best_block.read().unwrap().height; - let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - self.pending_outbound_payments - .send_payment_for_static_invoice( + let mut res = Ok(()); + PersistenceNotifierGuard::optionally_notify(self, || { + let outbound_pmts_res = self.pending_outbound_payments.send_payment_for_static_invoice( payment_id, payment_release_secret, &self.router, self.list_usable_channels(), || self.compute_inflight_htlcs(), &self.entropy_source, &self.node_signer, &self, &self.secp_ctx, best_block_height, &self.logger, &self.pending_events, |args| self.send_payment_along_path(args) - ) + ); + match outbound_pmts_res { + Err(Bolt12PaymentError::UnexpectedInvoice) | Err(Bolt12PaymentError::DuplicateInvoice) => { + res = outbound_pmts_res.map(|_| ()); + NotifyOption::SkipPersistNoEvents + }, + other_res => { + res = other_res; + NotifyOption::DoPersist + } + } + }); + res } /// Signals that no further attempts for the given payment should occur. Useful if you have a From 5a7f52313b9b530206cd4a8a9c812c0ca9d86ed7 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 5 Sep 2024 16:40:51 -0400 Subject: [PATCH 20/24] Rename Payment{Hash,Id} hmac creation/verification methods for offers. We want to specify that these methods are only to be used in an outbound offers payment context, because we'll be adding similar methods for the outbound async payments context in upcoming commits. --- lightning/src/ln/channelmanager.rs | 16 ++++++++-------- lightning/src/offers/signer.rs | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 90d896eddf0..7e891f93dfd 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -420,7 +420,7 @@ pub trait Verification { ) -> Hmac; /// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`]. - fn verify( + fn verify_for_offer_payment( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()>; } @@ -436,7 +436,7 @@ impl Verification for PaymentHash { /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::InboundPayment`]. - fn verify( + fn verify_for_offer_payment( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { signer::verify_payment_hash(*self, hmac, nonce, expanded_key) @@ -461,15 +461,15 @@ impl Verification for PaymentId { fn hmac_for_offer_payment( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { - signer::hmac_for_payment_id(*self, nonce, expanded_key) + signer::hmac_for_offer_payment_id(*self, nonce, expanded_key) } /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::OutboundPayment`]. - fn verify( + fn verify_for_offer_payment( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { - signer::verify_payment_id(*self, hmac, nonce, expanded_key) + signer::verify_offer_payment_id(*self, hmac, nonce, expanded_key) } } @@ -11144,7 +11144,7 @@ where 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() { + if payment_id.verify_for_offer_payment(hmac, nonce, expanded_key).is_err() { return None } payment_id @@ -11157,7 +11157,7 @@ where OffersMessage::InvoiceError(invoice_error) => { let payment_hash = match context { Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => { - match payment_hash.verify(hmac, nonce, expanded_key) { + match payment_hash.verify_for_offer_payment(hmac, nonce, expanded_key) { Ok(_) => Some(payment_hash), Err(_) => None, } @@ -11170,7 +11170,7 @@ where match context { Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if let Ok(()) = payment_id.verify(hmac, nonce, expanded_key) { + if let Ok(()) = payment_id.verify_for_offer_payment(hmac, nonce, expanded_key) { self.abandon_payment_with_reason( payment_id, PaymentFailureReason::InvoiceRequestRejected, ); diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 907e478227f..152c7af5736 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -38,7 +38,7 @@ const WITHOUT_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[3; 16]; const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16]; // HMAC input for a `PaymentId`. The HMAC is used in `OffersContext::OutboundPayment`. -const PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16]; +const OFFER_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16]; // HMAC input for a `PaymentHash`. The HMAC is used in `OffersContext::InboundPayment`. const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[6; 16]; @@ -399,23 +399,23 @@ fn hmac_for_message<'a>( Ok(hmac) } -pub(crate) fn hmac_for_payment_id( +pub(crate) fn hmac_for_offer_payment_id( payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey, ) -> Hmac { const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment ID ~"; let mut hmac = expanded_key.hmac_for_offer(); hmac.input(IV_BYTES); hmac.input(&nonce.0); - hmac.input(PAYMENT_ID_HMAC_INPUT); + hmac.input(OFFER_PAYMENT_ID_HMAC_INPUT); hmac.input(&payment_id.0); Hmac::from_engine(hmac) } -pub(crate) fn verify_payment_id( +pub(crate) fn verify_offer_payment_id( payment_id: PaymentId, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, ) -> Result<(), ()> { - if hmac_for_payment_id(payment_id, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } + if hmac_for_offer_payment_id(payment_id, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } } pub(crate) fn hmac_for_payment_hash( From 615eefb543822bf361bb11de07bf391a99095352 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 5 Sep 2024 17:32:08 -0400 Subject: [PATCH 21/24] Verify inbound ReleaseHeldHtlc messages via hmac. See AsyncPaymentsContext::hmac, but this prevents the recipient from deanonymizing us. Without this, if they are able to guess the correct payment id, then they could create a blinded path to us and confirm our identity. We also move the PAYMENT_HASH_HMAC_INPUT const to use &[7; 16], which is safe because this const was added since the last release. This ordering reads more smoothly. --- lightning/src/blinded_path/message.rs | 15 +++++++++- lightning/src/ln/channelmanager.rs | 27 ++++++++++++++++-- lightning/src/offers/signer.rs | 41 +++++++++++++++++++++------ 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index e6aa4e340ce..805d35a010c 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -383,7 +383,18 @@ pub enum AsyncPaymentsContext { /// which of our pending outbound payments should be released to its often-offline payee. /// /// [`Offer`]: crate::offers::offer::Offer - payment_id: PaymentId + payment_id: PaymentId, + /// A nonce used for authenticating that a [`ReleaseHeldHtlc`] message is valid for a preceding + /// [`HeldHtlcAvailable`] message. + /// + /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + nonce: Nonce, + /// Authentication code for the [`PaymentId`]. + /// + /// Prevents the recipient from being able to deanonymize us by creating a blinded path to us + /// containing the expected [`PaymentId`]. + hmac: Hmac, }, } @@ -412,6 +423,8 @@ impl_writeable_tlv_based_enum!(OffersContext, impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (0, OutboundPayment) => { (0, payment_id, required), + (2, nonce, required), + (4, hmac, required), }, ); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 7e891f93dfd..b2f182a6365 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -453,6 +453,24 @@ pub struct PaymentId(pub [u8; Self::LENGTH]); impl PaymentId { /// Number of bytes in the id. pub const LENGTH: usize = 32; + + /// Constructs an HMAC to include in [`AsyncPaymentsContext::OutboundPayment`] for the payment id + /// along with the given [`Nonce`]. + #[cfg(async_payments)] + pub fn hmac_for_async_payment( + &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, + ) -> Hmac { + signer::hmac_for_async_payment_id(*self, nonce, expanded_key) + } + + /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an + /// [`AsyncPaymentsContext::OutboundPayment`]. + #[cfg(async_payments)] + pub fn verify_for_async_payment( + &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, + ) -> Result<(), ()> { + signer::verify_async_payment_id(*self, hmac, nonce, expanded_key) + } } impl Verification for PaymentId { @@ -4353,8 +4371,12 @@ where } }; + let nonce = Nonce::from_entropy_source(&*self.entropy_source); + let hmac = payment_id.hmac_for_async_payment(nonce, &self.inbound_payment_key); let reply_paths = match self.create_blinded_paths( - MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundPayment { payment_id }) + MessageContext::AsyncPayments( + AsyncPaymentsContext::OutboundPayment { payment_id, nonce, hmac } + ) ) { Ok(paths) => paths, Err(()) => { @@ -11209,7 +11231,8 @@ where fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { #[cfg(async_payments)] { - let AsyncPaymentsContext::OutboundPayment { payment_id } = _context; + let AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } = _context; + if payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key).is_err() { return } if let Err(e) = self.send_payment_for_static_invoice(payment_id, _message.payment_release_secret) { log_trace!( self.logger, "Failed to release held HTLC with payment id {} and release secret {:02x?}: {:?}", diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 152c7af5736..d8caa2175fe 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -39,9 +39,12 @@ const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16]; // HMAC input for a `PaymentId`. The HMAC is used in `OffersContext::OutboundPayment`. const OFFER_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16]; +// HMAC input for a `PaymentId`. The HMAC is used in `AsyncPaymentsContext::OutboundPayment`. +#[cfg(async_payments)] +const ASYNC_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[6; 16]; // HMAC input for a `PaymentHash`. The HMAC is used in `OffersContext::InboundPayment`. -const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[6; 16]; +const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[7; 16]; /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. @@ -402,14 +405,7 @@ fn hmac_for_message<'a>( pub(crate) fn hmac_for_offer_payment_id( payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey, ) -> Hmac { - const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment ID ~"; - let mut hmac = expanded_key.hmac_for_offer(); - hmac.input(IV_BYTES); - hmac.input(&nonce.0); - hmac.input(OFFER_PAYMENT_ID_HMAC_INPUT); - hmac.input(&payment_id.0); - - Hmac::from_engine(hmac) + hmac_for_payment_id(payment_id, nonce, OFFER_PAYMENT_ID_HMAC_INPUT, expanded_key) } pub(crate) fn verify_offer_payment_id( @@ -436,3 +432,30 @@ pub(crate) fn verify_payment_hash( ) -> Result<(), ()> { if hmac_for_payment_hash(payment_hash, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } } + +#[cfg(async_payments)] +pub(crate) fn hmac_for_async_payment_id( + payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + hmac_for_payment_id(payment_id, nonce, ASYNC_PAYMENT_ID_HMAC_INPUT, expanded_key) +} + +#[cfg(async_payments)] +pub(crate) fn verify_async_payment_id( + payment_id: PaymentId, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_async_payment_id(payment_id, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } +} + +fn hmac_for_payment_id( + payment_id: PaymentId, nonce: Nonce, hmac_input: &[u8; 16], expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment ID ~"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(hmac_input); + hmac.input(&payment_id.0); + + Hmac::from_engine(hmac) +} From 26d1582c3bbd2cb36758c7ea52516b6eaae85b56 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 6 Sep 2024 15:28:43 -0400 Subject: [PATCH 22/24] Add new Bolt12PaymentError for failed blinded path creation. Currently used when initiating an async payment via held_htlc_available OM. This OM needs a reply path back to us, so use this error for our invoice_error OM if we fail to create said reply path. --- lightning/src/ln/channelmanager.rs | 8 +++++++- lightning/src/ln/outbound_payment.rs | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b2f182a6365..63faa91f583 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4381,7 +4381,7 @@ where Ok(paths) => paths, Err(()) => { self.abandon_payment_with_reason(payment_id, PaymentFailureReason::RouteNotFound); - res = Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::RouteNotFound)); + res = Err(Bolt12PaymentError::BlindedPathCreationFailed); return NotifyOption::DoPersist } }; @@ -11018,6 +11018,12 @@ where log_trace!($logger, "Failed paying invoice: {:?}", e); InvoiceError::from_string(format!("{:?}", e)) }, + #[cfg(async_payments)] + Err(Bolt12PaymentError::BlindedPathCreationFailed) => { + let err_msg = "Failed to create a blinded path back to ourselves"; + log_trace!($logger, "{}", err_msg); + InvoiceError::from_string(err_msg.to_string()) + }, Err(Bolt12PaymentError::UnexpectedInvoice) | Err(Bolt12PaymentError::DuplicateInvoice) | Ok(()) => return None, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 65563325c8f..092f1a7fc20 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -535,6 +535,15 @@ pub enum Bolt12PaymentError { UnknownRequiredFeatures, /// The invoice was valid for the corresponding [`PaymentId`], but sending the payment failed. SendingFailed(RetryableSendFailure), + #[cfg(async_payments)] + /// Failed to create a blinded path back to ourselves. + /// + /// We attempted to initiate payment to a [`StaticInvoice`] but failed to create a reply path for + /// our [`HeldHtlcAvailable`] message. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + BlindedPathCreationFailed, } /// Indicates that we failed to send a payment probe. Further errors may be surfaced later via From 4bcf53e597543ece3684cd49ce1afa4331ce59ff Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 10 Sep 2024 18:50:04 -0400 Subject: [PATCH 23/24] Document PendingOutboundPayment::{Static}InvoiceReceived semantics. While these variants may sound similar, they are very different. One is so temporary it's never even persisted to disk, the other is a state we will stay in for hours or days. See added docs for more info. --- lightning/src/ln/outbound_payment.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 092f1a7fc20..ac2578a6721 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -64,6 +64,9 @@ pub(crate) enum PendingOutboundPayment { max_total_routing_fee_msat: Option, retryable_invoice_request: Option }, + // This state will never be persisted to disk because we transition from `AwaitingInvoice` to + // `Retryable` atomically within the `ChannelManager::total_consistency_lock`. Useful to avoid + // holding the `OutboundPayments::pending_outbound_payments` lock during pathfinding. InvoiceReceived { payment_hash: PaymentHash, retry_strategy: Retry, @@ -71,6 +74,10 @@ pub(crate) enum PendingOutboundPayment { // used anywhere. max_total_routing_fee_msat: Option, }, + // This state applies when we are paying an often-offline recipient and another node on the + // network served us a static invoice on the recipient's behalf in response to our invoice + // request. As a result, once a payment gets in this state it will remain here until the recipient + // comes back online, which may take hours or even days. StaticInvoiceReceived { payment_hash: PaymentHash, keysend_preimage: PaymentPreimage, From 6e27aecb408b79d1c5d3383a3e3cc331691a95c7 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 11 Sep 2024 10:20:58 -0400 Subject: [PATCH 24/24] Remove payment_release_secret from async payments messages. This field isn't necessary because we already authenticate the messages via the blinded reply paths payment_id, nonce and HMAC. --- fuzz/src/onion_message.rs | 5 +--- lightning/src/ln/channelmanager.rs | 22 +++++++---------- lightning/src/ln/outbound_payment.rs | 24 ++++++------------- lightning/src/onion_message/async_payments.rs | 19 ++++----------- 4 files changed, 21 insertions(+), 49 deletions(-) diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 26bb0bc0ee4..5cd45238df2 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -126,10 +126,7 @@ impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { Some(resp) => resp, None => return None, }; - Some(( - ReleaseHeldHtlc { payment_release_secret: message.payment_release_secret }, - responder.respond(), - )) + Some((ReleaseHeldHtlc {}, responder.respond())) } fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {} } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 63faa91f583..03164e04fb5 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4359,8 +4359,8 @@ where invoice, payment_id, features, best_block_height, &*self.entropy_source, &self.pending_events ); - let payment_release_secret = match outbound_pmts_res { - Ok(secret) => secret, + match outbound_pmts_res { + Ok(()) => {}, Err(Bolt12PaymentError::UnexpectedInvoice) | Err(Bolt12PaymentError::DuplicateInvoice) => { res = outbound_pmts_res.map(|_| ()); return NotifyOption::SkipPersistNoEvents @@ -4397,9 +4397,7 @@ where destination: Destination::BlindedPath(invoice_path.clone()), reply_path: reply_path.clone(), }; - let message = AsyncPaymentsMessage::HeldHtlcAvailable( - HeldHtlcAvailable { payment_release_secret } - ); + let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); pending_async_payments_messages.push((message, instructions)); }); @@ -4411,16 +4409,15 @@ where #[cfg(async_payments)] fn send_payment_for_static_invoice( - &self, payment_id: PaymentId, payment_release_secret: [u8; 32] + &self, payment_id: PaymentId ) -> Result<(), Bolt12PaymentError> { let best_block_height = self.best_block.read().unwrap().height; let mut res = Ok(()); PersistenceNotifierGuard::optionally_notify(self, || { let outbound_pmts_res = self.pending_outbound_payments.send_payment_for_static_invoice( - payment_id, payment_release_secret, &self.router, self.list_usable_channels(), - || self.compute_inflight_htlcs(), &self.entropy_source, &self.node_signer, &self, - &self.secp_ctx, best_block_height, &self.logger, &self.pending_events, - |args| self.send_payment_along_path(args) + payment_id, &self.router, self.list_usable_channels(), || self.compute_inflight_htlcs(), + &self.entropy_source, &self.node_signer, &self, &self.secp_ctx, best_block_height, + &self.logger, &self.pending_events, |args| self.send_payment_along_path(args) ); match outbound_pmts_res { Err(Bolt12PaymentError::UnexpectedInvoice) | Err(Bolt12PaymentError::DuplicateInvoice) => { @@ -11239,10 +11236,9 @@ where #[cfg(async_payments)] { let AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } = _context; if payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key).is_err() { return } - if let Err(e) = self.send_payment_for_static_invoice(payment_id, _message.payment_release_secret) { + if let Err(e) = self.send_payment_for_static_invoice(payment_id) { log_trace!( - self.logger, "Failed to release held HTLC with payment id {} and release secret {:02x?}: {:?}", - payment_id, _message.payment_release_secret, e + self.logger, "Failed to release held HTLC with payment id {}: {:?}", payment_id, e ); } } diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index ac2578a6721..7a850889d4c 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -82,7 +82,6 @@ pub(crate) enum PendingOutboundPayment { payment_hash: PaymentHash, keysend_preimage: PaymentPreimage, retry_strategy: Retry, - payment_release_secret: [u8; 32], route_params: RouteParameters, }, Retryable { @@ -984,7 +983,7 @@ impl OutboundPayments { &self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures, best_block_height: u32, entropy_source: ES, pending_events: &Mutex)>> - ) -> Result<[u8; 32], Bolt12PaymentError> where ES::Target: EntropySource { + ) -> Result<(), Bolt12PaymentError> where ES::Target: EntropySource { macro_rules! abandon_with_entry { ($payment: expr, $reason: expr) => { $payment.get_mut().mark_abandoned($reason); @@ -1029,7 +1028,6 @@ impl OutboundPayments { }; 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; @@ -1046,10 +1044,9 @@ impl OutboundPayments { payment_hash, keysend_preimage, retry_strategy: *retry_strategy, - payment_release_secret, route_params, }; - return Ok(payment_release_secret) + return Ok(()) }, _ => return Err(Bolt12PaymentError::DuplicateInvoice), }, @@ -1061,9 +1058,9 @@ impl OutboundPayments { pub(super) fn send_payment_for_static_invoice< R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP, L: Deref >( - &self, payment_id: PaymentId, payment_release_secret: [u8; 32], router: &R, - first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, - node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, logger: &L, + &self, payment_id: PaymentId, router: &R, first_hops: Vec, inflight_htlcs: IH, + entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, + secp_ctx: &Secp256k1, best_block_height: u32, logger: &L, pending_events: &Mutex)>>, send_payment_along_path: SP, ) -> Result<(), Bolt12PaymentError> @@ -1080,12 +1077,8 @@ impl OutboundPayments { match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::StaticInvoiceReceived { - payment_hash, payment_release_secret: release_secret, route_params, retry_strategy, - keysend_preimage, .. + payment_hash, route_params, retry_strategy, keysend_preimage, .. } => { - if payment_release_secret != *release_secret { - return Err(Bolt12PaymentError::UnexpectedInvoice) - } (*payment_hash, *keysend_preimage, route_params.clone(), *retry_strategy) }, _ => return Err(Bolt12PaymentError::DuplicateInvoice), @@ -2196,8 +2189,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (0, payment_hash, required), (2, keysend_preimage, required), (4, retry_strategy, required), - (6, payment_release_secret, required), - (8, route_params, required), + (6, route_params, required), }, ); @@ -2785,7 +2777,6 @@ mod tests { payment_hash, keysend_preimage: PaymentPreimage([0; 32]), retry_strategy: Retry::Attempts(0), - payment_release_secret: [0; 32], route_params, }; outbounds.insert(payment_id, outbound); @@ -2832,7 +2823,6 @@ mod tests { payment_hash, keysend_preimage: PaymentPreimage([0; 32]), retry_strategy: Retry::Attempts(0), - payment_release_secret: [0; 32], route_params, }; outbounds.insert(payment_id, outbound); diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index e2a7f7bf74e..cc4ca5edfb0 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -61,18 +61,11 @@ pub enum AsyncPaymentsMessage { /// accompanying this onion message should be used to send a [`ReleaseHeldHtlc`] response, which /// will cause the upstream HTLC to be released. #[derive(Clone, Debug)] -pub struct HeldHtlcAvailable { - /// The secret that will be used by the recipient of this message to release the held HTLC. - pub payment_release_secret: [u8; 32], -} +pub struct HeldHtlcAvailable {} /// Releases the HTLC corresponding to an inbound [`HeldHtlcAvailable`] message. #[derive(Clone, Debug)] -pub struct ReleaseHeldHtlc { - /// Used to release the HTLC held upstream if it matches the corresponding - /// [`HeldHtlcAvailable::payment_release_secret`]. - pub payment_release_secret: [u8; 32], -} +pub struct ReleaseHeldHtlc {} impl OnionMessageContents for ReleaseHeldHtlc { fn tlv_type(&self) -> u64 { @@ -88,13 +81,9 @@ impl OnionMessageContents for ReleaseHeldHtlc { } } -impl_writeable_tlv_based!(HeldHtlcAvailable, { - (0, payment_release_secret, required), -}); +impl_writeable_tlv_based!(HeldHtlcAvailable, {}); -impl_writeable_tlv_based!(ReleaseHeldHtlc, { - (0, payment_release_secret, required), -}); +impl_writeable_tlv_based!(ReleaseHeldHtlc, {}); impl AsyncPaymentsMessage { /// Returns whether `tlv_type` corresponds to a TLV record for async payment messages.