diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 19666847a23..5cd45238df2 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}; @@ -124,12 +126,9 @@ 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) {} + fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) {} } #[derive(Debug)] diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 256483fec01..805d35a010c 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,41 @@ 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, + /// 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, + }, +} + impl_writeable_tlv_based_enum!(MessageContext, {0, Offers} => (), {1, Custom} => (), + {2, AsyncPayments} => (), ); impl_writeable_tlv_based_enum!(OffersContext, @@ -384,6 +420,14 @@ impl_writeable_tlv_based_enum!(OffersContext, }, ); +impl_writeable_tlv_based_enum!(AsyncPaymentsContext, + (0, OutboundPayment) => { + (0, payment_id, required), + (2, nonce, required), + (4, hmac, 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/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 5f8dc1e5541..03164e04fb5 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}; @@ -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}; @@ -418,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<(), ()>; } @@ -434,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) @@ -451,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 { @@ -459,15 +479,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) } } @@ -2060,6 +2080,8 @@ where // // `pending_offers_messages` // +// `pending_async_payments_messages` +// // `total_consistency_lock` // | // |__`forward_htlcs` @@ -2315,6 +2337,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>, @@ -2543,14 +2566,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. @@ -2563,8 +2589,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. @@ -2574,8 +2603,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, @@ -3135,6 +3167,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()), @@ -3370,6 +3403,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, @@ -4311,6 +4347,92 @@ 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 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, best_block_height, &*self.entropy_source, + &self.pending_events + ); + match outbound_pmts_res { + Ok(()) => {}, + Err(Bolt12PaymentError::UnexpectedInvoice) | Err(Bolt12PaymentError::DuplicateInvoice) => { + res = outbound_pmts_res.map(|_| ()); + return NotifyOption::SkipPersistNoEvents + }, + Err(e) => { + res = Err(e); + return NotifyOption::DoPersist + } + }; + + 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, nonce, hmac } + ) + ) { + Ok(paths) => paths, + Err(()) => { + self.abandon_payment_with_reason(payment_id, PaymentFailureReason::RouteNotFound); + res = Err(Bolt12PaymentError::BlindedPathCreationFailed); + 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 {}); + pending_async_payments_messages.push((message, instructions)); + }); + + NotifyOption::DoPersist + }); + + res + } + + #[cfg(async_payments)] + fn send_payment_for_static_invoice( + &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, &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 /// pending outbound payment with retries remaining, but wish to stop retrying the payment before /// retries are exhausted. @@ -9124,7 +9246,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)?; @@ -9244,9 +9368,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)?; @@ -9394,7 +9518,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)) } } @@ -9415,7 +9539,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; @@ -9428,7 +9552,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(())) } @@ -10832,11 +10956,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(_) => {} @@ -10877,6 +11001,41 @@ 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)) + }, + #[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, + }; + + 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 { @@ -11003,48 +11162,27 @@ 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) => { - match responder { - Some(responder) => { - return Some((OffersMessage::InvoiceError( - InvoiceError::from_string("Static invoices not yet supported".to_string()) - ), responder.respond())); + OffersMessage::StaticInvoice(invoice) => { + let payment_id = match context { + Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { + if payment_id.verify_for_offer_payment(hmac, nonce, expanded_key).is_err() { + return None + } + payment_id }, - None => return None, - } + _ => return 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 { 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, } @@ -11057,7 +11195,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, ); @@ -11094,10 +11232,20 @@ where None } - fn release_held_htlc(&self, _message: ReleaseHeldHtlc) {} + fn release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { + #[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) { + log_trace!( + self.logger, "Failed to release held HTLC with payment id {}: {:?}", payment_id, e + ); + } + } + } fn release_pending_messages(&self) -> Vec<(AsyncPaymentsMessage, MessageSendInstructions)> { - Vec::new() + core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap()) } } @@ -11718,6 +11866,7 @@ where } PendingOutboundPayment::AwaitingInvoice { .. } => {}, PendingOutboundPayment::InvoiceReceived { .. } => {}, + PendingOutboundPayment::StaticInvoiceReceived { .. } => {}, PendingOutboundPayment::Fulfilled { .. } => {}, PendingOutboundPayment::Abandoned { .. } => {}, } @@ -12827,6 +12976,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()), diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index ca0d7c17d99..7a850889d4c 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}; @@ -58,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, @@ -65,6 +74,16 @@ 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, + retry_strategy: Retry, + route_params: RouteParameters, + }, Retryable { retry_strategy: Option, attempts: PaymentAttempts, @@ -182,6 +201,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,27 +216,34 @@ 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 }; } 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), + }; + }, + _ => {} } } @@ -230,7 +257,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 +287,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 +321,7 @@ impl PendingOutboundPayment { }, PendingOutboundPayment::AwaitingInvoice { .. } => 0, PendingOutboundPayment::InvoiceReceived { .. } => 0, + PendingOutboundPayment::StaticInvoiceReceived { .. } => 0, } } } @@ -511,6 +541,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 @@ -811,7 +850,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, @@ -831,11 +870,42 @@ 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, 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, + 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, + 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) => { @@ -851,15 +921,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, @@ -884,12 +945,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, - 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), @@ -898,7 +960,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 ); @@ -916,6 +978,121 @@ impl OutboundPayments { Ok(()) } + #[cfg(async_payments)] + pub(super) fn static_invoice_received( + &self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures, + best_block_height: u32, entropy_source: ES, + pending_events: &Mutex)>> + ) -> Result<(), 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) + } + 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(_) => { + // 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 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, + retry_strategy: *retry_strategy, + route_params, + }; + return Ok(()) + }, + _ => return Err(Bolt12PaymentError::DuplicateInvoice), + }, + hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice), + }; + } + + #[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, 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, route_params, retry_strategy, keysend_preimage, .. + } => { + (*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, @@ -1195,6 +1372,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 @@ -1725,6 +1907,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, }); } @@ -1985,6 +2183,14 @@ 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, route_params, required), + }, ); #[cfg(test)] @@ -1996,11 +2202,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; @@ -2550,4 +2756,89 @@ 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), + 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)); + } + + #[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), + 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)); + } } diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index ec0e86c8b57..5293906f91c 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/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/signer.rs b/lightning/src/offers/signer.rs index 907e478227f..d8caa2175fe 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -38,10 +38,13 @@ 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 `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. @@ -399,23 +402,16 @@ 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(&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_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( @@ -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) +} 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 { diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 89756d9f1f3..cc4ca5edfb0 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. /// @@ -60,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 { @@ -87,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. 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 7445c3c4c0b..ab7ccbdab38 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,8 +1591,8 @@ where let context = match context { 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 } }; @@ -1608,14 +1612,22 @@ 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 { 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 } }; @@ -1753,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( diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 7e063dc9cee..4215f66b2da 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 {