From 065e3001a306df6e277e0cadc4c51b5d6f328c1a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 27 Jan 2025 13:08:06 -0500 Subject: [PATCH 1/7] Remove trailing whitespace in events/mod.rs My text editor removes trailing whitespace automatically so let's make the removal official. --- lightning/src/events/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 07a97759f3b..201508e2811 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1190,12 +1190,12 @@ pub enum Event { /// events generated or serialized by versions prior to 0.0.122. next_user_channel_id: Option, /// The node id of the previous node. - /// + /// /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by /// versions prior to 0.1 prev_node_id: Option, /// The node id of the next node. - /// + /// /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by /// versions prior to 0.1 next_node_id: Option, From ffef1c38814fb5b136465311aeafd8da63d11468 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 7 Jan 2025 20:06:15 -0500 Subject: [PATCH 2/7] Support expiring async payments blinded message paths Store an absolute expiry in blinded message contexts for inbound async payments. Without this, anyone with the path corresponding to this context is able to trivially ask if we're online forever. --- lightning/src/blinded_path/message.rs | 5 +++++ lightning/src/ln/channelmanager.rs | 5 ++++- lightning/src/ln/inbound_payment.rs | 16 ++++++++++------ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index a47ea8242f0..441a2c2a625 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -434,6 +434,10 @@ pub enum AsyncPaymentsContext { /// /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable hmac: Hmac, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. Without this, anyone with the path corresponding to this context is + /// able to trivially ask if we're online forever. + path_absolute_expiry: core::time::Duration, }, } @@ -469,6 +473,7 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (1, InboundPayment) => { (0, nonce, required), (2, hmac, required), + (4, path_absolute_expiry, required), }, ); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index eac2a1474d7..0135235d8ad 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -9958,8 +9958,11 @@ where let nonce = Nonce::from_entropy_source(entropy); let hmac = signer::hmac_for_held_htlc_available_context(nonce, expanded_key); + let path_absolute_expiry = Duration::from_secs( + inbound_payment::calculate_absolute_expiry(created_at.as_secs(), relative_expiry_secs) + ); let context = MessageContext::AsyncPayments( - AsyncPaymentsContext::InboundPayment { nonce, hmac } + AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } ); let async_receive_message_paths = self.create_blinded_paths(context) .map_err(|()| Bolt12SemanticError::MissingPaths)?; diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 72f877978fe..87781793d06 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -213,6 +213,15 @@ pub(super) fn create_for_spontaneous_payment( Ok(construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key)) } +pub(super) fn calculate_absolute_expiry(highest_seen_timestamp: u64, invoice_expiry_delta_secs: u32) -> u64 { + // We assume that highest_seen_timestamp is pretty close to the current time - it's updated when + // we receive a new block with the maximum time we've seen in a header. It should never be more + // than two hours in the future. Thus, we add two hours here as a buffer to ensure we + // absolutely never fail a payment too early. + // Note that we assume that received blocks have reasonably up-to-date timestamps. + highest_seen_timestamp + invoice_expiry_delta_secs as u64 + 7200 +} + fn construct_metadata_bytes(min_value_msat: Option, payment_type: Method, invoice_expiry_delta_secs: u32, highest_seen_timestamp: u64, min_final_cltv_expiry_delta: Option) -> Result<[u8; METADATA_LEN], ()> { if min_value_msat.is_some() && min_value_msat.unwrap() > MAX_VALUE_MSAT { @@ -225,12 +234,7 @@ fn construct_metadata_bytes(min_value_msat: Option, payment_type: Method, }; min_amt_msat_bytes[0] |= (payment_type as u8) << METHOD_TYPE_OFFSET; - // We assume that highest_seen_timestamp is pretty close to the current time - it's updated when - // we receive a new block with the maximum time we've seen in a header. It should never be more - // than two hours in the future. Thus, we add two hours here as a buffer to ensure we - // absolutely never fail a payment too early. - // Note that we assume that received blocks have reasonably up-to-date timestamps. - let expiry_timestamp = highest_seen_timestamp + invoice_expiry_delta_secs as u64 + 7200; + let expiry_timestamp = calculate_absolute_expiry(highest_seen_timestamp, invoice_expiry_delta_secs); let mut expiry_bytes = expiry_timestamp.to_be_bytes(); // `min_value_msat` should fit in (64 bits - 3 payment type bits =) 61 bits as an unsigned integer. From 6448a0d70c2ca592c600d7653a7cd4cc82876514 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 7 Jan 2025 20:08:50 -0500 Subject: [PATCH 3/7] Release async payment HTLCs held upstream via OM If we receive a message that an HTLC is being held upstream for us, send a reply onion message back releasing it since we are online to receive the corresponding payment. --- lightning/src/ln/channelmanager.rs | 15 ++++++++++++++- lightning/src/offers/signer.rs | 7 +++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 0135235d8ad..c72c089a8ce 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12188,7 +12188,20 @@ where &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, _responder: Option ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { - None + #[cfg(async_payments)] { + match _context { + AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } => { + if let Err(()) = signer::verify_held_htlc_available_context( + nonce, hmac, &self.inbound_payment_key + ) { return None } + if self.duration_since_epoch() > path_absolute_expiry { return None } + }, + _ => return None + } + return _responder.map(|responder| (ReleaseHeldHtlc {}, responder.respond())) + } + #[cfg(not(async_payments))] + return None } fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 7deff734b34..93ff4ecea3b 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -501,3 +501,10 @@ pub(crate) fn hmac_for_held_htlc_available_context( Hmac::from_engine(hmac) } + +#[cfg(async_payments)] +pub(crate) fn verify_held_htlc_available_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_held_htlc_available_context(nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } +} From 82ad53c84aa9cc255992847bc1df11b08c88e335 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 30 Jul 2024 16:53:52 -0700 Subject: [PATCH 4/7] Bubble up invreq from htlc onion to forwarding flow As part of receiving an async payment, we need to verify the sender's original invoice request. Therefore, add support for parsing the invreq contained in the onion and storing it in PendingHTLCForwards to prepare for when we add this verification in an upcoming commit. The invreq also needs to be bubbled up for inclusion in the PaymentClaimable event's PaymentPurpose. --- lightning/src/ln/channelmanager.rs | 11 +++++++---- lightning/src/ln/msgs.rs | 11 ++++++++--- lightning/src/ln/onion_payment.rs | 10 ++++++---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c72c089a8ce..91a826cb40b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -240,6 +240,8 @@ pub enum PendingHTLCRouting { /// [`PaymentSecret`] and should verify it using our /// [`NodeSigner::get_inbound_payment_key`]. has_recipient_created_payment_secret: bool, + /// The [`InvoiceRequest`] associated with the [`Offer`] corresponding to this payment. + invoice_request: Option, /// The context of the payment included by the recipient in a blinded path, or `None` if a /// blinded path was not used. /// @@ -6045,7 +6047,7 @@ where let blinded_failure = routing.blinded_failure(); let ( cltv_expiry, onion_payload, payment_data, payment_context, phantom_shared_secret, - mut onion_fields, has_recipient_created_payment_secret + mut onion_fields, has_recipient_created_payment_secret, _invoice_request_opt ) = match routing { PendingHTLCRouting::Receive { payment_data, payment_metadata, payment_context, @@ -6057,12 +6059,12 @@ where payment_metadata, custom_tlvs }; (incoming_cltv_expiry, OnionPayload::Invoice { _legacy_hop_data }, Some(payment_data), payment_context, phantom_shared_secret, onion_fields, - true) + true, None) }, PendingHTLCRouting::ReceiveKeysend { payment_data, payment_preimage, payment_metadata, incoming_cltv_expiry, custom_tlvs, requires_blinded_error: _, - has_recipient_created_payment_secret, payment_context, + has_recipient_created_payment_secret, payment_context, invoice_request, } => { let onion_fields = RecipientOnionFields { payment_secret: payment_data.as_ref().map(|data| data.payment_secret), @@ -6071,7 +6073,7 @@ where }; (incoming_cltv_expiry, OnionPayload::Spontaneous(payment_preimage), payment_data, payment_context, None, onion_fields, - has_recipient_created_payment_secret) + has_recipient_created_payment_secret, invoice_request) }, _ => { panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive"); @@ -12411,6 +12413,7 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting, (5, custom_tlvs, optional_vec), (7, has_recipient_created_payment_secret, (default_value, false)), (9, payment_context, option), + (11, invoice_request, option), }, ); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 659ec65f6cf..ae89c2d6d50 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -37,6 +37,7 @@ use crate::ln::types::ChannelId; use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret}; use crate::types::features::{ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures}; use crate::ln::onion_utils; +use crate::offers::invoice_request::InvoiceRequest; use crate::onion_message; use crate::sign::{NodeSigner, Recipient}; @@ -1791,6 +1792,7 @@ mod fuzzy_internal_msgs { payment_context: PaymentContext, intro_node_blinding_point: Option, keysend_preimage: Option, + invoice_request: Option, custom_tlvs: Vec<(u64, Vec)>, } } @@ -2852,6 +2854,7 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh let mut payment_metadata: Option>> = None; let mut total_msat = None; let mut keysend_preimage: Option = None; + let mut invoice_request: Option = None; let mut custom_tlvs = Vec::new(); let tlv_len = BigSize::read(r)?; @@ -2865,6 +2868,7 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh (12, intro_node_blinding_point, option), (16, payment_metadata, option), (18, total_msat, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), + (77_777, invoice_request, option), // See https://github.com/lightning/blips/blob/master/blip-0003.md (5482373484, keysend_preimage, option) }, |msg_type: u64, msg_reader: &mut FixedLengthReader<_>| -> Result { @@ -2895,7 +2899,7 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh short_channel_id, payment_relay, payment_constraints, features, next_blinding_override })} => { if amt.is_some() || cltv_value.is_some() || total_msat.is_some() || - keysend_preimage.is_some() + keysend_preimage.is_some() || invoice_request.is_some() { return Err(DecodeError::InvalidValue) } @@ -2928,13 +2932,14 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh payment_context, intro_node_blinding_point, keysend_preimage, + invoice_request, custom_tlvs, }) }, } } else if let Some(short_channel_id) = short_id { if payment_data.is_some() || payment_metadata.is_some() || encrypted_tlvs_opt.is_some() || - total_msat.is_some() + total_msat.is_some() || invoice_request.is_some() { return Err(DecodeError::InvalidValue) } Ok(Self::Forward { short_channel_id, @@ -2942,7 +2947,7 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh outgoing_cltv_value: cltv_value.ok_or(DecodeError::InvalidValue)?, }) } else { - if encrypted_tlvs_opt.is_some() || total_msat.is_some() { + if encrypted_tlvs_opt.is_some() || total_msat.is_some() || invoice_request.is_some() { return Err(DecodeError::InvalidValue) } if let Some(data) = &payment_data { diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 254274fd16f..cedbe7c7c73 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -136,18 +136,19 @@ pub(super) fn create_recv_pending_htlc_info( ) -> Result { let ( payment_data, keysend_preimage, custom_tlvs, onion_amt_msat, onion_cltv_expiry, - payment_metadata, payment_context, requires_blinded_error, has_recipient_created_payment_secret + payment_metadata, payment_context, requires_blinded_error, has_recipient_created_payment_secret, + invoice_request ) = match hop_data { msgs::InboundOnionPayload::Receive { payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, cltv_expiry_height, payment_metadata, .. } => (payment_data, keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, - cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none()), + cltv_expiry_height, payment_metadata, None, false, keysend_preimage.is_none(), None), msgs::InboundOnionPayload::BlindedReceive { sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret, intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage, - custom_tlvs + custom_tlvs, invoice_request } => { check_blinded_payment_constraints( sender_intended_htlc_amt_msat, cltv_expiry, &payment_constraints @@ -162,7 +163,7 @@ pub(super) fn create_recv_pending_htlc_info( let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat }; (Some(payment_data), keysend_preimage, custom_tlvs, sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context), - intro_node_blinding_point.is_none(), true) + intro_node_blinding_point.is_none(), true, invoice_request) } msgs::InboundOnionPayload::Forward { .. } => { return Err(InboundHTLCErr { @@ -237,6 +238,7 @@ pub(super) fn create_recv_pending_htlc_info( requires_blinded_error, has_recipient_created_payment_secret, payment_context, + invoice_request, } } else if let Some(data) = payment_data { PendingHTLCRouting::Receive { From f5d65d46743c94859f876e4e53b773a609cead50 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 2 Aug 2024 17:19:54 -0700 Subject: [PATCH 5/7] Support receiving async payment HTLCs After a lot of setup in prior commits, here we finally finish support for receiving HTLCs paid to static BOLT 12 invoices. It amounts to verifying the invoice request contained within the onion and generating the right PaymentPurpose for the claimable event. --- lightning/src/events/mod.rs | 12 ++++--- lightning/src/ln/channelmanager.rs | 57 +++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 201508e2811..bbaaca4e8b3 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -178,9 +178,10 @@ impl PaymentPurpose { } } + /// Errors when provided an `AsyncBolt12OfferContext`, see below. pub(crate) fn from_parts( payment_preimage: Option, payment_secret: PaymentSecret, - payment_context: Option, + payment_context: Option ) -> Result { match payment_context { None => { @@ -203,11 +204,12 @@ impl PaymentPurpose { payment_context: context, }) }, - Some(PaymentContext::AsyncBolt12Offer(_context)) => { - // This code will change to return Self::Bolt12OfferPayment when we add support for async - // receive. + Some(PaymentContext::AsyncBolt12Offer(_)) => { + // Callers are expected to convert from `AsyncBolt12OfferContext` to `Bolt12OfferContext` + // using the invoice request provided in the payment onion prior to calling this method. + debug_assert!(false); Err(()) - }, + } } } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 91a826cb40b..917bb6fc0c8 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -36,7 +36,7 @@ use crate::events::FundingInfo; 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, UnauthenticatedReceiveTlvs}; +use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentPath, Bolt12OfferContext, Bolt12RefundContext, PaymentConstraints, PaymentContext, UnauthenticatedReceiveTlvs}; use crate::chain; use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock}; use crate::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator}; @@ -87,7 +87,6 @@ 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}, }; @@ -6047,7 +6046,7 @@ where let blinded_failure = routing.blinded_failure(); let ( cltv_expiry, onion_payload, payment_data, payment_context, phantom_shared_secret, - mut onion_fields, has_recipient_created_payment_secret, _invoice_request_opt + mut onion_fields, has_recipient_created_payment_secret, invoice_request_opt ) = match routing { PendingHTLCRouting::Receive { payment_data, payment_metadata, payment_context, @@ -6271,14 +6270,54 @@ where }; check_total_value!(purpose); }, - OnionPayload::Spontaneous(preimage) => { - if payment_context.is_some() { - if !matches!(payment_context, Some(PaymentContext::AsyncBolt12Offer(_))) { - log_trace!(self.logger, "Failing new HTLC with payment_hash {}: received a keysend payment to a non-async payments context {:#?}", payment_hash, payment_context); + OnionPayload::Spontaneous(keysend_preimage) => { + let purpose = if let Some(PaymentContext::AsyncBolt12Offer( + AsyncBolt12OfferContext { offer_nonce } + )) = payment_context { + let payment_data = match payment_data { + Some(data) => data, + None => { + debug_assert!(false, "We checked that payment_data is Some above"); + fail_htlc!(claimable_htlc, payment_hash); + }, + }; + + let verified_invreq = match invoice_request_opt + .and_then(|invreq| invreq.verify_using_recipient_data( + offer_nonce, &self.inbound_payment_key, &self.secp_ctx + ).ok()) + { + Some(verified_invreq) => { + if let Some(invreq_amt_msat) = verified_invreq.amount_msats() { + if payment_data.total_msat < invreq_amt_msat { + fail_htlc!(claimable_htlc, payment_hash); + } + } + verified_invreq + }, + None => { + fail_htlc!(claimable_htlc, payment_hash); + } + }; + let payment_purpose_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: verified_invreq.offer_id, + invoice_request: verified_invreq.fields(), + }); + match events::PaymentPurpose::from_parts( + Some(keysend_preimage), payment_data.payment_secret, + Some(payment_purpose_context), + ) { + Ok(purpose) => purpose, + Err(()) => { + fail_htlc!(claimable_htlc, payment_hash); + } } + } else if payment_context.is_some() { + log_trace!(self.logger, "Failing new HTLC with payment_hash {}: received a keysend payment to a non-async payments context {:#?}", payment_hash, payment_context); fail_htlc!(claimable_htlc, payment_hash); - } - let purpose = events::PaymentPurpose::SpontaneousPayment(preimage); + } else { + events::PaymentPurpose::SpontaneousPayment(keysend_preimage) + }; check_total_value!(purpose); } } From 8d1d136be6e68833c547817915a92cd75e8d0177 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 7 Jan 2025 21:22:05 -0500 Subject: [PATCH 6/7] Test utils: fix config overrides for private channels Previously, this test util did not account for config overrides supplied at node creation time. Uncovered because it caused test nodes to be unable to forward over private channels created with this util because that is not the default. --- lightning/src/ln/functional_test_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index f1361337b35..5b4f370658d 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -1509,7 +1509,7 @@ pub fn create_announced_chan_between_nodes_with_value<'a, 'b, 'c: 'd, 'd>(nodes: } pub fn create_unannounced_chan_between_nodes_with_value<'a, 'b, 'c, 'd>(nodes: &'a Vec>, a: usize, b: usize, channel_value: u64, push_msat: u64) -> (msgs::ChannelReady, Transaction) { - let mut no_announce_cfg = test_default_channel_config(); + let mut no_announce_cfg = nodes[a].node.get_current_default_configuration().clone(); no_announce_cfg.channel_handshake_config.announce_for_forwarding = false; nodes[a].node.create_channel(nodes[b].node.get_our_node_id(), channel_value, push_msat, 42, None, Some(no_announce_cfg)).unwrap(); let open_channel = get_event_msg!(nodes[a], MessageSendEvent::SendOpenChannel, nodes[b].node.get_our_node_id()); From 1f53a311e2a55d18c34a246d513242677029db76 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 8 Jan 2025 22:13:03 -0500 Subject: [PATCH 7/7] Test async receive In the previous commit we completed support for async receive from an always-online sender to an often-offline receiver, minus support for acting as the async receiver's always-online channel counterparty. --- lightning/src/blinded_path/payment.rs | 36 +- lightning/src/ln/async_payments_tests.rs | 729 ++++++++++++++++++++-- lightning/src/ln/blinded_payment_tests.rs | 12 +- lightning/src/ln/channelmanager.rs | 21 + lightning/src/ln/functional_test_utils.rs | 27 +- 5 files changed, 739 insertions(+), 86 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 7302ab5ed1d..df7c29909f8 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -11,6 +11,7 @@ use bitcoin::hashes::hmac::Hmac; use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; use crate::blinded_path::utils; @@ -193,17 +194,11 @@ impl BlindedPaymentPath { NL::Target: NodeIdLookUp, T: secp256k1::Signing + secp256k1::Verification, { - let control_tlvs_ss = - node_signer.ecdh(Recipient::Node, &self.inner_path.blinding_point, None)?; - let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes()); - let encrypted_control_tlvs = - &self.inner_path.blinded_hops.get(0).ok_or(())?.encrypted_payload; - let mut s = Cursor::new(encrypted_control_tlvs); - let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64); - match ChaChaPolyReadAdapter::read(&mut reader, rho) { - Ok(ChaChaPolyReadAdapter { - readable: BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), - }) => { + match self.decrypt_intro_payload::(node_signer) { + Ok(( + BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), + control_tlvs_ss, + )) => { let next_node_id = match node_id_lookup.next_node_id(short_channel_id) { Some(node_id) => node_id, None => return Err(()), @@ -223,6 +218,25 @@ impl BlindedPaymentPath { } } + pub(crate) fn decrypt_intro_payload( + &self, node_signer: &NS, + ) -> Result<(BlindedPaymentTlvs, SharedSecret), ()> + where + NS::Target: NodeSigner, + { + let control_tlvs_ss = + node_signer.ecdh(Recipient::Node, &self.inner_path.blinding_point, None)?; + let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes()); + let encrypted_control_tlvs = + &self.inner_path.blinded_hops.get(0).ok_or(())?.encrypted_payload; + let mut s = Cursor::new(encrypted_control_tlvs); + let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64); + match ChaChaPolyReadAdapter::read(&mut reader, rho) { + Ok(ChaChaPolyReadAdapter { readable, .. }) => Ok((readable, control_tlvs_ss)), + _ => Err(()), + } + } + pub(crate) fn inner_blinded_path(&self) -> &BlindedPath { &self.inner_path } diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index a62f7b5936a..8d18938272c 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -8,33 +8,100 @@ // licenses. use crate::blinded_path::message::{MessageContext, OffersContext}; -use crate::events::{Event, HTLCDestination, MessageSendEventsProvider, PaymentFailureReason}; -use crate::ln::blinded_payment_tests::get_blinded_route_parameters; +use crate::blinded_path::payment::PaymentContext; +use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentTlvs}; +use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS}; +use crate::events::{ + Event, HTLCDestination, MessageSendEvent, MessageSendEventsProvider, PaymentFailureReason, +}; +use crate::ln::blinded_payment_tests::{fail_blinded_htlc_backwards, get_blinded_route_parameters}; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; use crate::ln::functional_test_utils::*; +use crate::ln::msgs; 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::PendingOutboundPayment; use crate::ln::outbound_payment::Retry; +use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::offer::Offer; use crate::offers::static_invoice::StaticInvoice; -use crate::onion_message::async_payments::{ - AsyncPaymentsMessage, AsyncPaymentsMessageHandler, ReleaseHeldHtlc, -}; +use crate::onion_message::async_payments::{AsyncPaymentsMessage, AsyncPaymentsMessageHandler}; 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::routing::router::{Payee, PaymentParameters}; +use crate::sign::NodeSigner; +use crate::sync::Mutex; use crate::types::features::Bolt12InvoiceFeatures; -use crate::types::payment::{PaymentPreimage, PaymentSecret}; +use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use bitcoin::constants::ChainHash; +use bitcoin::network::Network; use bitcoin::secp256k1; use bitcoin::secp256k1::Secp256k1; use core::convert::Infallible; use core::time::Duration; +// Goes through the async receive onion message flow, returning the final release_held_htlc OM. +// +// Assumes the held_htlc_available message will be sent: +// sender -> always_online_recipient_counterparty -> recipient. +// +// Returns: (held_htlc_available_om, release_held_htlc_om) +fn pass_async_payments_oms( + static_invoice: StaticInvoice, sender: &Node, always_online_recipient_counterparty: &Node, + recipient: &Node, +) -> (msgs::OnionMessage, msgs::OnionMessage) { + let sender_node_id = sender.node.get_our_node_id(); + let always_online_node_id = always_online_recipient_counterparty.node.get_our_node_id(); + + // 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 = + sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap(); + let invreq_reply_path = + offers_tests::extract_invoice_request(always_online_recipient_counterparty, &invreq_om).1; + + always_online_recipient_counterparty + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + static_invoice, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path), + }, + ) + .unwrap(); + let static_invoice_om = always_online_recipient_counterparty + .onion_messenger + .next_onion_message_for_peer(sender_node_id) + .unwrap(); + sender.onion_messenger.handle_onion_message(always_online_node_id, &static_invoice_om); + + let held_htlc_available_om_0_1 = + sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap(); + always_online_recipient_counterparty + .onion_messenger + .handle_onion_message(sender_node_id, &held_htlc_available_om_0_1); + let held_htlc_available_om_1_2 = always_online_recipient_counterparty + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient + .onion_messenger + .handle_onion_message(always_online_node_id, &held_htlc_available_om_1_2); + + ( + held_htlc_available_om_1_2, + recipient.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(), + ) +} + fn create_static_invoice( always_online_counterparty: &Node, recipient: &Node, relative_expiry: Option, secp_ctx: &Secp256k1, @@ -319,17 +386,25 @@ fn ignore_unexpected_static_invoice() { } #[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. +fn async_receive_flow_success() { + // Test that an always-online sender can successfully pay an async receiver. 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 mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), 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); + // Set the random bytes so we can predict the payment preimage and hash. + let hardcoded_random_bytes = [42; 32]; + let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); + let payment_hash: PaymentHash = keysend_preimage.into(); + *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + let relative_expiry = Duration::from_secs(1000); let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx); @@ -342,68 +417,16 @@ fn pays_static_invoice() { .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(); + let release_held_htlc_om = + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]).1; 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); + 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); check_added_monitors!(nodes[0], 1); // Receiving a duplicate release_htlc message doesn't result in duplicate payment. @@ -411,6 +434,12 @@ fn pays_static_invoice() { .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()); + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_payment_preimage(keysend_preimage); + do_pass_along_path(args); + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); } #[cfg_attr(feature = "std", ignore)] @@ -483,3 +512,569 @@ fn expired_static_invoice_fail() { // The sender doesn't reply with InvoiceError right now because the always-online node doesn't // currently provide them with a reply path to do so. } + +#[test] +fn async_receive_mpp() { + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = create_node_chanmgrs( + 4, + &node_cfgs, + &[None, Some(allow_priv_chan_fwds_cfg.clone()), Some(allow_priv_chan_fwds_cfg), None], + ); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + + // Create this network topology: + // n1 + // / \ + // n0 n3 + // \ / + // n2 + create_announced_chan_between_nodes(&nodes, 0, 1); + create_announced_chan_between_nodes(&nodes, 0, 2); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); + + // In other tests we hardcode the sender's random bytes so we can predict the keysend preimage to + // check later in the test, but that doesn't work for MPP because it causes the session_privs for + // the different MPP parts to not be unique. + let amt_msat = 15_000_000; + let payment_id = PaymentId([1; 32]); + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), None) + .unwrap(); + let release_held_htlc_om_3_0 = + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3]).1; + nodes[0] + .onion_messenger + .handle_onion_message(nodes[3].node.get_our_node_id(), &release_held_htlc_om_3_0); + 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); + let payment_hash = match ev { + MessageSendEvent::UpdateHTLCs { ref updates, .. } => { + updates.update_add_htlcs[0].payment_hash + }, + _ => panic!(), + }; + + let args = PassAlongPathArgs::new(&nodes[0], expected_route[0], amt_msat, payment_hash, ev) + .without_claimable_event(); + do_pass_along_path(args); + + let ev = remove_first_msg_event_to_node(&nodes[2].node.get_our_node_id(), &mut events); + let args = PassAlongPathArgs::new(&nodes[0], expected_route[1], amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + let keysend_preimage = match claimable_ev { + crate::events::Event::PaymentClaimable { + purpose: crate::events::PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. }, + .. + } => payment_preimage.unwrap(), + _ => panic!(), + }; + claim_payment_along_route(ClaimAlongRouteArgs::new( + &nodes[0], + expected_route, + keysend_preimage, + )); +} + +#[test] +fn amount_doesnt_match_invreq() { + // Ensure that we'll fail an async payment backwards if the amount in the HTLC is lower than the + // amount from the original invoice request. + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + // Make one blinded path's fees slightly higher so they are tried in a deterministic order. + let mut higher_fee_chan_cfg = allow_priv_chan_fwds_cfg.clone(); + higher_fee_chan_cfg.channel_config.forwarding_fee_base_msat += 5000; + let node_chanmgrs = create_node_chanmgrs( + 4, + &node_cfgs, + &[None, Some(allow_priv_chan_fwds_cfg), Some(higher_fee_chan_cfg), None], + ); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + + // Create this network topology so nodes[0] has a blinded route hint to retry over. + // n1 + // / \ + // n0 n3 + // \ / + // n2 + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + + let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); + + // Set the random bytes so we can predict the payment preimage and hash. + let hardcoded_random_bytes = [42; 32]; + let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); + let payment_hash: PaymentHash = keysend_preimage.into(); + *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + + 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(1), None) + .unwrap(); + let release_held_htlc_om_3_0 = + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3]).1; + + // Replace the invoice request contained within outbound_payments before sending so the invreq + // amount doesn't match the onion amount when the HTLC gets to the recipient. + let mut valid_invreq = None; + nodes[0].node.test_modify_pending_payment(&payment_id, |pmt| { + if let PendingOutboundPayment::StaticInvoiceReceived { invoice_request, .. } = pmt { + valid_invreq = Some(invoice_request.clone()); + *invoice_request = offer + .request_invoice( + &nodes[0].keys_manager.get_inbound_payment_key(), + Nonce::from_entropy_source(nodes[0].keys_manager), + &secp_ctx, + payment_id, + ) + .unwrap() + .amount_msats(amt_msat + 1) + .unwrap() + .chain_hash(ChainHash::using_genesis_block(Network::Testnet)) + .unwrap() + .build_and_sign() + .unwrap(); + } else { + panic!() + } + }); + + nodes[0] + .onion_messenger + .handle_onion_message(nodes[3].node.get_our_node_id(), &release_held_htlc_om_3_0); + check_added_monitors(&nodes[0], 1); + + // Check that we've queued the HTLCs of the async keysend payment. + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + assert!(matches!( + ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_payment_preimage(keysend_preimage) + .without_claimable_event() + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + do_pass_along_path(args); + + // Modify the invoice request stored in our outbounds to be the correct one, to make sure the + // payment retry will succeed after we finish failing the invalid HTLC back. + nodes[0].node.test_modify_pending_payment(&payment_id, |pmt| { + if let PendingOutboundPayment::Retryable { invoice_request, .. } = pmt { + *invoice_request = valid_invreq.take(); + } else { + panic!() + } + }); + + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[3]], true); + + // The retry with the correct invoice request should succeed. + nodes[0].node.process_pending_htlc_forwards(); + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let mut ev = remove_first_msg_event_to_node(&nodes[2].node.get_our_node_id(), &mut events); + assert!(matches!( + ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); + check_added_monitors!(nodes[0], 1); + let route: &[&[&Node]] = &[&[&nodes[2], &nodes[3]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_payment_preimage(keysend_preimage); + do_pass_along_path(args); + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); +} + +#[test] +fn reject_missing_invreq() { + // Ensure we'll fail an async payment backwards if the HTLC onion doesn't contain the sender's + // original invoice request. + let mut valid_invreq: Mutex> = Mutex::new(None); + + invalid_async_receive_with_retry( + |sender, _, payment_id| { + // Remove the invoice request from our Retryable payment so we don't include it in the onion on + // retry. + sender.node.test_modify_pending_payment(&payment_id, |pmt| { + if let PendingOutboundPayment::Retryable { invoice_request, .. } = pmt { + assert!(invoice_request.is_some()); + *valid_invreq.lock().unwrap() = invoice_request.take(); + } else { + panic!() + } + }); + }, + |sender, payment_id| { + // Re-add the invoice request so we include it in the onion on the next retry. + sender.node.test_modify_pending_payment(&payment_id, |pmt| { + if let PendingOutboundPayment::Retryable { invoice_request, .. } = pmt { + *invoice_request = valid_invreq.lock().unwrap().take(); + } else { + panic!() + } + }); + }, + ); +} + +#[test] +fn reject_bad_payment_secret() { + // Ensure we'll fail an async payment backwards if the payment secret in the onion is invalid. + + let mut valid_payment_params: Mutex> = Mutex::new(None); + invalid_async_receive_with_retry( + |sender, recipient, payment_id| { + // Store invalid payment paths in the sender's outbound Retryable payment to induce the failure + // on the recipient's end. Store multiple paths so the sender still thinks they can retry after + // the failure we're about to cause below. + let mut invalid_blinded_payment_paths = Vec::new(); + for i in 0..2 { + let mut paths = recipient + .node + .test_create_blinded_payment_paths( + None, + PaymentSecret([42; 32]), // invalid payment secret + PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { + // We don't reach the point of checking the invreq nonce due to the invalid payment secret + offer_nonce: Nonce([i; Nonce::LENGTH]), + }), + u32::MAX, + ) + .unwrap(); + invalid_blinded_payment_paths.append(&mut paths); + } + + // Modify the outbound payment parameters to use payment paths with an invalid payment secret. + sender.node.test_modify_pending_payment(&payment_id, |pmt| { + if let PendingOutboundPayment::Retryable { ref mut payment_params, .. } = pmt { + assert!(payment_params.is_some()); + let valid_params = payment_params.clone(); + if let Payee::Blinded { ref mut route_hints, .. } = + &mut payment_params.as_mut().unwrap().payee + { + core::mem::swap(route_hints, &mut invalid_blinded_payment_paths); + } else { + panic!() + } + *valid_payment_params.lock().unwrap() = valid_params; + } else { + panic!() + } + }); + }, + |sender, payment_id| { + // Re-add the valid payment params so we use the right payment secret on the next retry. + sender.node.test_modify_pending_payment(&payment_id, |pmt| { + if let PendingOutboundPayment::Retryable { payment_params, .. } = pmt { + *payment_params = valid_payment_params.lock().unwrap().take(); + } else { + panic!() + } + }); + }, + ); +} + +fn invalid_async_receive_with_retry( + mut modify_outbounds_for_failure: F1, mut modify_outbounds_for_success: F2, +) where + F1: FnMut(&Node, &Node, PaymentId), + F2: FnMut(&Node, PaymentId), +{ + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), 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]); + + // Hardcode the payment paths so nodes[0] has something to retry over. Set all of these paths to + // use the same nodes to avoid complicating the test with a bunch of extra nodes. + let mut static_invoice_paths = Vec::new(); + for _ in 0..3 { + let static_inv_for_path = nodes[2] + .node + .create_static_invoice_builder(&offer, offer_nonce, None) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + static_invoice_paths.push(static_inv_for_path.payment_paths()[0].clone()); + } + nodes[2].router.expect_blinded_payment_paths(static_invoice_paths); + + let static_invoice = nodes[2] + .node + .create_static_invoice_builder(&offer, offer_nonce, None) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + + // Set the random bytes so we can predict the payment preimage and hash. + let hardcoded_random_bytes = [42; 32]; + let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); + let payment_hash: PaymentHash = keysend_preimage.into(); + *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(2), None) + .unwrap(); + let release_held_htlc_om_2_0 = + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]).1; + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om_2_0); + check_added_monitors(&nodes[0], 1); + + // Check that we've queued the HTLCs of the async keysend payment. + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + assert!(matches!( + ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_payment_preimage(keysend_preimage); + do_pass_along_path(args); + + // Fail the HTLC backwards to enable us to more easily modify the now-Retryable outbound to test + // failures on the recipient's end. + nodes[2].node.fail_htlc_backwards(&payment_hash); + expect_pending_htlcs_forwardable_conditions( + nodes[2].node.get_and_clear_pending_events(), + &[HTLCDestination::FailedPayment { payment_hash }], + ); + nodes[2].node.process_pending_htlc_forwards(); + check_added_monitors!(nodes[2], 1); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2]], true); + + // Trigger a retry and make sure it fails after calling the closure that induces recipient + // failure. + modify_outbounds_for_failure(&nodes[0], &nodes[2], payment_id); + nodes[0].node.process_pending_htlc_forwards(); + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + assert!(matches!( + ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); + check_added_monitors!(nodes[0], 1); + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_payment_preimage(keysend_preimage) + .without_claimable_event() + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + do_pass_along_path(args); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2]], true); + + // The retry after calling the 2nd closure should succeed. + modify_outbounds_for_success(&nodes[0], payment_id); + nodes[0].node.process_pending_htlc_forwards(); + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + check_added_monitors!(nodes[0], 1); + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_payment_preimage(keysend_preimage); + do_pass_along_path(args); + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); +} + +#[cfg(not(feature = "std"))] +#[test] +fn expired_static_invoice_message_path() { + // Test that if we receive a held_htlc_available message over an expired blinded path, we'll + // ignore it. + 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); + + const INVOICE_EXPIRY_SECS: u32 = 10; + let (offer, static_invoice) = create_static_invoice( + &nodes[1], + &nodes[2], + Some(Duration::from_secs(INVOICE_EXPIRY_SECS as u64)), + &secp_ctx, + ); + + 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(1), None) + .unwrap(); + + // While the invoice is unexpired, respond with release_held_htlc. + let (held_htlc_available_om, _release_held_htlc_om) = + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]); + + // After the invoice is expired, ignore inbound held_htlc_available messages over the path. + let path_absolute_expiry = crate::ln::inbound_payment::calculate_absolute_expiry( + nodes[2].node.duration_since_epoch().as_secs(), + INVOICE_EXPIRY_SECS, + ); + let block = create_dummy_block( + nodes[2].best_block_hash(), + (path_absolute_expiry + 1) as u32, + Vec::new(), + ); + connect_block(&nodes[2], &block); + nodes[2] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &held_htlc_available_om); + for i in 0..2 { + assert!(nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[i].node.get_our_node_id()) + .is_none()); + } +} + +#[test] +fn expired_static_invoice_payment_path() { + // Test that we'll reject inbound payments to expired payment paths. + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), 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); + + // Make sure all nodes are at the same block height in preparation for CLTV timeout things. + let node_max_height = + nodes.iter().map(|node| node.blocks.lock().unwrap().len()).max().unwrap() as u32; + connect_blocks(&nodes[0], node_max_height - nodes[0].best_block_info().1); + connect_blocks(&nodes[1], node_max_height - nodes[1].best_block_info().1); + connect_blocks(&nodes[2], node_max_height - nodes[2].best_block_info().1); + + // Set the random bytes so we can predict the payment preimage and hash. + let hardcoded_random_bytes = [42; 32]; + let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); + let payment_hash: PaymentHash = keysend_preimage.into(); + *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + + // Hardcode the blinded payment path returned by the router so we can expire it via mining blocks. + let (_, static_invoice_expired_paths) = + create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); + nodes[2] + .router + .expect_blinded_payment_paths(static_invoice_expired_paths.payment_paths().to_vec()); + + // Extract the expiry height from the to-be-expired blinded payment path. + let final_max_cltv_expiry = { + let mut blinded_path = static_invoice_expired_paths.payment_paths().to_vec().pop().unwrap(); + blinded_path + .advance_path_by_one(&nodes[1].keys_manager, &nodes[1].node, &secp_ctx) + .unwrap(); + match blinded_path.decrypt_intro_payload(&nodes[2].keys_manager).unwrap().0 { + BlindedPaymentTlvs::Receive(tlvs) => tlvs.tlvs.payment_constraints.max_cltv_expiry, + _ => panic!(), + } + }; + + // Mine a bunch of blocks so the hardcoded path's `max_cltv_expiry` is expired at the recipient's + // end by the time the payment arrives. + let min_cltv_expiry_delta = test_default_channel_config().channel_config.cltv_expiry_delta; + connect_blocks( + &nodes[0], + final_max_cltv_expiry + - nodes[0].best_block_info().1 + - min_cltv_expiry_delta as u32 + - HTLC_FAIL_BACK_BUFFER + - LATENCY_GRACE_PERIOD_BLOCKS + - 1, + ); + connect_blocks( + &nodes[1], + final_max_cltv_expiry + - nodes[1].best_block_info().1 + // Don't expire the path for nodes[1] + - min_cltv_expiry_delta as u32 + - HTLC_FAIL_BACK_BUFFER + - LATENCY_GRACE_PERIOD_BLOCKS + - 1, + ); + connect_blocks(&nodes[2], final_max_cltv_expiry - nodes[2].best_block_info().1); + + let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); + 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(); + let release_held_htlc_om = + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]).1; + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + + 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); + check_added_monitors!(nodes[0], 1); + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_payment_preimage(keysend_preimage) + .without_claimable_event() + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + do_pass_along_path(args); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2]], false); + nodes[2].logger.assert_log_contains( + "lightning::ln::channelmanager", + "violated blinded payment constraints", + 1, + ); +} diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 9708dbd6d88..a760f9573f4 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -107,13 +107,17 @@ pub fn get_blinded_route_parameters( pub fn fail_blinded_htlc_backwards( payment_hash: PaymentHash, intro_node_idx: usize, nodes: &[&Node], + retry_expected: bool ) { for i in (0..nodes.len()).rev() { match i { 0 => { let mut payment_failed_conditions = PaymentFailedConditions::new() .expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32]); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + if retry_expected { + payment_failed_conditions = payment_failed_conditions.retry_expected(); + } + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); }, i if i <= intro_node_idx => { let unblinded_node_updates = get_htlc_update_msgs!(nodes[i], nodes[i-1].node.get_our_node_id()); @@ -604,7 +608,7 @@ fn do_forward_fail_in_process_pending_htlc_fwds(check: ProcessPendingHTLCsCheck, if intro_fails { cause_error!(nodes[0], nodes[1], nodes[2], chan_id_1_2, chan_upd_1_2.short_channel_id); check_added_monitors!(nodes[1], 1); - fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1]]); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1]], false); return } @@ -696,7 +700,7 @@ fn do_blinded_intercept_payment(intercept_node_fails: bool) { expect_pending_htlcs_forwardable_and_htlc_handling_failed_ignore!(nodes[1], vec![HTLCDestination::UnknownNextHop { requested_forward_scid: intercept_scid }]); nodes[1].node.process_pending_htlc_forwards(); check_added_monitors!(&nodes[1], 1); - fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1]]); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1]], false); return } @@ -802,7 +806,7 @@ fn three_hop_blinded_path_fail() { ); nodes[3].node.process_pending_htlc_forwards(); check_added_monitors!(nodes[3], 1); - fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2], &nodes[3]]); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2], &nodes[3]], false); } #[derive(PartialEq)] diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 917bb6fc0c8..a9d934056c5 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4693,6 +4693,17 @@ where self.pending_outbound_payments.test_add_new_pending_payment(payment_hash, recipient_onion, payment_id, route, None, &self.entropy_source, best_block_height) } + #[cfg(all(test, async_payments))] + pub(crate) fn test_modify_pending_payment( + &self, payment_id: &PaymentId, mut callback: Fn + ) where Fn: FnMut(&mut PendingOutboundPayment) { + let mut outbounds = self.pending_outbound_payments.pending_outbound_payments.lock().unwrap(); + match outbounds.get_mut(payment_id) { + Some(outb) => callback(outb), + _ => panic!() + } + } + #[cfg(test)] pub(crate) fn test_set_payment_metadata(&self, payment_id: PaymentId, new_payment_metadata: Option>) { self.pending_outbound_payments.test_set_payment_metadata(payment_id, new_payment_metadata); @@ -10553,6 +10564,16 @@ where ) } + #[cfg(all(test, async_payments))] + pub(super) fn test_create_blinded_payment_paths( + &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext, + relative_expiry_seconds: u32 + ) -> Result, ()> { + self.create_blinded_payment_paths( + amount_msats, payment_secret, payment_context, relative_expiry_seconds + ) + } + /// Gets a fake short channel id for use in receiving [phantom node payments]. These fake scids /// are used when constructing the phantom invoice's route hints. /// diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 5b4f370658d..f9f6440d6fd 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -2496,6 +2496,7 @@ pub struct PaymentFailedConditions<'a> { pub(crate) expected_blamed_scid: Option, pub(crate) expected_blamed_chan_closed: Option, pub(crate) expected_mpp_parts_remain: bool, + pub(crate) retry_expected: bool, } impl<'a> PaymentFailedConditions<'a> { @@ -2505,6 +2506,7 @@ impl<'a> PaymentFailedConditions<'a> { expected_blamed_scid: None, expected_blamed_chan_closed: None, expected_mpp_parts_remain: false, + retry_expected: false, } } pub fn mpp_parts_remain(mut self) -> Self { @@ -2523,6 +2525,10 @@ impl<'a> PaymentFailedConditions<'a> { self.expected_htlc_error_data = Some((code, data)); self } + pub fn retry_expected(mut self) -> Self { + self.retry_expected = true; + self + } } #[cfg(test)] @@ -2588,7 +2594,7 @@ pub fn expect_payment_failed_conditions_event<'a, 'b, 'c, 'd, 'e>( }, _ => panic!("Unexpected event"), }; - if !conditions.expected_mpp_parts_remain { + if !conditions.expected_mpp_parts_remain && !conditions.retry_expected { match &payment_failed_events[1] { Event::PaymentFailed { ref payment_hash, ref payment_id, ref reason } => { assert_eq!(*payment_hash, Some(expected_payment_hash), "unexpected second payment_hash"); @@ -2601,6 +2607,11 @@ pub fn expect_payment_failed_conditions_event<'a, 'b, 'c, 'd, 'e>( } _ => panic!("Unexpected second event"), } + } else if conditions.retry_expected { + match &payment_failed_events[1] { + Event::PendingHTLCsForwardable { .. } => {}, + _ => panic!("Unexpected second event"), + } } } @@ -2751,8 +2762,12 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option assert_eq!(Some(*payment_secret), onion_fields.as_ref().unwrap().payment_secret); }, PaymentPurpose::Bolt12OfferPayment { payment_preimage, payment_secret, .. } => { - assert_eq!(expected_preimage, *payment_preimage); - assert_eq!(our_payment_secret.unwrap(), *payment_secret); + if let Some(preimage) = expected_preimage { + assert_eq!(preimage, payment_preimage.unwrap()); + } + if let Some(secret) = our_payment_secret { + assert_eq!(secret, *payment_secret); + } assert_eq!(Some(*payment_secret), onion_fields.as_ref().unwrap().payment_secret); }, PaymentPurpose::Bolt12RefundPayment { payment_preimage, payment_secret, .. } => { @@ -2774,7 +2789,11 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option } event = Some(events_2[0].clone()); } else if let Some(ref failure) = expected_failure { - assert_eq!(events_2.len(), 2); + // If we successfully decode the HTLC onion but then fail later in + // process_pending_htlc_forwards, then we'll queue the failure and generate a new + // `ProcessPendingHTLCForwards` event. If we fail during the process of decoding the HTLC, + // we'll fail it immediately with no intermediate forwarding event. + assert!(events_2.len() == 1 || events_2.len() == 2); expect_htlc_handling_failed_destinations!(events_2, &[failure]); node.node.process_pending_htlc_forwards(); check_added_monitors!(node, 1);