From 43029bf36b111c1902f560599f2c30916057c56a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 26 Jul 2024 15:44:36 -0400 Subject: [PATCH 01/10] Fix outdated static invoice docs. --- lightning/src/offers/static_invoice.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 39c17eb3bcc..fdeffcb0c20 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -49,7 +49,7 @@ use crate::offers::invoice::is_expired; use crate::prelude::*; /// Static invoices default to expiring after 2 weeks. -const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24 * 14); +pub const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24 * 14); /// Tag for the hash function used when signing a [`StaticInvoice`]'s merkle root. pub const SIGNATURE_TAG: &'static str = concat!("lightning", "static_invoice", "signature"); @@ -102,8 +102,8 @@ pub struct StaticInvoiceBuilder<'a> { impl<'a> StaticInvoiceBuilder<'a> { /// Initialize a [`StaticInvoiceBuilder`] from the given [`Offer`]. /// - /// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours - /// after `created_at`. + /// The invoice's expiration will default to [`DEFAULT_RELATIVE_EXPIRY`] after `created_at` unless + /// overridden by [`StaticInvoiceBuilder::relative_expiry`]. pub fn for_offer_using_derived_keys( offer: &'a Offer, payment_paths: Vec, message_paths: Vec, created_at: Duration, expanded_key: &ExpandedKey, From 605cc3cd250bfc979b6a03fe5e4647c20d0e77da Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 29 Oct 2024 15:37:19 -0400 Subject: [PATCH 02/10] Don't take() outbound invoice requests on retry Prior to this patch, we would take() the invoice request stored for AwaitingInvoice outbound payments when retrying sending the invoice request onion message. This doesn't work for async payments because we need to keep the invoice request stored for inclusion in the payment onion. Therefore, clone it instead of take()ing it. --- lightning/src/ln/channelmanager.rs | 4 +++- lightning/src/ln/outbound_payment.rs | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b312e0055ee..e3a25591dbf 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10046,6 +10046,7 @@ where let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), nonce, + needs_retry: true, }; self.pending_outbound_payments .add_new_awaiting_invoice( @@ -11915,7 +11916,7 @@ where .pending_outbound_payments .release_invoice_requests_awaiting_invoice() { - let RetryableInvoiceRequest { invoice_request, nonce } = retryable_invoice_request; + let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, @@ -12250,6 +12251,7 @@ where let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), nonce, + needs_retry: true, }; self.pending_outbound_payments .received_offer(payment_id, Some(retryable_invoice_request)) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 80d93387ac3..28d4a02965d 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -134,13 +134,16 @@ pub(crate) enum PendingOutboundPayment { }, } +#[derive(Clone)] pub(crate) struct RetryableInvoiceRequest { pub(crate) invoice_request: InvoiceRequest, pub(crate) nonce: Nonce, + pub(super) needs_retry: bool, } impl_writeable_tlv_based!(RetryableInvoiceRequest, { (0, invoice_request, required), + (1, needs_retry, (default_value, true)), (2, nonce, required), }); @@ -760,7 +763,12 @@ pub(super) struct OutboundPayments { impl OutboundPayments { pub(super) fn new(pending_outbound_payments: HashMap) -> Self { let has_invoice_requests = pending_outbound_payments.values().any(|payment| { - matches!(payment, PendingOutboundPayment::AwaitingInvoice { retryable_invoice_request: Some(_), .. }) + matches!( + payment, + PendingOutboundPayment::AwaitingInvoice { + retryable_invoice_request: Some(invreq), .. + } if invreq.needs_retry + ) }); Self { @@ -2228,11 +2236,12 @@ impl OutboundPayments { .iter_mut() .filter_map(|(payment_id, payment)| { if let PendingOutboundPayment::AwaitingInvoice { - retryable_invoice_request, .. + retryable_invoice_request: Some(invreq), .. } = payment { - retryable_invoice_request.take().map(|retryable_invoice_request| { - (*payment_id, retryable_invoice_request) - }) + if invreq.needs_retry { + invreq.needs_retry = false; + Some((*payment_id, invreq.clone())) + } else { None } } else { None } From 08d81fbe4e09631755373802915552019e3c783e Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 13 Nov 2024 15:49:29 -0500 Subject: [PATCH 03/10] Fix failure to abandon async payments on invalid static invoice Prior to this fix, we would attempt to mark outbound async payments as abandoned but silently fail because they were in state AwaitingInvoice, which the mark_abandoned utility doesn't currently work for. These payments would eventually be removed by the remove_stale_payments method, but there would be a delay in generating the PaymentFailed event. Move to manually removing the outbound payment entry. --- lightning/src/ln/outbound_payment.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 28d4a02965d..803aa615288 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1016,17 +1016,16 @@ impl OutboundPayments { ) -> 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(); - } - } + assert!( + matches!($payment.get(), PendingOutboundPayment::AwaitingInvoice { .. }), + "Generating PaymentFailed for unexpected outbound payment type can result in funds loss" + ); + pending_events.lock().unwrap().push_back((events::Event::PaymentFailed { + payment_id, + payment_hash: None, + reason: Some($reason), + }, None)); + $payment.remove(); } } From e8100758b965fcdae4ae87bca9cac938fee61d25 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 24 Jun 2024 13:36:55 -0400 Subject: [PATCH 04/10] Make create_blinded_payment_paths methods amount optional. Useful for creating payment paths for static invoices which are typically amount-less. --- fuzz/src/chanmon_consistency.rs | 2 +- fuzz/src/full_stack.rs | 2 +- lightning/src/ln/channelmanager.rs | 6 +++--- lightning/src/routing/router.rs | 10 +++++----- lightning/src/util/test_utils.rs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index ca3f0028f3a..aac26a7cfb8 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -133,7 +133,7 @@ impl Router for FuzzRouter { fn create_blinded_payment_paths( &self, _recipient: PublicKey, _first_hops: Vec, _tlvs: ReceiveTlvs, - _amount_msats: u64, _secp_ctx: &Secp256k1, + _amount_msats: Option, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index c1f2dd11b1e..a11dae31baf 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -160,7 +160,7 @@ impl Router for FuzzRouter { fn create_blinded_payment_paths( &self, _recipient: PublicKey, _first_hops: Vec, _tlvs: ReceiveTlvs, - _amount_msats: u64, _secp_ctx: &Secp256k1, + _amount_msats: Option, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e3a25591dbf..77e84c66cd7 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10182,7 +10182,7 @@ where Ok((payment_hash, payment_secret)) => { let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let payment_paths = self.create_blinded_payment_paths( - amount_msats, payment_secret, payment_context + Some(amount_msats), payment_secret, payment_context ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -10489,7 +10489,7 @@ where /// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to /// [`Router::create_blinded_payment_paths`]. fn create_blinded_payment_paths( - &self, amount_msats: u64, payment_secret: PaymentSecret, payment_context: PaymentContext + &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext ) -> Result, ()> { let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; @@ -12048,7 +12048,7 @@ where invoice_request: invoice_request.fields(), }); let payment_paths = match self.create_blinded_payment_paths( - amount_msats, payment_secret, payment_context + Some(amount_msats), payment_secret, payment_context ) { Ok(payment_paths) => payment_paths, Err(()) => { diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 78a93aa0d39..a257d2b4abe 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -95,7 +95,7 @@ impl>, L: Deref, ES: Deref, S: Deref, SP: Size T: secp256k1::Signing + secp256k1::Verification > ( &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, - amount_msats: u64, secp_ctx: &Secp256k1 + amount_msats: Option, secp_ctx: &Secp256k1 ) -> Result, ()> { // Limit the number of blinded paths that are computed. const MAX_PAYMENT_PATHS: usize = 3; @@ -120,9 +120,9 @@ impl>, L: Deref, ES: Deref, S: Deref, SP: Size let paths = first_hops.into_iter() .filter(|details| details.counterparty.features.supports_route_blinding()) - .filter(|details| amount_msats <= details.inbound_capacity_msat) - .filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0)) - .filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX)) + .filter(|details| amount_msats.unwrap_or(0) <= details.inbound_capacity_msat) + .filter(|details| amount_msats.unwrap_or(u64::MAX) >= details.inbound_htlc_minimum_msat.unwrap_or(0)) + .filter(|details| amount_msats.unwrap_or(0) <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX)) // Limit to peers with announced channels unless the recipient is unannounced. .filter(|details| network_graph .node(&NodeId::from_pubkey(&details.counterparty.node_id)) @@ -218,7 +218,7 @@ pub trait Router { T: secp256k1::Signing + secp256k1::Verification > ( &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, - amount_msats: u64, secp_ctx: &Secp256k1 + amount_msats: Option, secp_ctx: &Secp256k1 ) -> Result, ()>; } diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 442a709866c..9db85508a5b 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -252,7 +252,7 @@ impl<'a> Router for TestRouter<'a> { T: secp256k1::Signing + secp256k1::Verification >( &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, - amount_msats: u64, secp_ctx: &Secp256k1, + amount_msats: Option, secp_ctx: &Secp256k1, ) -> Result, ()> { let mut expected_paths = self.next_blinded_payment_paths.lock().unwrap(); if expected_paths.is_empty() { From 542deeb4dd19a0bf58a1069ca85cc160f7bf5cb7 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 8 Jul 2024 16:56:24 -0400 Subject: [PATCH 05/10] Factor invoice expiry into blinded path max_cltv_expiry Will be useful for static invoices' blinded paths, which may have long expiries. Rather than having a default max_cltv_expiry, we now base it on the invoice expiry. --- lightning/src/ln/channelmanager.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 77e84c66cd7..fac1689ec8f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10182,7 +10182,7 @@ where Ok((payment_hash, payment_secret)) => { let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let payment_paths = self.create_blinded_payment_paths( - Some(amount_msats), payment_secret, payment_context + Some(amount_msats), payment_secret, payment_context, relative_expiry, ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -10489,7 +10489,8 @@ where /// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to /// [`Router::create_blinded_payment_paths`]. fn create_blinded_payment_paths( - &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext + &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext, + relative_expiry_seconds: u32 ) -> Result, ()> { let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; @@ -10497,8 +10498,13 @@ where let first_hops = self.list_usable_channels(); let payee_node_id = self.get_our_node_id(); - let max_cltv_expiry = self.best_block.read().unwrap().height + CLTV_FAR_FAR_AWAY - + LATENCY_GRACE_PERIOD_BLOCKS; + + // Assume shorter than usual block times to avoid spuriously failing payments too early. + const SECONDS_PER_BLOCK: u32 = 9 * 60; + let relative_expiry_blocks = relative_expiry_seconds / SECONDS_PER_BLOCK; + let max_cltv_expiry = core::cmp::max(relative_expiry_blocks, CLTV_FAR_FAR_AWAY) + .saturating_add(LATENCY_GRACE_PERIOD_BLOCKS) + .saturating_add(self.best_block.read().unwrap().height); let payee_tlvs = UnauthenticatedReceiveTlvs { payment_secret, @@ -12048,7 +12054,7 @@ where invoice_request: invoice_request.fields(), }); let payment_paths = match self.create_blinded_payment_paths( - Some(amount_msats), payment_secret, payment_context + Some(amount_msats), payment_secret, payment_context, relative_expiry ) { Ok(payment_paths) => payment_paths, Err(()) => { From 84f200f0cacf0de57c24d5838986498ba3fc5183 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 24 Jun 2024 15:51:27 -0400 Subject: [PATCH 06/10] Add PaymentContext for async payments This context is stored in the blinded payment paths we put in static invoices and is useful to authenticate payments over these paths to the recipient. We can't reuse Bolt12OfferContext for this because we don't have access to the invoice request fields at static invoice creation time. --- lightning/src/blinded_path/payment.rs | 22 ++++++++++++++++++++++ lightning/src/events/mod.rs | 22 ++++++++++++++-------- lightning/src/ln/channelmanager.rs | 9 +++++++-- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index e3a81927146..cf5af7d784e 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -349,6 +349,11 @@ pub enum PaymentContext { /// [`Offer`]: crate::offers::offer::Offer Bolt12Offer(Bolt12OfferContext), + /// The payment was made for a static invoice requested from a BOLT 12 [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + AsyncBolt12Offer(AsyncBolt12OfferContext), + /// The payment was made for an invoice sent for a BOLT 12 [`Refund`]. /// /// [`Refund`]: crate::offers::refund::Refund @@ -378,6 +383,18 @@ pub struct Bolt12OfferContext { pub invoice_request: InvoiceRequestFields, } +/// The context of a payment made for a static invoice requested from a BOLT 12 [`Offer`]. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AsyncBolt12OfferContext { + /// The [`Nonce`] used to verify that an inbound [`InvoiceRequest`] corresponds to this static + /// invoice's offer. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + pub offer_nonce: Nonce, +} + /// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`]. /// /// [`Refund`]: crate::offers::refund::Refund @@ -627,6 +644,7 @@ impl_writeable_tlv_based_enum_legacy!(PaymentContext, // 0 for Unknown removed in version 0.1. (1, Bolt12Offer), (2, Bolt12Refund), + (3, AsyncBolt12Offer), ); impl<'a> Writeable for PaymentContextRef<'a> { @@ -651,6 +669,10 @@ impl_writeable_tlv_based!(Bolt12OfferContext, { (2, invoice_request, required), }); +impl_writeable_tlv_based!(AsyncBolt12OfferContext, { + (0, offer_nonce, required), +}); + impl_writeable_tlv_based!(Bolt12RefundContext, {}); #[cfg(test)] diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 5bc446f9724..0a2a2093cb7 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -181,27 +181,32 @@ impl PaymentPurpose { pub(crate) fn from_parts( payment_preimage: Option, payment_secret: PaymentSecret, payment_context: Option, - ) -> Self { + ) -> Result { match payment_context { None => { - PaymentPurpose::Bolt11InvoicePayment { + Ok(PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, - } + }) }, Some(PaymentContext::Bolt12Offer(context)) => { - PaymentPurpose::Bolt12OfferPayment { + Ok(PaymentPurpose::Bolt12OfferPayment { payment_preimage, payment_secret, payment_context: context, - } + }) }, Some(PaymentContext::Bolt12Refund(context)) => { - PaymentPurpose::Bolt12RefundPayment { + Ok(PaymentPurpose::Bolt12RefundPayment { payment_preimage, payment_secret, payment_context: context, - } + }) + }, + Some(PaymentContext::AsyncBolt12Offer(_context)) => { + // This code will change to return Self::Bolt12OfferPayment when we add support for async + // receive. + Err(()) }, } } @@ -1865,7 +1870,8 @@ impl MaybeReadable for Event { (13, payment_id, option), }); let purpose = match payment_secret { - Some(secret) => PaymentPurpose::from_parts(payment_preimage, secret, payment_context), + Some(secret) => PaymentPurpose::from_parts(payment_preimage, secret, payment_context) + .map_err(|()| msgs::DecodeError::InvalidValue)?, None if payment_preimage.is_some() => PaymentPurpose::SpontaneousPayment(payment_preimage.unwrap()), None => return Err(msgs::DecodeError::InvalidValue), }; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fac1689ec8f..58b458cbb4a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -6253,11 +6253,16 @@ where match claimable_htlc.onion_payload { OnionPayload::Invoice { .. } => { let payment_data = payment_data.unwrap(); - let purpose = events::PaymentPurpose::from_parts( + let purpose = match events::PaymentPurpose::from_parts( payment_preimage, payment_data.payment_secret, payment_context, - ); + ) { + Ok(purpose) => purpose, + Err(()) => { + fail_htlc!(claimable_htlc, payment_hash); + }, + }; check_total_value!(purpose); }, OnionPayload::Spontaneous(preimage) => { From 96db8aa3d2247ba587f35f85e19f0b8d9bec9b78 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 30 Oct 2024 14:20:50 -0400 Subject: [PATCH 07/10] Add onion message AsyncPaymentsContext for inbound payments This context is included in static invoice's blinded message paths, provided back to us in HeldHtlcAvailable onion messages for blinded path authentication. In future work, we will check if this context is valid and respond with a ReleaseHeldHtlc message to release the upstream payment if so. We also add creation methods for the hmac used for authenticating said blinded path. --- lightning/src/blinded_path/message.rs | 22 ++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 7 ++++++- lightning/src/offers/signer.rs | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 4d96434dd63..51494e1b21e 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -402,6 +402,24 @@ pub enum AsyncPaymentsContext { /// containing the expected [`PaymentId`]. hmac: Hmac, }, + /// Context contained within the [`BlindedMessagePath`]s we put in static invoices, provided back + /// to us in corresponding [`HeldHtlcAvailable`] messages. + /// + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + InboundPayment { + /// A nonce used for authenticating that a [`HeldHtlcAvailable`] message is valid for a + /// preceding static invoice. + /// + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + nonce: Nonce, + /// Authentication code for the [`HeldHtlcAvailable`] message. + /// + /// Prevents nodes from creating their own blinded path to us, sending a [`HeldHtlcAvailable`] + /// message and trivially getting notified whenever we come online. + /// + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + hmac: Hmac, + }, } impl_writeable_tlv_based_enum!(MessageContext, @@ -433,6 +451,10 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (2, nonce, required), (4, hmac, required), }, + (1, InboundPayment) => { + (0, nonce, required), + (2, hmac, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 58b458cbb4a..b2b0f4e6d59 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12209,7 +12209,12 @@ where fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { #[cfg(async_payments)] { - let AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } = _context; + let (payment_id, nonce, hmac) = match _context { + AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } => { + (payment_id, nonce, hmac) + }, + _ => return + }; 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!( diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index fa9fdfa3467..7deff734b34 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -50,6 +50,11 @@ const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[7; 16]; // HMAC input for `ReceiveTlvs`. The HMAC is used in `blinded_path::payment::PaymentContext`. const PAYMENT_TLVS_HMAC_INPUT: &[u8; 16] = &[8; 16]; +// HMAC input used in `AsyncPaymentsContext::InboundPayment` to authenticate inbound +// held_htlc_available onion messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -483,3 +488,16 @@ pub(crate) fn verify_payment_tlvs( ) -> Result<(), ()> { if hmac_for_payment_tlvs(receive_tlvs, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } } + +#[cfg(async_payments)] +pub(crate) fn hmac_for_held_htlc_available_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Held HTLC OM"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT); + + Hmac::from_engine(hmac) +} From 9898e67db5a12e08c1d31756e33ff5ea1069e8e8 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 30 Oct 2024 16:09:25 -0400 Subject: [PATCH 08/10] Add utils to create static invoices and their corresponding offers We can't use our regular offer creation util for receiving async payments because the recipient can't be relied on to be online to service invoice_requests. Therefore, add a new offer creation util that is parameterized by blinded message paths to another node on the network that *is* always-online and can serve static invoices on behalf of the often-offline recipient. Also add a utility for creating static invoices corresponding to these offers. See new utils' docs and BOLTs PR 1149 for more info. --- lightning/src/ln/channelmanager.rs | 87 +++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b2b0f4e6d59..755198a8e52 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -72,8 +72,6 @@ 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::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions}; @@ -88,6 +86,11 @@ use crate::util::ser::{BigSize, FixedLengthReader, Readable, ReadableArgs, Maybe use crate::util::ser::TransactionU16LenLimited; use crate::util::logger::{Level, Logger, WithContext}; use crate::util::errors::APIError; +#[cfg(async_payments)] use { + crate::blinded_path::payment::AsyncBolt12OfferContext, + crate::offers::offer::Amount, + crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder}, +}; #[cfg(feature = "dnssec")] use crate::blinded_path::message::DNSResolverContext; @@ -9988,6 +9991,86 @@ where #[cfg(c_bindings)] create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder); + /// Create an offer for receiving async payments as an often-offline recipient. + /// + /// Because we may be offline when the payer attempts to request an invoice, you MUST: + /// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will + /// serve the [`StaticInvoice`] created from this offer on our behalf. + /// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this + /// [`Offer`] plus the returned [`Nonce`], and provide the static invoice to the + /// aforementioned always-online node. + #[cfg(async_payments)] + pub fn create_async_receive_offer_builder( + &self, message_paths_to_always_online_node: Vec + ) -> Result<(OfferBuilder, Nonce), Bolt12SemanticError> { + if message_paths_to_always_online_node.is_empty() { + return Err(Bolt12SemanticError::MissingPaths) + } + + let node_id = self.get_our_node_id(); + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let secp_ctx = &self.secp_ctx; + + let nonce = Nonce::from_entropy_source(entropy); + let mut builder = OfferBuilder::deriving_signing_pubkey( + node_id, expanded_key, nonce, secp_ctx + ).chain_hash(self.chain_hash); + + for path in message_paths_to_always_online_node { + builder = builder.path(path); + } + + Ok((builder.into(), nonce)) + } + + /// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were + /// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the + /// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`]. + #[cfg(async_payments)] + pub fn create_static_invoice_builder<'a>( + &self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option + ) -> Result, Bolt12SemanticError> { + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let secp_ctx = &self.secp_ctx; + + let payment_context = PaymentContext::AsyncBolt12Offer( + AsyncBolt12OfferContext { offer_nonce } + ); + let amount_msat = offer.amount().and_then(|amount| { + match amount { + Amount::Bitcoin { amount_msats } => Some(amount_msats), + Amount::Currency { .. } => None + } + }); + + let relative_expiry = relative_expiry.unwrap_or(STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY); + let relative_expiry_secs: u32 = relative_expiry.as_secs().try_into().unwrap_or(u32::MAX); + + let created_at = self.duration_since_epoch(); + let payment_secret = inbound_payment::create_for_spontaneous_payment( + &self.inbound_payment_key, amount_msat, relative_expiry_secs, created_at.as_secs(), None + ).map_err(|()| Bolt12SemanticError::InvalidAmount)?; + + let payment_paths = self.create_blinded_payment_paths( + amount_msat, payment_secret, payment_context, relative_expiry_secs + ).map_err(|()| Bolt12SemanticError::MissingPaths)?; + + let nonce = Nonce::from_entropy_source(entropy); + let hmac = signer::hmac_for_held_htlc_available_context(nonce, expanded_key); + let context = MessageContext::AsyncPayments( + AsyncPaymentsContext::InboundPayment { nonce, hmac } + ); + let async_receive_message_paths = self.create_blinded_paths(context) + .map_err(|()| Bolt12SemanticError::MissingPaths)?; + + StaticInvoiceBuilder::for_offer_using_derived_keys( + offer, payment_paths, async_receive_message_paths, created_at, expanded_key, + offer_nonce, secp_ctx + ).map(|inv| inv.allow_mpp().relative_expiry(relative_expiry_secs)) + } + /// Pays for an [`Offer`] using the given parameters by creating an [`InvoiceRequest`] and /// enqueuing it to be sent via an onion message. [`ChannelManager`] will pay the actual /// [`Bolt12Invoice`] once it is received. From 7d4af6a170c8f84de853ae246aa0df9d6418d84a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 13 Nov 2024 20:25:42 -0500 Subject: [PATCH 09/10] Test failures on paying static invoices Since adding support for creating static invoices from ChannelManager, it's easier to test these failure cases that went untested when we added support for paying static invoices. --- lightning/src/ln/async_payments_tests.rs | 359 +++++++++++++++++++++++ lightning/src/ln/mod.rs | 3 + lightning/src/ln/offers_tests.rs | 2 +- 3 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 lightning/src/ln/async_payments_tests.rs diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs new file mode 100644 index 00000000000..5444ccca969 --- /dev/null +++ b/lightning/src/ln/async_payments_tests.rs @@ -0,0 +1,359 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::blinded_path::message::{MessageContext, OffersContext}; +use crate::events::{Event, MessageSendEventsProvider, PaymentFailureReason}; +use crate::ln::channelmanager::PaymentId; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs::OnionMessageHandler; +use crate::ln::offers_tests; +use crate::ln::outbound_payment::Retry; +use crate::offers::nonce::Nonce; +use crate::onion_message::async_payments::{ + AsyncPaymentsMessage, AsyncPaymentsMessageHandler, ReleaseHeldHtlc, +}; +use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; +use crate::onion_message::offers::OffersMessage; +use crate::onion_message::packet::ParsedOnionMessageContents; +use crate::prelude::*; +use crate::types::features::Bolt12InvoiceFeatures; +use bitcoin::secp256k1::Secp256k1; + +use core::convert::Infallible; +use core::time::Duration; + +#[test] +#[cfg(async_payments)] +fn static_invoice_unknown_required_features() { + // Test that we will fail to pay a static invoice with unsupported required features. + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + let blinded_paths_to_always_online_node = nodes[1] + .message_router + .create_blinded_paths( + nodes[1].node.get_our_node_id(), + MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + Vec::new(), + &secp_ctx, + ) + .unwrap(); + let (offer_builder, nonce) = nodes[2] + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node) + .unwrap(); + let offer = offer_builder.build().unwrap(); + let static_invoice_unknown_req_features = nodes[2] + .node + .create_static_invoice_builder(&offer, nonce, None) + .unwrap() + .features_unchecked(Bolt12InvoiceFeatures::unknown()) + .build_and_sign(&secp_ctx) + .unwrap(); + + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) + .unwrap(); + + // Don't forward the invreq since we don't support retrieving the static invoice from the + // recipient's LSP yet, instead manually construct the response. + let invreq_om = nodes[0] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + static_invoice_unknown_req_features, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path), + }, + ) + .unwrap(); + + let static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &static_invoice_om); + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::PaymentFailed { payment_hash, payment_id: ev_payment_id, reason } => { + assert_eq!(payment_hash, None); + assert_eq!(payment_id, ev_payment_id); + assert_eq!(reason, Some(PaymentFailureReason::UnknownRequiredFeatures)); + }, + _ => panic!(), + } +} + +#[test] +fn ignore_unexpected_static_invoice() { + // Test that we'll ignore unexpected static invoices, invoices that don't match our invoice + // request, and duplicate invoices. + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + // Initiate payment to the sender's intended offer. + let blinded_paths_to_always_online_node = nodes[1] + .message_router + .create_blinded_paths( + nodes[1].node.get_our_node_id(), + MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + Vec::new(), + &secp_ctx, + ) + .unwrap(); + let (offer_builder, offer_nonce) = nodes[2] + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node.clone()) + .unwrap(); + let offer = offer_builder.build().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) + .unwrap(); + + // Don't forward the invreq since we don't support retrieving the static invoice from the + // recipient's LSP yet, instead manually construct the responses below. + let invreq_om = nodes[0] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; + + // Create a static invoice to be sent over the reply path containing the original payment_id, but + // the static invoice corresponds to a different offer than was originally paid. + let unexpected_static_invoice = { + let (offer_builder, nonce) = nodes[2] + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node) + .unwrap(); + let sender_unintended_offer = offer_builder.build().unwrap(); + + nodes[2] + .node + .create_static_invoice_builder(&sender_unintended_offer, nonce, None) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap() + }; + + // Check that we'll ignore the unexpected static invoice. + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + unexpected_static_invoice, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path.clone()), + }, + ) + .unwrap(); + let unexpected_static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &unexpected_static_invoice_om); + let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(async_pmts_msgs.is_empty()); + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + + // A valid static invoice corresponding to the correct offer will succeed and cause us to send a + // held_htlc_available onion message. + let valid_static_invoice = nodes[2] + .node + .create_static_invoice_builder(&offer, offer_nonce, None) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + valid_static_invoice.clone(), + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path.clone()), + }, + ) + .unwrap(); + let static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &static_invoice_om); + let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(!async_pmts_msgs.is_empty()); + assert!(async_pmts_msgs + .into_iter() + .all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_)))); + + // Receiving a duplicate invoice will have no effect. + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + valid_static_invoice, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path), + }, + ) + .unwrap(); + let dup_static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &dup_static_invoice_om); + let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(async_pmts_msgs.is_empty()); +} + +#[test] +fn pays_static_invoice() { + // Test that we support the async payments flow up to and including sending the actual payment. + // Async receive is not yet supported so we don't complete the payment yet. + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + let blinded_paths_to_always_online_node = nodes[1] + .message_router + .create_blinded_paths( + nodes[1].node.get_our_node_id(), + MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + Vec::new(), + &secp_ctx, + ) + .unwrap(); + let (offer_builder, offer_nonce) = nodes[2] + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node) + .unwrap(); + let offer = offer_builder.build().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + let relative_expiry = Duration::from_secs(1000); + let static_invoice = nodes[2] + .node + .create_static_invoice_builder(&offer, offer_nonce, Some(relative_expiry)) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + assert!(static_invoice.invoice_features().supports_basic_mpp()); + assert_eq!(static_invoice.relative_expiry(), relative_expiry); + + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) + .unwrap(); + + // Don't forward the invreq since we don't support retrieving the static invoice from the + // recipient's LSP yet, instead manually construct the response. + let invreq_om = nodes[0] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; + + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + static_invoice, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path), + }, + ) + .unwrap(); + let static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &static_invoice_om); + let mut async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(!async_pmts_msgs.is_empty()); + assert!(async_pmts_msgs + .iter() + .all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_)))); + + // Manually send the message and context releasing the HTLC since the recipient doesn't support + // responding themselves yet. + let held_htlc_avail_reply_path = match async_pmts_msgs.pop().unwrap().1 { + MessageSendInstructions::WithSpecifiedReplyPath { reply_path, .. } => reply_path, + _ => panic!(), + }; + nodes[2] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::AsyncPayments( + AsyncPaymentsMessage::ReleaseHeldHtlc(ReleaseHeldHtlc {}), + ), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(held_htlc_avail_reply_path), + }, + ) + .unwrap(); + + let release_held_htlc_om = nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + + // Check that we've queued the HTLCs of the async keysend payment. + let htlc_updates = get_htlc_update_msgs!(nodes[0], nodes[1].node.get_our_node_id()); + assert_eq!(htlc_updates.update_add_htlcs.len(), 1); + check_added_monitors!(nodes[0], 1); + + // Receiving a duplicate release_htlc message doesn't result in duplicate payment. + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); +} diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index e1631a2892c..c1bdc554c00 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -55,6 +55,9 @@ pub use onion_utils::create_payment_onion; #[cfg(test)] #[allow(unused_mut)] mod blinded_payment_tests; +#[cfg(all(test, async_payments))] +#[allow(unused_mut)] +mod async_payments_tests; #[cfg(test)] #[allow(unused_mut)] mod functional_tests; diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 6455a60b139..2cbf9e56647 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -199,7 +199,7 @@ fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessa } } -fn extract_invoice_request<'a, 'b, 'c>( +pub(super) fn extract_invoice_request<'a, 'b, 'c>( node: &Node<'a, 'b, 'c>, message: &OnionMessage ) -> (InvoiceRequest, BlindedMessagePath) { match node.onion_messenger.peel_onion_message(message) { From d3a7efa4ce121eeacd1324c99250f5f2d2b7377a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 3 Jan 2025 17:55:41 -0500 Subject: [PATCH 10/10] Move blinded keysend tests into new async_payments_tests.rs Blinded keysend is only planned to be supported in the async payments context. --- lightning/src/ln/async_payments_tests.rs | 248 +++++++++++++++++++++- lightning/src/ln/blinded_payment_tests.rs | 149 +------------ 2 files changed, 246 insertions(+), 151 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 5444ccca969..424d76da6c2 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -8,11 +8,16 @@ // licenses. use crate::blinded_path::message::{MessageContext, OffersContext}; -use crate::events::{Event, MessageSendEventsProvider, PaymentFailureReason}; -use crate::ln::channelmanager::PaymentId; +use crate::events::{Event, HTLCDestination, MessageSendEventsProvider, PaymentFailureReason}; +use crate::ln::blinded_payment_tests::{blinded_payment_path, get_blinded_route_parameters}; +use crate::ln::channelmanager; +use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; use crate::ln::functional_test_utils::*; +use crate::ln::inbound_payment; +use crate::ln::msgs::ChannelMessageHandler; use crate::ln::msgs::OnionMessageHandler; use crate::ln::offers_tests; +use crate::ln::onion_utils::INVALID_ONION_BLINDING; use crate::ln::outbound_payment::Retry; use crate::offers::nonce::Nonce; use crate::onion_message::async_payments::{ @@ -22,14 +27,251 @@ use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendIns use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::prelude::*; +use crate::routing::router::{PaymentParameters, RouteParameters}; +use crate::sign::NodeSigner; use crate::types::features::Bolt12InvoiceFeatures; +use crate::types::payment::{PaymentPreimage, PaymentSecret}; +use crate::util::config::UserConfig; use bitcoin::secp256k1::Secp256k1; use core::convert::Infallible; use core::time::Duration; #[test] -#[cfg(async_payments)] +fn blinded_keysend() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let chan_upd_1_2 = + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.contents; + + let inbound_payment_key = nodes[2].keys_manager.get_inbound_payment_key(); + let payment_secret = inbound_payment::create_for_spontaneous_payment( + &inbound_payment_key, + None, + u32::MAX, + nodes[2].node.duration_since_epoch().as_secs(), + None, + ) + .unwrap(); + + let amt_msat = 5000; + let keysend_preimage = PaymentPreimage([42; 32]); + let route_params = get_blinded_route_parameters( + amt_msat, + payment_secret, + 1, + 1_0000_0000, + nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), + &[&chan_upd_1_2], + &chanmon_cfgs[2].keys_manager, + ); + + let payment_hash = nodes[0] + .node + .send_spontaneous_payment( + Some(keysend_preimage), + RecipientOnionFields::spontaneous_empty(), + PaymentId(keysend_preimage.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + pass_along_path( + &nodes[0], + expected_route[0], + amt_msat, + payment_hash, + Some(payment_secret), + ev.clone(), + true, + Some(keysend_preimage), + ); + claim_payment_along_route(ClaimAlongRouteArgs::new( + &nodes[0], + expected_route, + keysend_preimage, + )); +} + +#[test] +fn blinded_mpp_keysend() { + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes(&nodes, 0, 1); + create_announced_chan_between_nodes(&nodes, 0, 2); + let chan_1_3 = create_announced_chan_between_nodes(&nodes, 1, 3); + let chan_2_3 = create_announced_chan_between_nodes(&nodes, 2, 3); + + let inbound_payment_key = nodes[3].keys_manager.get_inbound_payment_key(); + let payment_secret = inbound_payment::create_for_spontaneous_payment( + &inbound_payment_key, + None, + u32::MAX, + nodes[3].node.duration_since_epoch().as_secs(), + None, + ) + .unwrap(); + + let amt_msat = 15_000_000; + let keysend_preimage = PaymentPreimage([42; 32]); + let route_params = { + let pay_params = PaymentParameters::blinded(vec![ + blinded_payment_path( + payment_secret, + 1, + 1_0000_0000, + vec![nodes[1].node.get_our_node_id(), nodes[3].node.get_our_node_id()], + &[&chan_1_3.0.contents], + &chanmon_cfgs[3].keys_manager, + ), + blinded_payment_path( + payment_secret, + 1, + 1_0000_0000, + vec![nodes[2].node.get_our_node_id(), nodes[3].node.get_our_node_id()], + &[&chan_2_3.0.contents], + &chanmon_cfgs[3].keys_manager, + ), + ]) + .with_bolt12_features(channelmanager::provided_bolt12_invoice_features( + &UserConfig::default(), + )) + .unwrap(); + RouteParameters::from_payment_params_and_value(pay_params, amt_msat) + }; + + let payment_hash = nodes[0] + .node + .send_spontaneous_payment( + Some(keysend_preimage), + RecipientOnionFields::spontaneous_empty(), + PaymentId(keysend_preimage.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors!(nodes[0], 2); + + let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + pass_along_path( + &nodes[0], + expected_route[0], + amt_msat, + payment_hash.clone(), + Some(payment_secret), + ev.clone(), + false, + Some(keysend_preimage), + ); + + let ev = remove_first_msg_event_to_node(&nodes[2].node.get_our_node_id(), &mut events); + pass_along_path( + &nodes[0], + expected_route[1], + amt_msat, + payment_hash.clone(), + Some(payment_secret), + ev.clone(), + true, + Some(keysend_preimage), + ); + claim_payment_along_route(ClaimAlongRouteArgs::new( + &nodes[0], + expected_route, + keysend_preimage, + )); +} + +#[test] +fn invalid_keysend_payment_secret() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let chan_upd_1_2 = + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.contents; + + let invalid_payment_secret = PaymentSecret([42; 32]); + let amt_msat = 5000; + let keysend_preimage = PaymentPreimage([42; 32]); + let route_params = get_blinded_route_parameters( + amt_msat, + invalid_payment_secret, + 1, + 1_0000_0000, + nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), + &[&chan_upd_1_2], + &chanmon_cfgs[2].keys_manager, + ); + + let payment_hash = nodes[0] + .node + .send_spontaneous_payment( + Some(keysend_preimage), + RecipientOnionFields::spontaneous_empty(), + PaymentId(keysend_preimage.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let args = + PassAlongPathArgs::new(&nodes[0], &expected_route[0], amt_msat, payment_hash, ev.clone()) + .with_payment_secret(invalid_payment_secret) + .with_payment_preimage(keysend_preimage) + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + do_pass_along_path(args); + + let updates_2_1 = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id()); + assert_eq!(updates_2_1.update_fail_malformed_htlcs.len(), 1); + let update_malformed = &updates_2_1.update_fail_malformed_htlcs[0]; + assert_eq!(update_malformed.sha256_of_onion, [0; 32]); + assert_eq!(update_malformed.failure_code, INVALID_ONION_BLINDING); + nodes[1] + .node + .handle_update_fail_malformed_htlc(nodes[2].node.get_our_node_id(), update_malformed); + do_commitment_signed_dance(&nodes[1], &nodes[2], &updates_2_1.commitment_signed, true, false); + + let updates_1_0 = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); + assert_eq!(updates_1_0.update_fail_htlcs.len(), 1); + nodes[0].node.handle_update_fail_htlc( + nodes[1].node.get_our_node_id(), + &updates_1_0.update_fail_htlcs[0], + ); + do_commitment_signed_dance(&nodes[0], &nodes[1], &updates_1_0.commitment_signed, false, false); + expect_payment_failed_conditions( + &nodes[0], + payment_hash, + false, + PaymentFailedConditions::new().expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32]), + ); +} + +#[test] fn static_invoice_unknown_required_features() { // Test that we will fail to pay a static invoice with unsupported required features. let secp_ctx = Secp256k1::new(); diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index c1fad65c14f..96e6e76ac2c 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -36,12 +36,8 @@ use crate::util::config::UserConfig; use crate::util::ser::WithoutLength; use crate::util::test_utils; use lightning_invoice::RawBolt11Invoice; -#[cfg(async_payments)] use { - crate::ln::inbound_payment, - crate::types::payment::PaymentPreimage, -}; -fn blinded_payment_path( +pub fn blinded_payment_path( payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64, node_ids: Vec, channel_upds: &[&msgs::UnsignedChannelUpdate], keys_manager: &test_utils::TestKeysInterface @@ -1226,149 +1222,6 @@ fn conditionally_round_fwd_amt() { expect_payment_sent(&nodes[0], payment_preimage, Some(Some(expected_fee)), true, true); } -#[test] -#[cfg(async_payments)] -fn blinded_keysend() { - let chanmon_cfgs = create_chanmon_cfgs(3); - let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let chan_upd_1_2 = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.contents; - - let inbound_payment_key = nodes[2].keys_manager.get_inbound_payment_key(); - let payment_secret = inbound_payment::create_for_spontaneous_payment( - &inbound_payment_key, None, u32::MAX, nodes[2].node.duration_since_epoch().as_secs(), None - ).unwrap(); - - let amt_msat = 5000; - let keysend_preimage = PaymentPreimage([42; 32]); - let route_params = get_blinded_route_parameters(amt_msat, payment_secret, 1, - 1_0000_0000, - nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), - &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); - - let payment_hash = nodes[0].node.send_spontaneous_payment(Some(keysend_preimage), RecipientOnionFields::spontaneous_empty(), PaymentId(keysend_preimage.0), route_params, Retry::Attempts(0)).unwrap(); - check_added_monitors(&nodes[0], 1); - - let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - - let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); - pass_along_path(&nodes[0], expected_route[0], amt_msat, payment_hash, Some(payment_secret), ev.clone(), true, Some(keysend_preimage)); - claim_payment_along_route( - ClaimAlongRouteArgs::new(&nodes[0], expected_route, keysend_preimage) - ); -} - -#[test] -#[cfg(async_payments)] -fn blinded_mpp_keysend() { - let chanmon_cfgs = create_chanmon_cfgs(4); - let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); - let nodes = create_network(4, &node_cfgs, &node_chanmgrs); - - create_announced_chan_between_nodes(&nodes, 0, 1); - create_announced_chan_between_nodes(&nodes, 0, 2); - let chan_1_3 = create_announced_chan_between_nodes(&nodes, 1, 3); - let chan_2_3 = create_announced_chan_between_nodes(&nodes, 2, 3); - - let inbound_payment_key = nodes[3].keys_manager.get_inbound_payment_key(); - let payment_secret = inbound_payment::create_for_spontaneous_payment( - &inbound_payment_key, None, u32::MAX, nodes[3].node.duration_since_epoch().as_secs(), None - ).unwrap(); - - let amt_msat = 15_000_000; - let keysend_preimage = PaymentPreimage([42; 32]); - let route_params = { - let pay_params = PaymentParameters::blinded( - vec![ - blinded_payment_path(payment_secret, 1, 1_0000_0000, - vec![nodes[1].node.get_our_node_id(), nodes[3].node.get_our_node_id()], &[&chan_1_3.0.contents], - &chanmon_cfgs[3].keys_manager - ), - blinded_payment_path(payment_secret, 1, 1_0000_0000, - vec![nodes[2].node.get_our_node_id(), nodes[3].node.get_our_node_id()], &[&chan_2_3.0.contents], - &chanmon_cfgs[3].keys_manager - ), - ] - ) - .with_bolt12_features(channelmanager::provided_bolt12_invoice_features(&UserConfig::default())) - .unwrap(); - RouteParameters::from_payment_params_and_value(pay_params, amt_msat) - }; - - let payment_hash = nodes[0].node.send_spontaneous_payment(Some(keysend_preimage), RecipientOnionFields::spontaneous_empty(), PaymentId(keysend_preimage.0), route_params, Retry::Attempts(0)).unwrap(); - check_added_monitors!(nodes[0], 2); - - let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 2); - - let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); - pass_along_path(&nodes[0], expected_route[0], amt_msat, payment_hash.clone(), - Some(payment_secret), ev.clone(), false, Some(keysend_preimage)); - - let ev = remove_first_msg_event_to_node(&nodes[2].node.get_our_node_id(), &mut events); - pass_along_path(&nodes[0], expected_route[1], amt_msat, payment_hash.clone(), - Some(payment_secret), ev.clone(), true, Some(keysend_preimage)); - claim_payment_along_route( - ClaimAlongRouteArgs::new(&nodes[0], expected_route, keysend_preimage) - ); -} - -#[test] -#[cfg(async_payments)] -fn invalid_keysend_payment_secret() { - let chanmon_cfgs = create_chanmon_cfgs(3); - let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let chan_upd_1_2 = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.contents; - - let invalid_payment_secret = PaymentSecret([42; 32]); - let amt_msat = 5000; - let keysend_preimage = PaymentPreimage([42; 32]); - let route_params = get_blinded_route_parameters( - amt_msat, invalid_payment_secret, 1, 1_0000_0000, - nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], - &chanmon_cfgs[2].keys_manager - ); - - let payment_hash = nodes[0].node.send_spontaneous_payment(Some(keysend_preimage), RecipientOnionFields::spontaneous_empty(), PaymentId(keysend_preimage.0), route_params, Retry::Attempts(0)).unwrap(); - check_added_monitors(&nodes[0], 1); - - let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - - let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); - let args = PassAlongPathArgs::new( - &nodes[0], &expected_route[0], amt_msat, payment_hash, ev.clone() - ) - .with_payment_secret(invalid_payment_secret) - .with_payment_preimage(keysend_preimage) - .expect_failure(HTLCDestination::FailedPayment { payment_hash }); - do_pass_along_path(args); - - let updates_2_1 = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id()); - assert_eq!(updates_2_1.update_fail_malformed_htlcs.len(), 1); - let update_malformed = &updates_2_1.update_fail_malformed_htlcs[0]; - assert_eq!(update_malformed.sha256_of_onion, [0; 32]); - assert_eq!(update_malformed.failure_code, INVALID_ONION_BLINDING); - nodes[1].node.handle_update_fail_malformed_htlc(nodes[2].node.get_our_node_id(), update_malformed); - do_commitment_signed_dance(&nodes[1], &nodes[2], &updates_2_1.commitment_signed, true, false); - - let updates_1_0 = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); - assert_eq!(updates_1_0.update_fail_htlcs.len(), 1); - nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &updates_1_0.update_fail_htlcs[0]); - do_commitment_signed_dance(&nodes[0], &nodes[1], &updates_1_0.commitment_signed, false, false); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, - PaymentFailedConditions::new().expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32])); -} #[test] fn custom_tlvs_to_blinded_path() {