From 7e085c5ce4a98e9ba8528810118d7b5c118eb45a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sat, 9 Mar 2024 18:58:01 -0600 Subject: [PATCH 01/17] Refactor handling of InvoiceRequest In order to generate and InvoiceSent event, it would be cleaner to have one location where a Bolt12Invoice is successfully generated. Refactor the handling code to this end and clean-up line length by making some of the type conversions more streamlined. --- lightning/src/ln/channelmanager.rs | 48 +++++++++++---------------- lightning/src/offers/invoice_error.rs | 14 ++++++++ 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 49ced4db335..ff3605b3900 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -61,7 +61,6 @@ use crate::ln::wire::Encode; use crate::offers::invoice::{BlindedPayInfo, Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{DerivedPayerId, InvoiceRequestBuilder}; -use crate::offers::merkle::SignError; use crate::offers::offer::{Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; @@ -10356,7 +10355,7 @@ where self.highest_seen_timestamp.load(Ordering::Acquire) as u64 ); - if invoice_request.keys.is_some() { + let response = if invoice_request.keys.is_some() { #[cfg(feature = "std")] let builder = invoice_request.respond_using_derived_keys( payment_paths, payment_hash @@ -10365,12 +10364,10 @@ where let builder = invoice_request.respond_using_derived_keys_no_std( payment_paths, payment_hash, created_at ); - let builder: Result, _> = - builder.map(|b| b.into()); - match builder.and_then(|b| b.allow_mpp().build_and_sign(secp_ctx)) { - Ok(invoice) => Some(OffersMessage::Invoice(invoice)), - Err(error) => Some(OffersMessage::InvoiceError(error.into())), - } + builder + .map(InvoiceBuilder::::from) + .and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx)) + .map_err(InvoiceError::from) } else { #[cfg(feature = "std")] let builder = invoice_request.respond_with(payment_paths, payment_hash); @@ -10378,29 +10375,24 @@ where let builder = invoice_request.respond_with_no_std( payment_paths, payment_hash, created_at ); - let builder: Result, _> = - builder.map(|b| b.into()); - let response = builder.and_then(|builder| builder.allow_mpp().build()) - .map_err(|e| OffersMessage::InvoiceError(e.into())) + builder + .map(InvoiceBuilder::::from) + .and_then(|builder| builder.allow_mpp().build()) + .map_err(InvoiceError::from) .and_then(|invoice| { #[cfg(c_bindings)] let mut invoice = invoice; - match invoice.sign(|invoice: &UnsignedBolt12Invoice| - self.node_signer.sign_bolt12_invoice(invoice) - ) { - Ok(invoice) => Ok(OffersMessage::Invoice(invoice)), - Err(SignError::Signing) => Err(OffersMessage::InvoiceError( - InvoiceError::from_string("Failed signing invoice".to_string()) - )), - Err(SignError::Verification(_)) => Err(OffersMessage::InvoiceError( - InvoiceError::from_string("Failed invoice signature verification".to_string()) - )), - } - }); - match response { - Ok(invoice) => Some(invoice), - Err(error) => Some(error), - } + invoice + .sign(|invoice: &UnsignedBolt12Invoice| + self.node_signer.sign_bolt12_invoice(invoice) + ) + .map_err(InvoiceError::from) + }) + }; + + match response { + Ok(invoice) => Some(OffersMessage::Invoice(invoice)), + Err(error) => Some(OffersMessage::InvoiceError(error.into())), } }, OffersMessage::Invoice(invoice) => { diff --git a/lightning/src/offers/invoice_error.rs b/lightning/src/offers/invoice_error.rs index 1b634525416..5ae5f457a84 100644 --- a/lightning/src/offers/invoice_error.rs +++ b/lightning/src/offers/invoice_error.rs @@ -11,6 +11,7 @@ use crate::io; use crate::ln::msgs::DecodeError; +use crate::offers::merkle::SignError; use crate::offers::parse::Bolt12SemanticError; use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; use crate::util::string::UntrustedString; @@ -113,6 +114,19 @@ impl From for InvoiceError { } } +impl From for InvoiceError { + fn from(error: SignError) -> Self { + let message = match error { + SignError::Signing => "Failed signing invoice", + SignError::Verification(_) => "Failed invoice signature verification", + }; + InvoiceError { + erroneous_field: None, + message: UntrustedString(message.to_string()), + } + } +} + #[cfg(test)] mod tests { use super::{ErroneousField, InvoiceError}; From e239c92cdad88f441367e31b115f3b1bbf5b9c8f Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 11 Mar 2024 17:03:26 -0500 Subject: [PATCH 02/17] Refactor handling of Bolt12Invoice In order to provide an InvoiceGenerated event, it would be cleaner to have one location where a Bolt12Invoice is successfully created. Refactor the handling code to this end and clean-up line length by making some of the type conversions more streamlined. --- lightning/src/ln/channelmanager.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ff3605b3900..21051523634 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10396,21 +10396,25 @@ where } }, OffersMessage::Invoice(invoice) => { - match invoice.verify(expanded_key, secp_ctx) { - Err(()) => { - Some(OffersMessage::InvoiceError(InvoiceError::from_string("Unrecognized invoice".to_owned()))) - }, - Ok(_) if invoice.invoice_features().requires_unknown_bits_from(&self.bolt12_invoice_features()) => { - Some(OffersMessage::InvoiceError(Bolt12SemanticError::UnknownRequiredFeatures.into())) - }, - Ok(payment_id) => { - if let Err(e) = self.send_payment_for_bolt12_invoice(&invoice, payment_id) { - log_trace!(self.logger, "Failed paying invoice: {:?}", e); - Some(OffersMessage::InvoiceError(InvoiceError::from_string(format!("{:?}", e)))) + let response = invoice + .verify(expanded_key, secp_ctx) + .map_err(|()| InvoiceError::from_string("Unrecognized invoice".to_owned())) + .and_then(|payment_id| { + let features = self.bolt12_invoice_features(); + if invoice.invoice_features().requires_unknown_bits_from(&features) { + Err(InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures)) } else { - None + self.send_payment_for_bolt12_invoice(&invoice, payment_id) + .map_err(|e| { + log_trace!(self.logger, "Failed paying invoice: {:?}", e); + InvoiceError::from_string(format!("{:?}", e)) + }) } - }, + }); + + match response { + Ok(()) => None, + Err(e) => Some(OffersMessage::InvoiceError(e)), } }, OffersMessage::InvoiceError(invoice_error) => { From 01814dcef28fd13e98223cb1fbc1d2d6abe3776d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 11 Mar 2024 18:31:50 -0500 Subject: [PATCH 03/17] Implement Eq and Hash for Bolt12Invoice Bolt12Invoice will be added to a new Event::InvoiceGenerated variant. These traits along with PartialEq are required to be implemented for any type used in an Event. --- lightning/src/offers/invoice.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index fbfcffe2ad8..a384ab08656 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -104,13 +104,13 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::hash_types::{WPubkeyHash, WScriptHash}; -use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; use bitcoin::address::{Address, Payload, WitnessProgram, WitnessVersion}; use bitcoin::key::TweakedPublicKey; use core::time::Duration; +use core::hash::{Hash, Hasher}; use crate::io; use crate::blinded_path::BlindedPath; use crate::ln::PaymentHash; @@ -390,6 +390,7 @@ macro_rules! invoice_builder_methods { ( /// Successive calls to this method will add another address. Caller is responsible for not /// adding duplicate addresses and only calling if capable of receiving to P2WSH addresses. pub fn fallback_v0_p2wsh($($self_mut)* $self: $self_type, script_hash: &WScriptHash) -> $return_type { + use bitcoin::hashes::Hash; let address = FallbackAddress { version: WitnessVersion::V0.to_num(), program: Vec::from(script_hash.to_byte_array()), @@ -403,6 +404,7 @@ macro_rules! invoice_builder_methods { ( /// Successive calls to this method will add another address. Caller is responsible for not /// adding duplicate addresses and only calling if capable of receiving to P2WPKH addresses. pub fn fallback_v0_p2wpkh($($self_mut)* $self: $self_type, pubkey_hash: &WPubkeyHash) -> $return_type { + use bitcoin::hashes::Hash; let address = FallbackAddress { version: WitnessVersion::V0.to_num(), program: Vec::from(pubkey_hash.to_byte_array()), @@ -614,7 +616,6 @@ impl AsRef for UnsignedBolt12Invoice { /// [`Refund`]: crate::offers::refund::Refund /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest #[derive(Clone, Debug)] -#[cfg_attr(test, derive(PartialEq))] pub struct Bolt12Invoice { bytes: Vec, contents: InvoiceContents, @@ -886,6 +887,20 @@ impl Bolt12Invoice { } } +impl PartialEq for Bolt12Invoice { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) + } +} + +impl Eq for Bolt12Invoice {} + +impl Hash for Bolt12Invoice { + fn hash(&self, state: &mut H) { + self.bytes.hash(state); + } +} + impl InvoiceContents { /// Whether the original offer or refund has expired. #[cfg(feature = "std")] From 12c3a24bebe67ba406609fdfe6747da9c1449756 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 11 Mar 2024 19:00:40 -0500 Subject: [PATCH 04/17] Return the invoice when requesting a refund When sending an invoice for a refund, information from the invoice may be useful for caller. For instance, the payment_hash can be used to track whether the refund was paid. Return the invoice to facilitate this use case. --- lightning/src/ln/channelmanager.rs | 24 ++++++++++++++++-------- lightning/src/ln/offers_tests.rs | 12 +++++++++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 21051523634..553ff853e7e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1694,15 +1694,20 @@ where /// # /// # fn example(channel_manager: T, refund: &Refund) { /// # let channel_manager = channel_manager.get_cm(); -/// match channel_manager.request_refund_payment(refund) { -/// Ok(()) => println!("Requesting payment for refund"), -/// Err(e) => println!("Unable to request payment for refund: {:?}", e), -/// } +/// let known_payment_hash = match channel_manager.request_refund_payment(refund) { +/// Ok(invoice) => { +/// let payment_hash = invoice.payment_hash(); +/// println!("Requesting refund payment {}", payment_hash); +/// payment_hash +/// }, +/// Err(e) => panic!("Unable to request payment for refund: {:?}", e), +/// }; /// /// // On the event processing thread /// channel_manager.process_pending_events(&|event| match event { /// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { /// PaymentPurpose::InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { +/// assert_eq!(payment_hash, known_payment_hash); /// println!("Claiming payment {}", payment_hash); /// channel_manager.claim_funds(payment_preimage); /// }, @@ -1713,6 +1718,7 @@ where /// # _ => {}, /// }, /// Event::PaymentClaimed { payment_hash, amount_msat, .. } => { +/// assert_eq!(payment_hash, known_payment_hash); /// println!("Claimed {} msats", amount_msat); /// }, /// // ... @@ -8774,7 +8780,7 @@ where /// /// The resulting invoice uses a [`PaymentHash`] recognized by the [`ChannelManager`] and a /// [`BlindedPath`] containing the [`PaymentSecret`] needed to reconstruct the corresponding - /// [`PaymentPreimage`]. + /// [`PaymentPreimage`]. It is returned purely for informational purposes. /// /// # Limitations /// @@ -8791,7 +8797,9 @@ where /// the invoice. /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - pub fn request_refund_payment(&self, refund: &Refund) -> Result<(), Bolt12SemanticError> { + pub fn request_refund_payment( + &self, refund: &Refund + ) -> Result { let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; let secp_ctx = &self.secp_ctx; @@ -8830,7 +8838,7 @@ where let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); if refund.paths().is_empty() { let message = new_pending_onion_message( - OffersMessage::Invoice(invoice), + OffersMessage::Invoice(invoice.clone()), Destination::Node(refund.payer_id()), Some(reply_path), ); @@ -8846,7 +8854,7 @@ where } } - Ok(()) + Ok(invoice) }, Err(()) => Err(Bolt12SemanticError::InvalidAmount), } diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 4c8c1ecba03..5b774ce4ee2 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -473,7 +473,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { } expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); - alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); connect_peers(alice, charlie); @@ -484,6 +484,8 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { david.onion_messenger.handle_onion_message(&charlie_id, &onion_message); let invoice = extract_invoice(david, &onion_message); + assert_eq!(invoice, expected_invoice); + assert_eq!(invoice.amount_msats(), 10_000_000); assert_ne!(invoice.signing_pubkey(), alice_id); assert!(!invoice.payment_paths().is_empty()); @@ -589,12 +591,14 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); - alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(&alice_id, &onion_message); let invoice = extract_invoice(bob, &onion_message); + assert_eq!(invoice, expected_invoice); + assert_eq!(invoice.amount_msats(), 10_000_000); assert_ne!(invoice.signing_pubkey(), alice_id); assert!(!invoice.payment_paths().is_empty()); @@ -681,12 +685,14 @@ fn pays_for_refund_without_blinded_paths() { assert!(refund.paths().is_empty()); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); - alice.node.request_refund_payment(&refund).unwrap(); + let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(&alice_id, &onion_message); let invoice = extract_invoice(bob, &onion_message); + assert_eq!(invoice, expected_invoice); + route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); From c8815384787e6b0837a51944555357623d4ffa4e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 26 Mar 2024 09:48:03 -0500 Subject: [PATCH 05/17] Add PaymentContext to payment::ReceiveTlvs Providing LDK-specific data in a BlindedPath allows for the data to be received with the corresponding payment. This is useful as it can then be surfaced in PaymentClaimable events where the user may correlated with an Offer, for instance. Define a PaymentContext enum for including various context (e.g., offer, refund, static invoice). --- lightning/src/blinded_path/payment.rs | 56 +++++++++++++++++++++-- lightning/src/ln/blinded_payment_tests.rs | 6 ++- lightning/src/ln/channelmanager.rs | 3 +- lightning/src/ln/msgs.rs | 2 +- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 3d56020a626..b722a441919 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -53,6 +53,8 @@ pub struct ReceiveTlvs { pub payment_secret: PaymentSecret, /// Constraints for the receiver of this payment. pub payment_constraints: PaymentConstraints, + /// Context for the receiver of this payment. + pub payment_context: PaymentContext, } /// Data to construct a [`BlindedHop`] for sending a payment over. @@ -97,6 +99,27 @@ pub struct PaymentConstraints { pub htlc_minimum_msat: u64, } +/// The context of an inbound payment, which is included in a [`BlindedPath`] via [`ReceiveTlvs`] +/// and surfaced in [`PaymentPurpose`]. +/// +/// [`BlindedPath`]: crate::blinded_path::BlindedPath +/// [`PaymentPurpose`]: crate::events::PaymentPurpose +#[derive(Clone, Debug)] +pub enum PaymentContext { + /// The payment context was unknown. + Unknown(UnknownPaymentContext), +} + +/// An unknown payment context. +#[derive(Clone, Debug)] +pub struct UnknownPaymentContext(()); + +impl PaymentContext { + pub(crate) fn unknown() -> Self { + PaymentContext::Unknown(UnknownPaymentContext(())) + } +} + impl TryFrom for PaymentRelay { type Error = (); @@ -137,7 +160,8 @@ impl Writeable for ReceiveTlvs { fn write(&self, w: &mut W) -> Result<(), io::Error> { encode_tlv_stream!(w, { (12, self.payment_constraints, required), - (65536, self.payment_secret, required) + (65536, self.payment_secret, required), + (65537, self.payment_context, required) }); Ok(()) } @@ -163,11 +187,14 @@ impl Readable for BlindedPaymentTlvs { (12, payment_constraints, required), (14, features, option), (65536, payment_secret, option), + (65537, payment_context, (default_value, PaymentContext::unknown())), }); let _padding: Option = _padding; if let Some(short_channel_id) = scid { - if payment_secret.is_some() { return Err(DecodeError::InvalidValue) } + if payment_secret.is_some() { + return Err(DecodeError::InvalidValue) + } Ok(BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?, @@ -179,6 +206,7 @@ impl Readable for BlindedPaymentTlvs { Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs { payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?, payment_constraints: payment_constraints.0.unwrap(), + payment_context: payment_context.0.unwrap(), })) } } @@ -309,10 +337,27 @@ impl Readable for PaymentConstraints { } } +impl_writeable_tlv_based_enum!(PaymentContext, + ; + (0, Unknown), +); + +impl Writeable for UnknownPaymentContext { + fn write(&self, _w: &mut W) -> Result<(), io::Error> { + Ok(()) + } +} + +impl Readable for UnknownPaymentContext { + fn read(_r: &mut R) -> Result { + Ok(UnknownPaymentContext(())) + } +} + #[cfg(test)] mod tests { use bitcoin::secp256k1::PublicKey; - use crate::blinded_path::payment::{ForwardNode, ForwardTlvs, ReceiveTlvs, PaymentConstraints, PaymentRelay}; + use crate::blinded_path::payment::{ForwardNode, ForwardTlvs, ReceiveTlvs, PaymentConstraints, PaymentContext, PaymentRelay}; use crate::ln::PaymentSecret; use crate::ln::features::BlindedHopFeatures; use crate::ln::functional_test_utils::TEST_FINAL_CLTV; @@ -361,6 +406,7 @@ mod tests { max_cltv_expiry: 0, htlc_minimum_msat: 1, }, + payment_context: PaymentContext::unknown(), }; let htlc_maximum_msat = 100_000; let blinded_payinfo = super::compute_payinfo(&intermediate_nodes[..], &recv_tlvs, htlc_maximum_msat, 12).unwrap(); @@ -379,6 +425,7 @@ mod tests { max_cltv_expiry: 0, htlc_minimum_msat: 1, }, + payment_context: PaymentContext::unknown(), }; let blinded_payinfo = super::compute_payinfo(&[], &recv_tlvs, 4242, TEST_FINAL_CLTV as u16).unwrap(); assert_eq!(blinded_payinfo.fee_base_msat, 0); @@ -432,6 +479,7 @@ mod tests { max_cltv_expiry: 0, htlc_minimum_msat: 3, }, + payment_context: PaymentContext::unknown(), }; let htlc_maximum_msat = 100_000; let blinded_payinfo = super::compute_payinfo(&intermediate_nodes[..], &recv_tlvs, htlc_maximum_msat, TEST_FINAL_CLTV as u16).unwrap(); @@ -482,6 +530,7 @@ mod tests { max_cltv_expiry: 0, htlc_minimum_msat: 1, }, + payment_context: PaymentContext::unknown(), }; let htlc_minimum_msat = 3798; assert!(super::compute_payinfo(&intermediate_nodes[..], &recv_tlvs, htlc_minimum_msat - 1, TEST_FINAL_CLTV as u16).is_err()); @@ -536,6 +585,7 @@ mod tests { max_cltv_expiry: 0, htlc_minimum_msat: 1, }, + payment_context: PaymentContext::unknown(), }; let blinded_payinfo = super::compute_payinfo(&intermediate_nodes[..], &recv_tlvs, 10_000, TEST_FINAL_CLTV as u16).unwrap(); diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index eb31be9eecf..a0438b2447a 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -9,7 +9,7 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use crate::blinded_path::BlindedPath; -use crate::blinded_path::payment::{ForwardNode, ForwardTlvs, PaymentConstraints, PaymentRelay, ReceiveTlvs}; +use crate::blinded_path::payment::{ForwardNode, ForwardTlvs, PaymentConstraints, PaymentContext, PaymentRelay, ReceiveTlvs}; use crate::events::{Event, HTLCDestination, MessageSendEvent, MessageSendEventsProvider, PaymentFailureReason}; use crate::ln::PaymentSecret; use crate::ln::channelmanager; @@ -63,6 +63,7 @@ fn blinded_payment_path( htlc_minimum_msat: intro_node_min_htlc_opt.unwrap_or_else(|| channel_upds.last().unwrap().htlc_minimum_msat), }, + payment_context: PaymentContext::unknown(), }; let mut secp_ctx = Secp256k1::new(); BlindedPath::new_for_payment( @@ -108,6 +109,7 @@ fn do_one_hop_blinded_path(success: bool) { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, + payment_context: PaymentContext::unknown(), }; let mut secp_ctx = Secp256k1::new(); let blinded_path = BlindedPath::one_hop_for_payment( @@ -151,6 +153,7 @@ fn mpp_to_one_hop_blinded_path() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd_1_3.htlc_minimum_msat, }, + payment_context: PaymentContext::unknown(), }; let blinded_path = BlindedPath::one_hop_for_payment( nodes[3].node.get_our_node_id(), payee_tlvs, TEST_FINAL_CLTV as u16, @@ -1281,6 +1284,7 @@ fn custom_tlvs_to_blinded_path() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, + payment_context: PaymentContext::unknown(), }; let mut secp_ctx = Secp256k1::new(); let blinded_path = BlindedPath::one_hop_for_payment( diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 553ff853e7e..6a33b2d54bc 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -32,7 +32,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{secp256k1, Sequence}; use crate::blinded_path::{BlindedPath, NodeIdLookUp}; -use crate::blinded_path::payment::{PaymentConstraints, ReceiveTlvs}; +use crate::blinded_path::payment::{PaymentConstraints, PaymentContext, ReceiveTlvs}; use crate::chain; use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock}; use crate::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator}; @@ -8995,6 +8995,7 @@ where max_cltv_expiry, htlc_minimum_msat: 1, }, + payment_context: PaymentContext::unknown(), }; self.router.create_blinded_payment_paths( payee_node_id, first_hops, payee_tlvs, amount_msats, secp_ctx diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 8040d8c4209..4d1f6985f23 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2713,7 +2713,7 @@ impl ReadableArgs<(Option, &NS)> for InboundOnionPayload w }) }, ChaChaPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(ReceiveTlvs { - payment_secret, payment_constraints + payment_secret, payment_constraints, payment_context: _ })} => { if total_msat.unwrap_or(0) > MAX_VALUE_MSAT { return Err(DecodeError::InvalidValue) } Ok(Self::BlindedReceive { From 9be364f60ea1ed348a94ea93e9e4d90394a4c1cb Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 26 Mar 2024 13:10:04 -0500 Subject: [PATCH 06/17] Pass PaymentContext through HTLC processing PendingInboundPayment::BlindedReceive includes a PaymentContext, which the recipient includes in each blinded path. Included this when processing HTLCs in ChannelManager, first into PendingHTLCRouting and then to OnionPayload. Later, this will be included in the PaymentPurpose when surfaced to user in PaymentClaimable and PaymentClaimed events. --- lightning/src/blinded_path/payment.rs | 4 ++-- lightning/src/ln/channelmanager.rs | 32 ++++++++++++++++++++------- lightning/src/ln/msgs.rs | 6 +++-- lightning/src/ln/onion_payment.rs | 10 +++++---- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index b722a441919..39f7c7f1108 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -104,14 +104,14 @@ pub struct PaymentConstraints { /// /// [`BlindedPath`]: crate::blinded_path::BlindedPath /// [`PaymentPurpose`]: crate::events::PaymentPurpose -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum PaymentContext { /// The payment context was unknown. Unknown(UnknownPaymentContext), } /// An unknown payment context. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct UnknownPaymentContext(()); impl PaymentContext { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 6a33b2d54bc..ef10717add2 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -155,6 +155,11 @@ pub enum PendingHTLCRouting { /// [`Event::PaymentClaimable::onion_fields`] as /// [`RecipientOnionFields::payment_metadata`]. payment_metadata: Option>, + /// The context of the payment included by the recipient in a blinded path, or `None` if a + /// blinded path was not used. + /// + /// Used in part to determine the [`events::PaymentPurpose`]. + payment_context: Option, /// CLTV expiry of the received HTLC. /// /// Used to track when we should expire pending HTLCs that go unclaimed. @@ -352,6 +357,11 @@ enum OnionPayload { /// This is only here for backwards-compatibility in serialization, in the future it can be /// removed, breaking clients running 0.0.106 and earlier. _legacy_hop_data: Option, + /// The context of the payment included by the recipient in a blinded path, or `None` if a + /// blinded path was not used. + /// + /// Used in part to determine the [`events::PaymentPurpose`]. + payment_context: Option, }, /// Contains the payer-provided preimage. Spontaneous(PaymentPreimage), @@ -5336,13 +5346,14 @@ where let blinded_failure = routing.blinded_failure(); let (cltv_expiry, onion_payload, payment_data, phantom_shared_secret, mut onion_fields) = match routing { PendingHTLCRouting::Receive { - payment_data, payment_metadata, incoming_cltv_expiry, phantom_shared_secret, - custom_tlvs, requires_blinded_error: _ + payment_data, payment_metadata, payment_context, + incoming_cltv_expiry, phantom_shared_secret, custom_tlvs, + requires_blinded_error: _ } => { let _legacy_hop_data = Some(payment_data.clone()); let onion_fields = RecipientOnionFields { payment_secret: Some(payment_data.payment_secret), payment_metadata, custom_tlvs }; - (incoming_cltv_expiry, OnionPayload::Invoice { _legacy_hop_data }, + (incoming_cltv_expiry, OnionPayload::Invoice { _legacy_hop_data, payment_context }, Some(payment_data), phantom_shared_secret, onion_fields) }, PendingHTLCRouting::ReceiveKeysend { @@ -10677,6 +10688,7 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting, (3, payment_metadata, option), (5, custom_tlvs, optional_vec), (7, requires_blinded_error, (default_value, false)), + (9, payment_context, option), }, (2, ReceiveKeysend) => { (0, payment_preimage, required), @@ -10791,9 +10803,11 @@ impl_writeable_tlv_based!(HTLCPreviousHopData, { impl Writeable for ClaimableHTLC { fn write(&self, writer: &mut W) -> Result<(), io::Error> { - let (payment_data, keysend_preimage) = match &self.onion_payload { - OnionPayload::Invoice { _legacy_hop_data } => (_legacy_hop_data.as_ref(), None), - OnionPayload::Spontaneous(preimage) => (None, Some(preimage)), + let (payment_data, keysend_preimage, payment_context) = match &self.onion_payload { + OnionPayload::Invoice { _legacy_hop_data, payment_context } => { + (_legacy_hop_data.as_ref(), None, payment_context.as_ref()) + }, + OnionPayload::Spontaneous(preimage) => (None, Some(preimage), None), }; write_tlv_fields!(writer, { (0, self.prev_hop, required), @@ -10805,6 +10819,7 @@ impl Writeable for ClaimableHTLC { (6, self.cltv_expiry, required), (8, keysend_preimage, option), (10, self.counterparty_skimmed_fee_msat, option), + (11, payment_context, option), }); Ok(()) } @@ -10822,6 +10837,7 @@ impl Readable for ClaimableHTLC { (6, cltv_expiry, required), (8, keysend_preimage, option), (10, counterparty_skimmed_fee_msat, option), + (11, payment_context, option), }); let payment_data: Option = payment_data_opt; let value = value_ser.0.unwrap(); @@ -10842,7 +10858,7 @@ impl Readable for ClaimableHTLC { } total_msat = Some(payment_data.as_ref().unwrap().total_msat); } - OnionPayload::Invoice { _legacy_hop_data: payment_data } + OnionPayload::Invoice { _legacy_hop_data: payment_data, payment_context } }, }; Ok(Self { @@ -12079,7 +12095,7 @@ where return Err(DecodeError::InvalidValue); } let purpose = match &htlcs[0].onion_payload { - OnionPayload::Invoice { _legacy_hop_data } => { + OnionPayload::Invoice { _legacy_hop_data, payment_context: _ } => { if let Some(hop_data) = _legacy_hop_data { events::PaymentPurpose::InvoicePayment { payment_preimage: match pending_inbound_payments.get(&payment_hash) { diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 4d1f6985f23..d688a38babc 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -1672,7 +1672,7 @@ pub struct FinalOnionHopData { mod fuzzy_internal_msgs { use bitcoin::secp256k1::PublicKey; - use crate::blinded_path::payment::{PaymentConstraints, PaymentRelay}; + use crate::blinded_path::payment::{PaymentConstraints, PaymentContext, PaymentRelay}; use crate::ln::{PaymentPreimage, PaymentSecret}; use crate::ln::features::BlindedHopFeatures; use super::{FinalOnionHopData, TrampolineOnionPacket}; @@ -1711,6 +1711,7 @@ mod fuzzy_internal_msgs { cltv_expiry_height: u32, payment_secret: PaymentSecret, payment_constraints: PaymentConstraints, + payment_context: PaymentContext, intro_node_blinding_point: Option, keysend_preimage: Option, custom_tlvs: Vec<(u64, Vec)>, @@ -2713,7 +2714,7 @@ impl ReadableArgs<(Option, &NS)> for InboundOnionPayload w }) }, ChaChaPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(ReceiveTlvs { - payment_secret, payment_constraints, payment_context: _ + payment_secret, payment_constraints, payment_context })} => { if total_msat.unwrap_or(0) > MAX_VALUE_MSAT { return Err(DecodeError::InvalidValue) } Ok(Self::BlindedReceive { @@ -2722,6 +2723,7 @@ impl ReadableArgs<(Option, &NS)> for InboundOnionPayload w cltv_expiry_height: cltv_value.ok_or(DecodeError::InvalidValue)?, payment_secret, payment_constraints, + payment_context, intro_node_blinding_point, keysend_preimage, custom_tlvs, diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index aa8ee0ce9be..db8c4cd0337 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -131,17 +131,18 @@ pub(super) fn create_recv_pending_htlc_info( ) -> Result { let ( payment_data, keysend_preimage, custom_tlvs, onion_amt_msat, onion_cltv_expiry, - payment_metadata, requires_blinded_error + payment_metadata, payment_context, requires_blinded_error ) = 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, false), + cltv_expiry_height, payment_metadata, None, false), msgs::InboundOnionPayload::BlindedReceive { sender_intended_htlc_amt_msat, total_msat, cltv_expiry_height, payment_secret, - intro_node_blinding_point, payment_constraints, keysend_preimage, custom_tlvs + intro_node_blinding_point, payment_constraints, payment_context, keysend_preimage, + custom_tlvs } => { check_blinded_payment_constraints( sender_intended_htlc_amt_msat, cltv_expiry, &payment_constraints @@ -155,7 +156,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, + sender_intended_htlc_amt_msat, cltv_expiry_height, None, Some(payment_context), intro_node_blinding_point.is_none()) } msgs::InboundOnionPayload::Forward { .. } => { @@ -241,6 +242,7 @@ pub(super) fn create_recv_pending_htlc_info( PendingHTLCRouting::Receive { payment_data: data, payment_metadata, + payment_context, incoming_cltv_expiry: onion_cltv_expiry, phantom_shared_secret, custom_tlvs, From b9970ff41b497bce5cba13b2bb8023368e8ea2f8 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 25 Mar 2024 16:10:36 -0500 Subject: [PATCH 07/17] Define an OfferId for BOLT 12 Offers Use a merkle root hash of the offer TLV records to define an offer id. Will be included in a BOLT 12 invoice's blinded payment paths in order for the recipient to identify which offer the payment is for. --- lightning/src/offers/merkle.rs | 4 +++ lightning/src/offers/offer.rs | 46 ++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index da3fab58996..9dd6bcde8ca 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -66,6 +66,10 @@ impl TaggedHash { pub fn merkle_root(&self) -> sha256::Hash { self.merkle_root } + + pub(super) fn to_bytes(&self) -> [u8; 32] { + *self.digest.as_ref() + } } impl AsRef for TaggedHash { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index d390552f866..df3df4b3f64 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -90,11 +90,11 @@ use crate::blinded_path::BlindedPath; use crate::ln::channelmanager::PaymentId; use crate::ln::features::OfferFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; -use crate::ln::msgs::MAX_VALUE_MSAT; -use crate::offers::merkle::TlvStream; +use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; +use crate::offers::merkle::{TaggedHash, TlvStream}; use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::signer::{Metadata, MetadataMaterial, self}; -use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; +use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; #[cfg(not(c_bindings))] @@ -114,6 +114,31 @@ use std::time::SystemTime; pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~"; +/// An identifier for an [`Offer`] built using [`DerivedMetadata`]. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct OfferId(pub [u8; 32]); + +impl OfferId { + const ID_TAG: &'static str = "LDK Offer ID"; + + fn from_valid_offer_tlv_stream(bytes: &[u8]) -> Self { + let tagged_hash = TaggedHash::new(Self::ID_TAG, &bytes); + Self(tagged_hash.to_bytes()) + } +} + +impl Writeable for OfferId { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.0.write(w) + } +} + +impl Readable for OfferId { + fn read(r: &mut R) -> Result { + Ok(OfferId(Readable::read(r)?)) + } +} + /// Builds an [`Offer`] for the "offer to be paid" flow. /// /// See [module-level documentation] for usage. @@ -370,12 +395,15 @@ macro_rules! offer_builder_methods { ( let mut bytes = Vec::new(); $self.offer.write(&mut bytes).unwrap(); + let id = OfferId::from_valid_offer_tlv_stream(&bytes); + Offer { bytes, #[cfg(not(c_bindings))] contents: $self.offer, #[cfg(c_bindings)] - contents: $self.offer.clone() + contents: $self.offer.clone(), + id, } } } } @@ -488,6 +516,7 @@ pub struct Offer { // fields. pub(super) bytes: Vec, pub(super) contents: OfferContents, + id: OfferId, } /// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or a @@ -577,6 +606,11 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => { impl Offer { offer_accessors!(self, self.contents); + /// Returns the id of the offer. + pub fn id(&self) -> OfferId { + self.id + } + pub(super) fn implied_chain(&self) -> ChainHash { self.contents.implied_chain() } @@ -1002,7 +1036,9 @@ impl TryFrom> for Offer { let offer = ParsedMessage::::try_from(bytes)?; let ParsedMessage { bytes, tlv_stream } = offer; let contents = OfferContents::try_from(tlv_stream)?; - Ok(Offer { bytes, contents }) + let id = OfferId::from_valid_offer_tlv_stream(&bytes); + + Ok(Offer { bytes, contents, id }) } } From ac6d4cb9ad45189e963bd906e9da971502dff129 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 26 Mar 2024 18:39:06 -0500 Subject: [PATCH 08/17] Include OfferId in VerifiedInvoiceRequest Extract the OfferId from the offer metadata sent back in the InvoiceRequest and include it in VerifiedInvoiceRequest. This can be used to correspond the eventual payment for the invoice response by including it in the invoice's blinded payment paths. --- lightning/src/offers/invoice.rs | 10 +++---- lightning/src/offers/invoice_request.rs | 24 ++++++++++------- lightning/src/offers/merkle.rs | 36 ++++++++++++++++--------- lightning/src/offers/offer.rs | 28 ++++++++++++++----- 4 files changed, 65 insertions(+), 33 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index a384ab08656..ee5e6deb408 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -546,7 +546,7 @@ impl UnsignedBolt12Invoice { let mut bytes = Vec::new(); unsigned_tlv_stream.write(&mut bytes).unwrap(); - let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); Self { bytes, contents, tagged_hash } } @@ -1225,7 +1225,7 @@ impl TryFrom> for UnsignedBolt12Invoice { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) )?; - let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); Ok(UnsignedBolt12Invoice { bytes, contents, tagged_hash }) } @@ -1370,7 +1370,7 @@ impl TryFrom> for Bolt12Invoice { None => return Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)), Some(signature) => signature, }; - let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); let pubkey = contents.fields().signing_pubkey; merkle::verify_signature(&signature, &tagged_hash, pubkey)?; @@ -1601,7 +1601,7 @@ mod tests { assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); - let message = TaggedHash::new(SIGNATURE_TAG, &invoice.bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); assert!(merkle::verify_signature(&invoice.signature, &message, recipient_pubkey()).is_ok()); let digest = Message::from_slice(&invoice.signable_hash()).unwrap(); @@ -1698,7 +1698,7 @@ mod tests { assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); - let message = TaggedHash::new(SIGNATURE_TAG, &invoice.bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); assert!(merkle::verify_signature(&invoice.signature, &message, recipient_pubkey()).is_ok()); assert_eq!( diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 5e3ed40ac08..07a117233fa 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -72,7 +72,7 @@ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; use crate::offers::invoice::BlindedPayInfo; use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, self}; -use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::signer::{Metadata, MetadataMaterial}; @@ -529,7 +529,7 @@ impl UnsignedInvoiceRequest { let mut bytes = Vec::new(); unsigned_tlv_stream.write(&mut bytes).unwrap(); - let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); Self { bytes, contents, tagged_hash } } @@ -607,6 +607,9 @@ pub struct InvoiceRequest { /// ways to respond depending on whether the signing keys were derived. #[derive(Clone, Debug)] pub struct VerifiedInvoiceRequest { + /// The identifier of the [`Offer`] for which the [`InvoiceRequest`] was made. + pub offer_id: OfferId, + /// The verified request. inner: InvoiceRequest, @@ -764,8 +767,9 @@ macro_rules! invoice_request_verify_method { ($self: ident, $self_type: ty) => { #[cfg(c_bindings)] secp_ctx: &Secp256k1, ) -> Result { - let keys = $self.contents.inner.offer.verify(&$self.bytes, key, secp_ctx)?; + let (offer_id, keys) = $self.contents.inner.offer.verify(&$self.bytes, key, secp_ctx)?; Ok(VerifiedInvoiceRequest { + offer_id, #[cfg(not(c_bindings))] inner: $self, #[cfg(c_bindings)] @@ -1022,7 +1026,7 @@ impl TryFrom> for UnsignedInvoiceRequest { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) )?; - let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); Ok(UnsignedInvoiceRequest { bytes, contents, tagged_hash }) } @@ -1046,7 +1050,7 @@ impl TryFrom> for InvoiceRequest { None => return Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)), Some(signature) => signature, }; - let message = TaggedHash::new(SIGNATURE_TAG, &bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); merkle::verify_signature(&signature, &message, contents.payer_id)?; Ok(InvoiceRequest { bytes, contents, signature }) @@ -1192,7 +1196,7 @@ mod tests { assert_eq!(invoice_request.payer_id(), payer_pubkey()); assert_eq!(invoice_request.payer_note(), None); - let message = TaggedHash::new(SIGNATURE_TAG, &invoice_request.bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice_request.bytes); assert!(merkle::verify_signature(&invoice_request.signature, &message, payer_pubkey()).is_ok()); assert_eq!( @@ -1297,7 +1301,7 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes); let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); @@ -1320,7 +1324,7 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes); let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); @@ -1369,7 +1373,7 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes); let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); @@ -1392,7 +1396,7 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes); let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 9dd6bcde8ca..a3979866926 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -38,10 +38,20 @@ pub struct TaggedHash { } impl TaggedHash { + /// Creates a tagged hash with the given parameters. + /// + /// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record. + pub(super) fn from_valid_tlv_stream_bytes(tag: &'static str, bytes: &[u8]) -> Self { + let tlv_stream = TlvStream::new(bytes); + Self::from_tlv_stream(tag, tlv_stream) + } + /// Creates a tagged hash with the given parameters. /// /// Panics if `tlv_stream` is not a well-formed TLV stream containing at least one TLV record. - pub(super) fn new(tag: &'static str, tlv_stream: &[u8]) -> Self { + pub(super) fn from_tlv_stream<'a, I: core::iter::Iterator>>( + tag: &'static str, tlv_stream: I + ) -> Self { let tag_hash = sha256::Hash::hash(tag.as_bytes()); let merkle_root = root_hash(tlv_stream); let digest = Message::from_slice(tagged_hash(tag_hash, merkle_root).as_byte_array()).unwrap(); @@ -141,9 +151,10 @@ pub(super) fn verify_signature( /// Computes a merkle root hash for the given data, which must be a well-formed TLV stream /// containing at least one TLV record. -fn root_hash(data: &[u8]) -> sha256::Hash { +fn root_hash<'a, I: core::iter::Iterator>>(tlv_stream: I) -> sha256::Hash { + let mut tlv_stream = tlv_stream.peekable(); let nonce_tag = tagged_hash_engine(sha256::Hash::from_engine({ - let first_tlv_record = TlvStream::new(&data[..]).next().unwrap(); + let first_tlv_record = tlv_stream.peek().unwrap(); let mut engine = sha256::Hash::engine(); engine.input("LnNonce".as_bytes()); engine.input(first_tlv_record.record_bytes); @@ -153,8 +164,7 @@ fn root_hash(data: &[u8]) -> sha256::Hash { let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes())); let mut leaves = Vec::new(); - let tlv_stream = TlvStream::new(&data[..]); - for record in tlv_stream.skip_signatures() { + for record in TlvStream::skip_signatures(tlv_stream) { leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes)); leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes)); } @@ -231,8 +241,10 @@ impl<'a> TlvStream<'a> { .take_while(move |record| take_range.contains(&record.r#type)) } - fn skip_signatures(self) -> core::iter::Filter, fn(&TlvRecord) -> bool> { - self.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) + fn skip_signatures( + tlv_stream: impl core::iter::Iterator> + ) -> impl core::iter::Iterator> { + tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) } } @@ -280,7 +292,7 @@ impl<'a> Writeable for WithoutSignatures<'a> { #[inline] fn write(&self, writer: &mut W) -> Result<(), io::Error> { let tlv_stream = TlvStream::new(self.0); - for record in tlv_stream.skip_signatures() { + for record in TlvStream::skip_signatures(tlv_stream) { writer.write_all(record.record_bytes)?; } Ok(()) @@ -308,15 +320,15 @@ mod tests { macro_rules! tlv2 { () => { "02080000010000020003" } } macro_rules! tlv3 { () => { "03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002" } } assert_eq!( - super::root_hash(&>::from_hex(tlv1!()).unwrap()), + super::root_hash(TlvStream::new(&>::from_hex(tlv1!()).unwrap())), sha256::Hash::from_slice(&>::from_hex("b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93").unwrap()).unwrap(), ); assert_eq!( - super::root_hash(&>::from_hex(concat!(tlv1!(), tlv2!())).unwrap()), + super::root_hash(TlvStream::new(&>::from_hex(concat!(tlv1!(), tlv2!())).unwrap())), sha256::Hash::from_slice(&>::from_hex("c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1").unwrap()).unwrap(), ); assert_eq!( - super::root_hash(&>::from_hex(concat!(tlv1!(), tlv2!(), tlv3!())).unwrap()), + super::root_hash(TlvStream::new(&>::from_hex(concat!(tlv1!(), tlv2!(), tlv3!())).unwrap())), sha256::Hash::from_slice(&>::from_hex("ab2e79b1283b0b31e0b035258de23782df6b89a38cfa7237bde69aed1a658c5d").unwrap()).unwrap(), ); } @@ -348,7 +360,7 @@ mod tests { "lnr1qqyqqqqqqqqqqqqqqcp4256ypqqkgzshgysy6ct5dpjk6ct5d93kzmpq23ex2ct5d9ek293pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpjkppqvjx204vgdzgsqpvcp4mldl3plscny0rt707gvpdh6ndydfacz43euzqhrurageg3n7kafgsek6gz3e9w52parv8gs2hlxzk95tzeswywffxlkeyhml0hh46kndmwf4m6xma3tkq2lu04qz3slje2rfthc89vss", ); assert_eq!( - super::root_hash(&invoice_request.bytes[..]), + super::root_hash(TlvStream::new(&invoice_request.bytes[..])), sha256::Hash::from_slice(&>::from_hex("608407c18ad9a94d9ea2bcdbe170b6c20c462a7833a197621c916f78cf18e624").unwrap()).unwrap(), ); assert_eq!( diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index df3df4b3f64..324e3ea42c9 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -122,7 +122,13 @@ impl OfferId { const ID_TAG: &'static str = "LDK Offer ID"; fn from_valid_offer_tlv_stream(bytes: &[u8]) -> Self { - let tagged_hash = TaggedHash::new(Self::ID_TAG, &bytes); + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(Self::ID_TAG, bytes); + Self(tagged_hash.to_bytes()) + } + + fn from_valid_invreq_tlv_stream(bytes: &[u8]) -> Self { + let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES); + let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream); Self(tagged_hash.to_bytes()) } } @@ -887,7 +893,7 @@ impl OfferContents { /// Verifies that the offer metadata was produced from the offer in the TLV stream. pub(super) fn verify( &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1 - ) -> Result, ()> { + ) -> Result<(OfferId, Option), ()> { match self.metadata() { Some(metadata) => { let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| { @@ -899,9 +905,13 @@ impl OfferContents { _ => true, } }); - signer::verify_recipient_metadata( + let keys = signer::verify_recipient_metadata( metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx - ) + )?; + + let offer_id = OfferId::from_valid_invreq_tlv_stream(bytes); + + Ok((offer_id, keys)) }, None => Err(()), } @@ -1246,7 +1256,10 @@ mod tests { let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok()); + match invoice_request.verify(&expanded_key, &secp_ctx) { + Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()), + Err(_) => panic!("unexpected error"), + } // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); @@ -1305,7 +1318,10 @@ mod tests { let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok()); + match invoice_request.verify(&expanded_key, &secp_ctx) { + Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()), + Err(_) => panic!("unexpected error"), + } // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); From 48d9245f199e58449bb4f7d0eeb314fa5aca104e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 26 Mar 2024 18:48:39 -0500 Subject: [PATCH 09/17] Include Offer context in blinded payment paths When constructing blinded payment paths for a Bolt12Invoice response, include the OfferId so that the eventual payment can be correlated with the Offer. --- lightning/src/blinded_path/payment.rs | 22 ++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 16 +++++++++++----- lightning/src/offers/offer.rs | 2 +- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 39f7c7f1108..99979ecf4a3 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -12,6 +12,7 @@ use crate::ln::channelmanager::CounterpartyForwardingInfo; use crate::ln::features::BlindedHopFeatures; use crate::ln::msgs::DecodeError; use crate::offers::invoice::BlindedPayInfo; +use crate::offers::offer::OfferId; use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer}; #[allow(unused_imports)] @@ -108,12 +109,28 @@ pub struct PaymentConstraints { pub enum PaymentContext { /// The payment context was unknown. Unknown(UnknownPaymentContext), + + /// The payment was made for an invoice requested from a BOLT 12 [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + Bolt12Offer(Bolt12OfferContext), } /// An unknown payment context. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UnknownPaymentContext(()); +/// The context of a payment made for an invoice requested from a BOLT 12 [`Offer`]. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Bolt12OfferContext { + /// The identifier of the [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + pub offer_id: OfferId, +} + impl PaymentContext { pub(crate) fn unknown() -> Self { PaymentContext::Unknown(UnknownPaymentContext(())) @@ -340,6 +357,7 @@ impl Readable for PaymentConstraints { impl_writeable_tlv_based_enum!(PaymentContext, ; (0, Unknown), + (1, Bolt12Offer), ); impl Writeable for UnknownPaymentContext { @@ -354,6 +372,10 @@ impl Readable for UnknownPaymentContext { } } +impl_writeable_tlv_based!(Bolt12OfferContext, { + (0, offer_id, required), +}); + #[cfg(test)] mod tests { use bitcoin::secp256k1::PublicKey; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ef10717add2..5487ae8ab84 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -32,7 +32,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{secp256k1, Sequence}; use crate::blinded_path::{BlindedPath, NodeIdLookUp}; -use crate::blinded_path::payment::{PaymentConstraints, PaymentContext, ReceiveTlvs}; +use crate::blinded_path::payment::{Bolt12OfferContext, PaymentConstraints, PaymentContext, ReceiveTlvs}; use crate::chain; use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock}; use crate::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator}; @@ -8826,7 +8826,10 @@ where match self.create_inbound_payment(Some(amount_msats), relative_expiry, None) { Ok((payment_hash, payment_secret)) => { - let payment_paths = self.create_blinded_payment_paths(amount_msats, payment_secret) + let payment_context = PaymentContext::unknown(); + let payment_paths = self.create_blinded_payment_paths( + amount_msats, payment_secret, payment_context + ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; #[cfg(feature = "std")] @@ -8992,7 +8995,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 + &self, amount_msats: u64, payment_secret: PaymentSecret, payment_context: PaymentContext ) -> Result, ()> { let secp_ctx = &self.secp_ctx; @@ -9006,7 +9009,7 @@ where max_cltv_expiry, htlc_minimum_msat: 1, }, - payment_context: PaymentContext::unknown(), + payment_context, }; self.router.create_blinded_payment_paths( payee_node_id, first_hops, payee_tlvs, amount_msats, secp_ctx @@ -10360,8 +10363,11 @@ where }, }; + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: invoice_request.offer_id, + }); let payment_paths = match self.create_blinded_payment_paths( - amount_msats, payment_secret + amount_msats, payment_secret, payment_context ) { Ok(payment_paths) => payment_paths, Err(()) => { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 324e3ea42c9..3dedc6cd35d 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -115,7 +115,7 @@ use std::time::SystemTime; pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~"; /// An identifier for an [`Offer`] built using [`DerivedMetadata`]. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct OfferId(pub [u8; 32]); impl OfferId { From 09ce1b5d161b1369e47dd25ada95fcbdf6ce7139 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 26 Mar 2024 18:57:57 -0500 Subject: [PATCH 10/17] Include Refund context in blinded payment paths When requesting a payment for a refund, include a context in the Bolt12Invoice's blinded payment paths indicated it is for such. When the eventual payment is received, the user can use the payment hash to correlate it with the corresponding Refund. --- lightning/src/blinded_path/payment.rs | 14 ++++++++++++++ lightning/src/ln/channelmanager.rs | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 99979ecf4a3..ad7b229b53f 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -114,6 +114,11 @@ pub enum PaymentContext { /// /// [`Offer`]: crate::offers::offer::Offer Bolt12Offer(Bolt12OfferContext), + + /// The payment was made for an invoice sent for a BOLT 12 [`Refund`]. + /// + /// [`Refund`]: crate::offers::refund::Refund + Bolt12Refund(Bolt12RefundContext), } /// An unknown payment context. @@ -131,6 +136,12 @@ pub struct Bolt12OfferContext { pub offer_id: OfferId, } +/// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`]. +/// +/// [`Refund`]: crate::offers::refund::Refund +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Bolt12RefundContext {} + impl PaymentContext { pub(crate) fn unknown() -> Self { PaymentContext::Unknown(UnknownPaymentContext(())) @@ -358,6 +369,7 @@ impl_writeable_tlv_based_enum!(PaymentContext, ; (0, Unknown), (1, Bolt12Offer), + (2, Bolt12Refund), ); impl Writeable for UnknownPaymentContext { @@ -376,6 +388,8 @@ impl_writeable_tlv_based!(Bolt12OfferContext, { (0, offer_id, required), }); +impl_writeable_tlv_based!(Bolt12RefundContext, {}); + #[cfg(test)] mod tests { use bitcoin::secp256k1::PublicKey; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 5487ae8ab84..54640ca7d05 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -32,7 +32,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::{secp256k1, Sequence}; use crate::blinded_path::{BlindedPath, NodeIdLookUp}; -use crate::blinded_path::payment::{Bolt12OfferContext, PaymentConstraints, PaymentContext, ReceiveTlvs}; +use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentConstraints, PaymentContext, ReceiveTlvs}; use crate::chain; use crate::chain::{Confirm, ChannelMonitorUpdateStatus, Watch, BestBlock}; use crate::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator}; @@ -8826,7 +8826,7 @@ where match self.create_inbound_payment(Some(amount_msats), relative_expiry, None) { Ok((payment_hash, payment_secret)) => { - let payment_context = PaymentContext::unknown(); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let payment_paths = self.create_blinded_payment_paths( amount_msats, payment_secret, payment_context ) From e74f24332f76beb21d20f9989244db1c57956d3b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 12 Apr 2024 21:17:35 -0500 Subject: [PATCH 11/17] Rename InvoicePayment to Bolt11InvoicePayment PaymentPurpose will be expanded to include BOLT 12 payment context. Rename the InvoicePayment variant accordinly before new variants are added. --- lightning/src/events/mod.rs | 14 +++++---- lightning/src/ln/chanmon_update_fail_tests.rs | 20 ++++++------- lightning/src/ln/channelmanager.rs | 30 +++++++++---------- lightning/src/ln/functional_test_utils.rs | 8 ++--- lightning/src/ln/functional_tests.rs | 20 ++++++------- lightning/src/ln/offers_tests.rs | 2 +- lightning/src/ln/payment_tests.rs | 8 +++-- 7 files changed, 53 insertions(+), 49 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index f6e7f716487..d6553ae3aa2 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -49,8 +49,8 @@ use crate::prelude::*; /// spontaneous payment or a "conventional" lightning payment that's paying an invoice. #[derive(Clone, Debug, PartialEq, Eq)] pub enum PaymentPurpose { - /// Information for receiving a payment that we generated an invoice for. - InvoicePayment { + /// A payment for a BOLT 11 invoice. + Bolt11InvoicePayment { /// The preimage to the payment_hash, if the payment hash (and secret) were fetched via /// [`ChannelManager::create_inbound_payment`]. If provided, this can be handed directly to /// [`ChannelManager::claim_funds`]. @@ -79,14 +79,14 @@ impl PaymentPurpose { /// Returns the preimage for this payment, if it is known. pub fn preimage(&self) -> Option { match self { - PaymentPurpose::InvoicePayment { payment_preimage, .. } => *payment_preimage, + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => *payment_preimage, PaymentPurpose::SpontaneousPayment(preimage) => Some(*preimage), } } } impl_writeable_tlv_based_enum!(PaymentPurpose, - (0, InvoicePayment) => { + (0, Bolt11InvoicePayment) => { (0, payment_preimage, option), (2, payment_secret, required), }; @@ -1059,7 +1059,9 @@ impl Writeable for Event { let mut payment_secret = None; let payment_preimage; match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage: preimage, payment_secret: secret } => { + PaymentPurpose::Bolt11InvoicePayment { + payment_preimage: preimage, payment_secret: secret + } => { payment_secret = Some(secret); payment_preimage = *preimage; }, @@ -1325,7 +1327,7 @@ impl MaybeReadable for Event { (10, counterparty_skimmed_fee_msat_opt, option), }); let purpose = match payment_secret { - Some(secret) => PaymentPurpose::InvoicePayment { + Some(secret) => PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret: secret }, diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs index 2e95f5c63ff..11ab1fbb851 100644 --- a/lightning/src/ln/chanmon_update_fail_tests.rs +++ b/lightning/src/ln/chanmon_update_fail_tests.rs @@ -173,11 +173,11 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { assert_eq!(receiver_node_id.unwrap(), nodes[1].node.get_our_node_id()); assert_eq!(via_channel_id, Some(channel_id)); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(payment_secret_1, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), @@ -554,11 +554,11 @@ fn do_test_monitor_temporary_update_fail(disconnect_count: usize) { assert_eq!(receiver_node_id.unwrap(), nodes[1].node.get_our_node_id()); assert_eq!(via_channel_id, Some(channel_id)); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(payment_secret_2, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), @@ -672,11 +672,11 @@ fn test_monitor_update_fail_cs() { assert_eq!(receiver_node_id.unwrap(), nodes[1].node.get_our_node_id()); assert_eq!(via_channel_id, Some(channel_id)); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(our_payment_secret, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), @@ -1683,11 +1683,11 @@ fn test_monitor_update_fail_claim() { assert_eq!(via_channel_id, Some(channel_id)); assert_eq!(via_user_channel_id, Some(42)); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(payment_secret_2, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), @@ -1699,11 +1699,11 @@ fn test_monitor_update_fail_claim() { assert_eq!(receiver_node_id.unwrap(), nodes[0].node.get_our_node_id()); assert_eq!(via_channel_id, Some(channel_id)); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(payment_secret_3, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 54640ca7d05..d9ce1940c64 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1464,12 +1464,12 @@ where /// // On the event processing thread /// channel_manager.process_pending_events(&|event| match event { /// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { -/// PaymentPurpose::InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { +/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { /// assert_eq!(payment_hash, known_payment_hash); /// println!("Claiming payment {}", payment_hash); /// channel_manager.claim_funds(payment_preimage); /// }, -/// PaymentPurpose::InvoicePayment { payment_preimage: None, .. } => { +/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: None, .. } => { /// println!("Unknown payment hash: {}", payment_hash); /// }, /// PaymentPurpose::SpontaneousPayment(payment_preimage) => { @@ -1564,11 +1564,11 @@ where /// // On the event processing thread /// channel_manager.process_pending_events(&|event| match event { /// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { -/// PaymentPurpose::InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { +/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { /// println!("Claiming payment {}", payment_hash); /// channel_manager.claim_funds(payment_preimage); /// }, -/// PaymentPurpose::InvoicePayment { payment_preimage: None, .. } => { +/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: None, .. } => { /// println!("Unknown payment hash: {}", payment_hash); /// }, /// // ... @@ -1716,12 +1716,12 @@ where /// // On the event processing thread /// channel_manager.process_pending_events(&|event| match event { /// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { -/// PaymentPurpose::InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { +/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { /// assert_eq!(payment_hash, known_payment_hash); /// println!("Claiming payment {}", payment_hash); /// channel_manager.claim_funds(payment_preimage); /// }, -/// PaymentPurpose::InvoicePayment { payment_preimage: None, .. } => { +/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: None, .. } => { /// println!("Unknown payment hash: {}", payment_hash); /// }, /// // ... @@ -5433,7 +5433,7 @@ where let mut payment_claimable_generated = false; let is_keysend = match $purpose { events::PaymentPurpose::SpontaneousPayment(_) => true, - events::PaymentPurpose::InvoicePayment { .. } => false, + events::PaymentPurpose::Bolt11InvoicePayment { .. } => false, }; let mut claimable_payments = self.claimable_payments.lock().unwrap(); if claimable_payments.pending_claiming_payments.contains_key(&payment_hash) { @@ -5548,7 +5548,7 @@ where fail_htlc!(claimable_htlc, payment_hash); } } - let purpose = events::PaymentPurpose::InvoicePayment { + let purpose = events::PaymentPurpose::Bolt11InvoicePayment { payment_preimage: payment_preimage.clone(), payment_secret: payment_data.payment_secret, }; @@ -5574,7 +5574,7 @@ where &payment_hash, payment_data.total_msat, inbound_payment.get().min_value_msat.unwrap()); fail_htlc!(claimable_htlc, payment_hash); } else { - let purpose = events::PaymentPurpose::InvoicePayment { + let purpose = events::PaymentPurpose::Bolt11InvoicePayment { payment_preimage: inbound_payment.get().payment_preimage, payment_secret: payment_data.payment_secret, }; @@ -8881,9 +8881,9 @@ where /// [`PaymentHash`] and [`PaymentPreimage`] for you. /// /// The [`PaymentPreimage`] will ultimately be returned to you in the [`PaymentClaimable`], which - /// will have the [`PaymentClaimable::purpose`] be [`PaymentPurpose::InvoicePayment`] with - /// its [`PaymentPurpose::InvoicePayment::payment_preimage`] field filled in. That should then be - /// passed directly to [`claim_funds`]. + /// will have the [`PaymentClaimable::purpose`] be [`PaymentPurpose::Bolt11InvoicePayment`] with + /// its [`PaymentPurpose::Bolt11InvoicePayment::payment_preimage`] field filled in. That should + /// then be passed directly to [`claim_funds`]. /// /// See [`create_inbound_payment_for_hash`] for detailed documentation on behavior and requirements. /// @@ -8903,8 +8903,8 @@ where /// [`claim_funds`]: Self::claim_funds /// [`PaymentClaimable`]: events::Event::PaymentClaimable /// [`PaymentClaimable::purpose`]: events::Event::PaymentClaimable::purpose - /// [`PaymentPurpose::InvoicePayment`]: events::PaymentPurpose::InvoicePayment - /// [`PaymentPurpose::InvoicePayment::payment_preimage`]: events::PaymentPurpose::InvoicePayment::payment_preimage + /// [`PaymentPurpose::Bolt11InvoicePayment`]: events::PaymentPurpose::Bolt11InvoicePayment + /// [`PaymentPurpose::Bolt11InvoicePayment::payment_preimage`]: events::PaymentPurpose::Bolt11InvoicePayment::payment_preimage /// [`create_inbound_payment_for_hash`]: Self::create_inbound_payment_for_hash pub fn create_inbound_payment(&self, min_value_msat: Option, invoice_expiry_delta_secs: u32, min_final_cltv_expiry_delta: Option) -> Result<(PaymentHash, PaymentSecret), ()> { @@ -12103,7 +12103,7 @@ where let purpose = match &htlcs[0].onion_payload { OnionPayload::Invoice { _legacy_hop_data, payment_context: _ } => { if let Some(hop_data) = _legacy_hop_data { - events::PaymentPurpose::InvoicePayment { + events::PaymentPurpose::Bolt11InvoicePayment { payment_preimage: match pending_inbound_payments.get(&payment_hash) { Some(inbound_payment) => inbound_payment.payment_preimage, None => match inbound_payment::verify(payment_hash, &hop_data, 0, &expanded_inbound_key, &args.logger) { diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index ee4c027a10e..35dbfd419a1 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -2129,7 +2129,7 @@ pub fn check_payment_claimable( assert_eq!(expected_recv_value, *amount_msat); assert_eq!(expected_receiver_node_id, receiver_node_id.unwrap()); match purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert_eq!(&expected_payment_preimage, payment_preimage); assert_eq!(expected_payment_secret, *payment_secret); }, @@ -2606,7 +2606,7 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option assert!(onion_fields.is_some()); assert_eq!(onion_fields.as_ref().unwrap().custom_tlvs, custom_tlvs); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert_eq!(expected_preimage, *payment_preimage); assert_eq!(our_payment_secret.unwrap(), *payment_secret); assert_eq!(Some(*payment_secret), onion_fields.as_ref().unwrap().payment_secret); @@ -2768,7 +2768,7 @@ pub fn pass_claimed_payment_along_route<'a, 'b, 'c, 'd>(args: ClaimAlongRouteArg ref htlcs, .. } | Event::PaymentClaimed { - purpose: PaymentPurpose::InvoicePayment { payment_preimage: Some(preimage), ..}, + purpose: PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(preimage), ..}, ref htlcs, amount_msat, .. @@ -2780,7 +2780,7 @@ pub fn pass_claimed_payment_along_route<'a, 'b, 'c, 'd>(args: ClaimAlongRouteArg fwd_amt_msat = amount_msat; }, Event::PaymentClaimed { - purpose: PaymentPurpose::InvoicePayment { .. }, + purpose: PaymentPurpose::Bolt11InvoicePayment { .. }, payment_hash, amount_msat, ref htlcs, diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 8dd3f1fc912..e271daccfb4 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -2039,11 +2039,11 @@ fn test_channel_reserve_holding_cell_htlcs() { assert_eq!(nodes[2].node.get_our_node_id(), receiver_node_id.unwrap()); assert_eq!(via_channel_id, Some(chan_2.2)); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(our_payment_secret_21, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), @@ -2055,11 +2055,11 @@ fn test_channel_reserve_holding_cell_htlcs() { assert_eq!(nodes[2].node.get_our_node_id(), receiver_node_id.unwrap()); assert_eq!(via_channel_id, Some(chan_2.2)); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(our_payment_secret_22, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), @@ -3954,11 +3954,11 @@ fn do_test_drop_messages_peer_disconnect(messages_delivered: u8, simulate_broken assert_eq!(receiver_node_id.unwrap(), nodes[1].node.get_our_node_id()); assert_eq!(via_channel_id, Some(channel_id)); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(payment_secret_1, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), @@ -4319,11 +4319,11 @@ fn test_drop_messages_peer_disconnect_dual_htlc() { Event::PaymentClaimable { ref payment_hash, ref purpose, .. } => { assert_eq!(payment_hash_2, *payment_hash); match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. } => { assert!(payment_preimage.is_none()); assert_eq!(payment_secret_2, *payment_secret); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), @@ -8388,10 +8388,10 @@ fn test_preimage_storage() { match events[0] { Event::PaymentClaimable { ref purpose, .. } => { match &purpose { - PaymentPurpose::InvoicePayment { payment_preimage, .. } => { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => { claim_payment(&nodes[0], &[&nodes[1]], payment_preimage.unwrap()); }, - _ => panic!("expected PaymentPurpose::InvoicePayment") + _ => panic!("expected PaymentPurpose::Bolt11InvoicePayment") } }, _ => panic!("Unexpected event"), diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 5b774ce4ee2..6a0eb6cb3b1 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -155,7 +155,7 @@ fn claim_bolt12_payment<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, path: &[&Node<'a, ' let recipient = &path[path.len() - 1]; match get_event!(recipient, Event::PaymentClaimable) { Event::PaymentClaimable { - purpose: PaymentPurpose::InvoicePayment { + purpose: PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(payment_preimage), .. }, .. } => claim_payment(node, path, payment_preimage), diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index a75120797ca..d1fa58372dd 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -2142,9 +2142,11 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { assert_eq!(skimmed_fee_msat * num_mpp_parts as u64, counterparty_skimmed_fee_msat); assert_eq!(nodes[2].node.get_our_node_id(), receiver_node_id.unwrap()); match purpose { - crate::events::PaymentPurpose::InvoicePayment { payment_preimage: ev_payment_preimage, - payment_secret: ev_payment_secret, .. } => - { + crate::events::PaymentPurpose::Bolt11InvoicePayment { + payment_preimage: ev_payment_preimage, + payment_secret: ev_payment_secret, + .. + } => { assert_eq!(payment_preimage, ev_payment_preimage.unwrap()); assert_eq!(payment_secret, *ev_payment_secret); }, From ef3ec8f146d2a6e9c750113ac8ce5af310fcef34 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 12 Apr 2024 21:21:00 -0500 Subject: [PATCH 12/17] Add PaymentPurpose::is_keysend utility method --- lightning/src/events/mod.rs | 7 +++++++ lightning/src/ln/channelmanager.rs | 5 +---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index d6553ae3aa2..41ba17806d3 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -83,6 +83,13 @@ impl PaymentPurpose { PaymentPurpose::SpontaneousPayment(preimage) => Some(*preimage), } } + + pub(crate) fn is_keysend(&self) -> bool { + match self { + PaymentPurpose::Bolt11InvoicePayment { .. } => false, + PaymentPurpose::SpontaneousPayment(..) => true, + } + } } impl_writeable_tlv_based_enum!(PaymentPurpose, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d9ce1940c64..0693f3c642e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5431,10 +5431,7 @@ where macro_rules! check_total_value { ($purpose: expr) => {{ let mut payment_claimable_generated = false; - let is_keysend = match $purpose { - events::PaymentPurpose::SpontaneousPayment(_) => true, - events::PaymentPurpose::Bolt11InvoicePayment { .. } => false, - }; + let is_keysend = $purpose.is_keysend(); let mut claimable_payments = self.claimable_payments.lock().unwrap(); if claimable_payments.pending_claiming_payments.contains_key(&payment_hash) { fail_htlc!(claimable_htlc, payment_hash); From 54ca54d1917ce5fea627f9144bcacb9cdca4c095 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sat, 13 Apr 2024 17:29:24 -0500 Subject: [PATCH 13/17] BOLT 12 variants of PaymentPurpose In order to provide more context in PaymentClaimable and PaymentClaimed events, introduce new variants of PaymentPurpose for use with BOLT 12 payments. Separate variants are used for offers and refunds. An unknown variant is used for backwards compatibility and ease of testing, but is otherwise not publicly constructable. --- lightning/src/blinded_path/payment.rs | 23 +++++ lightning/src/events/mod.rs | 100 +++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 12 +-- lightning/src/ln/functional_test_utils.rs | 32 +++++-- 4 files changed, 150 insertions(+), 17 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index ad7b229b53f..e243d478bd9 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -121,6 +121,12 @@ pub enum PaymentContext { Bolt12Refund(Bolt12RefundContext), } +// Used when writing PaymentContext in Event::PaymentClaimable to avoid cloning. +pub(crate) enum PaymentContextRef<'a> { + Bolt12Offer(&'a Bolt12OfferContext), + Bolt12Refund(&'a Bolt12RefundContext), +} + /// An unknown payment context. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UnknownPaymentContext(()); @@ -372,6 +378,23 @@ impl_writeable_tlv_based_enum!(PaymentContext, (2, Bolt12Refund), ); +impl<'a> Writeable for PaymentContextRef<'a> { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + match self { + PaymentContextRef::Bolt12Offer(context) => { + 1u8.write(w)?; + context.write(w)?; + }, + PaymentContextRef::Bolt12Refund(context) => { + 2u8.write(w)?; + context.write(w)?; + }, + } + + Ok(()) + } +} + impl Writeable for UnknownPaymentContext { fn write(&self, _w: &mut W) -> Result<(), io::Error> { Ok(()) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 41ba17806d3..94df71b69ea 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -18,6 +18,7 @@ pub mod bump_transaction; pub use bump_transaction::BumpTransactionEvent; +use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext, PaymentContextRef}; use crate::sign::SpendableOutputDescriptor; use crate::ln::channelmanager::{InterceptId, PaymentId, RecipientOnionFields}; use crate::ln::channel::FUNDING_CONF_DEADLINE_BLOCKS; @@ -70,6 +71,46 @@ pub enum PaymentPurpose { /// [`ChannelManager::create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash payment_secret: PaymentSecret, }, + /// A payment for a BOLT 12 [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + Bolt12OfferPayment { + /// The preimage to the payment hash. If provided, this can be handed directly to + /// [`ChannelManager::claim_funds`]. + /// + /// [`ChannelManager::claim_funds`]: crate::ln::channelmanager::ChannelManager::claim_funds + payment_preimage: Option, + /// The secret used to authenticate the sender to the recipient, preventing a number of + /// de-anonymization attacks while routing a payment. + /// + /// See [`PaymentPurpose::Bolt11InvoicePayment::payment_secret`] for further details. + payment_secret: PaymentSecret, + /// The context of the payment such as information about the corresponding [`Offer`] and + /// [`InvoiceRequest`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + payment_context: Bolt12OfferContext, + }, + /// A payment for a BOLT 12 [`Refund`]. + /// + /// [`Refund`]: crate::offers::refund::Refund + Bolt12RefundPayment { + /// The preimage to the payment hash. If provided, this can be handed directly to + /// [`ChannelManager::claim_funds`]. + /// + /// [`ChannelManager::claim_funds`]: crate::ln::channelmanager::ChannelManager::claim_funds + payment_preimage: Option, + /// The secret used to authenticate the sender to the recipient, preventing a number of + /// de-anonymization attacks while routing a payment. + /// + /// See [`PaymentPurpose::Bolt11InvoicePayment::payment_secret`] for further details. + payment_secret: PaymentSecret, + /// The context of the payment such as information about the corresponding [`Refund`]. + /// + /// [`Refund`]: crate::offers::refund::Refund + payment_context: Bolt12RefundContext, + }, /// Because this is a spontaneous payment, the payer generated their own preimage rather than us /// (the payee) providing a preimage. SpontaneousPayment(PaymentPreimage), @@ -80,6 +121,8 @@ impl PaymentPurpose { pub fn preimage(&self) -> Option { match self { PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => *payment_preimage, + PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. } => *payment_preimage, + PaymentPurpose::Bolt12RefundPayment { payment_preimage, .. } => *payment_preimage, PaymentPurpose::SpontaneousPayment(preimage) => Some(*preimage), } } @@ -87,6 +130,8 @@ impl PaymentPurpose { pub(crate) fn is_keysend(&self) -> bool { match self { PaymentPurpose::Bolt11InvoicePayment { .. } => false, + PaymentPurpose::Bolt12OfferPayment { .. } => false, + PaymentPurpose::Bolt12RefundPayment { .. } => false, PaymentPurpose::SpontaneousPayment(..) => true, } } @@ -96,7 +141,18 @@ impl_writeable_tlv_based_enum!(PaymentPurpose, (0, Bolt11InvoicePayment) => { (0, payment_preimage, option), (2, payment_secret, required), - }; + }, + (4, Bolt12OfferPayment) => { + (0, payment_preimage, option), + (2, payment_secret, required), + (4, payment_context, required), + }, + (6, Bolt12RefundPayment) => { + (0, payment_preimage, option), + (2, payment_secret, required), + (4, payment_context, required), + }, + ; (2, SpontaneousPayment) ); @@ -1065,6 +1121,7 @@ impl Writeable for Event { 1u8.write(writer)?; let mut payment_secret = None; let payment_preimage; + let mut payment_context = None; match &purpose { PaymentPurpose::Bolt11InvoicePayment { payment_preimage: preimage, payment_secret: secret @@ -1072,6 +1129,20 @@ impl Writeable for Event { payment_secret = Some(secret); payment_preimage = *preimage; }, + PaymentPurpose::Bolt12OfferPayment { + payment_preimage: preimage, payment_secret: secret, payment_context: context + } => { + payment_secret = Some(secret); + payment_preimage = *preimage; + payment_context = Some(PaymentContextRef::Bolt12Offer(context)); + }, + PaymentPurpose::Bolt12RefundPayment { + payment_preimage: preimage, payment_secret: secret, payment_context: context + } => { + payment_secret = Some(secret); + payment_preimage = *preimage; + payment_context = Some(PaymentContextRef::Bolt12Refund(context)); + }, PaymentPurpose::SpontaneousPayment(preimage) => { payment_preimage = Some(*preimage); } @@ -1090,6 +1161,7 @@ impl Writeable for Event { (8, payment_preimage, option), (9, onion_fields, option), (10, skimmed_fee_opt, option), + (11, payment_context, option), }); }, &Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref fee_paid_msat } => { @@ -1320,6 +1392,7 @@ impl MaybeReadable for Event { let mut claim_deadline = None; let mut via_user_channel_id = None; let mut onion_fields = None; + let mut payment_context = None; read_tlv_fields!(reader, { (0, payment_hash, required), (1, receiver_node_id, option), @@ -1332,11 +1405,30 @@ impl MaybeReadable for Event { (8, payment_preimage, option), (9, onion_fields, option), (10, counterparty_skimmed_fee_msat_opt, option), + (11, payment_context, option), }); let purpose = match payment_secret { - Some(secret) => PaymentPurpose::Bolt11InvoicePayment { - payment_preimage, - payment_secret: secret + Some(secret) => match payment_context { + Some(PaymentContext::Unknown(_)) | None => { + PaymentPurpose::Bolt11InvoicePayment { + payment_preimage, + payment_secret: secret, + } + }, + Some(PaymentContext::Bolt12Offer(context)) => { + PaymentPurpose::Bolt12OfferPayment { + payment_preimage, + payment_secret: secret, + payment_context: context, + } + }, + Some(PaymentContext::Bolt12Refund(context)) => { + PaymentPurpose::Bolt12RefundPayment { + payment_preimage, + payment_secret: secret, + payment_context: context, + } + }, }, 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 0693f3c642e..4b120748ad8 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1477,6 +1477,8 @@ where /// println!("Claiming spontaneous payment {}", payment_hash); /// channel_manager.claim_funds(payment_preimage); /// }, +/// // ... +/// # _ => {}, /// }, /// Event::PaymentClaimed { payment_hash, amount_msat, .. } => { /// assert_eq!(payment_hash, known_payment_hash); @@ -8877,10 +8879,9 @@ where /// This differs from [`create_inbound_payment_for_hash`] only in that it generates the /// [`PaymentHash`] and [`PaymentPreimage`] for you. /// - /// The [`PaymentPreimage`] will ultimately be returned to you in the [`PaymentClaimable`], which - /// will have the [`PaymentClaimable::purpose`] be [`PaymentPurpose::Bolt11InvoicePayment`] with - /// its [`PaymentPurpose::Bolt11InvoicePayment::payment_preimage`] field filled in. That should - /// then be passed directly to [`claim_funds`]. + /// The [`PaymentPreimage`] will ultimately be returned to you in the [`PaymentClaimable`] event, which + /// will have the [`PaymentClaimable::purpose`] return `Some` for [`PaymentPurpose::preimage`]. That + /// should then be passed directly to [`claim_funds`]. /// /// See [`create_inbound_payment_for_hash`] for detailed documentation on behavior and requirements. /// @@ -8900,8 +8901,7 @@ where /// [`claim_funds`]: Self::claim_funds /// [`PaymentClaimable`]: events::Event::PaymentClaimable /// [`PaymentClaimable::purpose`]: events::Event::PaymentClaimable::purpose - /// [`PaymentPurpose::Bolt11InvoicePayment`]: events::PaymentPurpose::Bolt11InvoicePayment - /// [`PaymentPurpose::Bolt11InvoicePayment::payment_preimage`]: events::PaymentPurpose::Bolt11InvoicePayment::payment_preimage + /// [`PaymentPurpose::preimage`]: events::PaymentPurpose::preimage /// [`create_inbound_payment_for_hash`]: Self::create_inbound_payment_for_hash pub fn create_inbound_payment(&self, min_value_msat: Option, invoice_expiry_delta_secs: u32, min_final_cltv_expiry_delta: Option) -> Result<(PaymentHash, PaymentSecret), ()> { diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 35dbfd419a1..cf52d946e34 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -2133,6 +2133,14 @@ pub fn check_payment_claimable( assert_eq!(&expected_payment_preimage, payment_preimage); assert_eq!(expected_payment_secret, *payment_secret); }, + PaymentPurpose::Bolt12OfferPayment { payment_preimage, payment_secret, .. } => { + assert_eq!(&expected_payment_preimage, payment_preimage); + assert_eq!(expected_payment_secret, *payment_secret); + }, + PaymentPurpose::Bolt12RefundPayment { payment_preimage, payment_secret, .. } => { + assert_eq!(&expected_payment_preimage, payment_preimage); + assert_eq!(expected_payment_secret, *payment_secret); + }, _ => {}, } }, @@ -2611,6 +2619,16 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option assert_eq!(our_payment_secret.unwrap(), *payment_secret); 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); + assert_eq!(Some(*payment_secret), onion_fields.as_ref().unwrap().payment_secret); + }, + PaymentPurpose::Bolt12RefundPayment { payment_preimage, payment_secret, .. } => { + assert_eq!(expected_preimage, *payment_preimage); + assert_eq!(our_payment_secret.unwrap(), *payment_secret); + assert_eq!(Some(*payment_secret), onion_fields.as_ref().unwrap().payment_secret); + }, PaymentPurpose::SpontaneousPayment(payment_preimage) => { assert_eq!(expected_preimage.unwrap(), *payment_preimage); assert_eq!(our_payment_secret, onion_fields.as_ref().unwrap().payment_secret); @@ -2763,14 +2781,12 @@ pub fn pass_claimed_payment_along_route<'a, 'b, 'c, 'd>(args: ClaimAlongRouteArg let mut fwd_amt_msat = 0; match claim_event[0] { Event::PaymentClaimed { - purpose: PaymentPurpose::SpontaneousPayment(preimage), + purpose: PaymentPurpose::SpontaneousPayment(preimage) + | PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(preimage), .. } + | PaymentPurpose::Bolt12OfferPayment { payment_preimage: Some(preimage), .. } + | PaymentPurpose::Bolt12RefundPayment { payment_preimage: Some(preimage), .. }, amount_msat, ref htlcs, - .. } - | Event::PaymentClaimed { - purpose: PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(preimage), ..}, - ref htlcs, - amount_msat, .. } => { assert_eq!(preimage, our_payment_preimage); @@ -2780,7 +2796,9 @@ pub fn pass_claimed_payment_along_route<'a, 'b, 'c, 'd>(args: ClaimAlongRouteArg fwd_amt_msat = amount_msat; }, Event::PaymentClaimed { - purpose: PaymentPurpose::Bolt11InvoicePayment { .. }, + purpose: PaymentPurpose::Bolt11InvoicePayment { .. } + | PaymentPurpose::Bolt12OfferPayment { .. } + | PaymentPurpose::Bolt12RefundPayment { .. }, payment_hash, amount_msat, ref htlcs, From 0c52ea23dc6adfb9189f776449e78c828fa5d1f4 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sat, 13 Apr 2024 18:20:19 -0500 Subject: [PATCH 14/17] Use BOLT 12 PaymentPurpose variants When constructing a PaymentPurpose in ChannelManager, use the PaymentContext from OnionPayload to determine which variant to construct, including those for BOLT 12 payments. --- lightning/src/events/mod.rs | 51 +++++++++++++++++------------- lightning/src/ln/channelmanager.rs | 39 +++++++++++++---------- lightning/src/ln/offers_tests.rs | 13 ++++---- 3 files changed, 57 insertions(+), 46 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 94df71b69ea..2716532e099 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -135,6 +135,34 @@ impl PaymentPurpose { PaymentPurpose::SpontaneousPayment(..) => true, } } + + pub(crate) fn from_parts( + payment_preimage: Option, payment_secret: PaymentSecret, + payment_context: Option, + ) -> Self { + match payment_context { + Some(PaymentContext::Unknown(_)) | None => { + PaymentPurpose::Bolt11InvoicePayment { + payment_preimage, + payment_secret, + } + }, + Some(PaymentContext::Bolt12Offer(context)) => { + PaymentPurpose::Bolt12OfferPayment { + payment_preimage, + payment_secret, + payment_context: context, + } + }, + Some(PaymentContext::Bolt12Refund(context)) => { + PaymentPurpose::Bolt12RefundPayment { + payment_preimage, + payment_secret, + payment_context: context, + } + }, + } + } } impl_writeable_tlv_based_enum!(PaymentPurpose, @@ -1408,28 +1436,7 @@ impl MaybeReadable for Event { (11, payment_context, option), }); let purpose = match payment_secret { - Some(secret) => match payment_context { - Some(PaymentContext::Unknown(_)) | None => { - PaymentPurpose::Bolt11InvoicePayment { - payment_preimage, - payment_secret: secret, - } - }, - Some(PaymentContext::Bolt12Offer(context)) => { - PaymentPurpose::Bolt12OfferPayment { - payment_preimage, - payment_secret: secret, - payment_context: context, - } - }, - Some(PaymentContext::Bolt12Refund(context)) => { - PaymentPurpose::Bolt12RefundPayment { - payment_preimage, - payment_secret: secret, - payment_context: context, - } - }, - }, + Some(secret) => PaymentPurpose::from_parts(payment_preimage, secret, payment_context), 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 4b120748ad8..63fa82be63e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1566,11 +1566,11 @@ where /// // On the event processing thread /// channel_manager.process_pending_events(&|event| match event { /// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { -/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { +/// PaymentPurpose::Bolt12OfferPayment { payment_preimage: Some(payment_preimage), .. } => { /// println!("Claiming payment {}", payment_hash); /// channel_manager.claim_funds(payment_preimage); /// }, -/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: None, .. } => { +/// PaymentPurpose::Bolt12OfferPayment { payment_preimage: None, .. } => { /// println!("Unknown payment hash: {}", payment_hash); /// }, /// // ... @@ -1718,12 +1718,12 @@ where /// // On the event processing thread /// channel_manager.process_pending_events(&|event| match event { /// Event::PaymentClaimable { payment_hash, purpose, .. } => match purpose { -/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: Some(payment_preimage), .. } => { +/// PaymentPurpose::Bolt12RefundPayment { payment_preimage: Some(payment_preimage), .. } => { /// assert_eq!(payment_hash, known_payment_hash); /// println!("Claiming payment {}", payment_hash); /// channel_manager.claim_funds(payment_preimage); /// }, -/// PaymentPurpose::Bolt11InvoicePayment { payment_preimage: None, .. } => { +/// PaymentPurpose::Bolt12RefundPayment { payment_preimage: None, .. } => { /// println!("Unknown payment hash: {}", payment_hash); /// }, /// // ... @@ -5530,7 +5530,7 @@ where match payment_secrets.entry(payment_hash) { hash_map::Entry::Vacant(_) => { match claimable_htlc.onion_payload { - OnionPayload::Invoice { .. } => { + OnionPayload::Invoice { ref payment_context, .. } => { let payment_data = payment_data.unwrap(); let (payment_preimage, min_final_cltv_expiry_delta) = match inbound_payment::verify(payment_hash, &payment_data, self.highest_seen_timestamp.load(Ordering::Acquire) as u64, &self.inbound_payment_key, &self.logger) { Ok(result) => result, @@ -5547,10 +5547,11 @@ where fail_htlc!(claimable_htlc, payment_hash); } } - let purpose = events::PaymentPurpose::Bolt11InvoicePayment { - payment_preimage: payment_preimage.clone(), - payment_secret: payment_data.payment_secret, - }; + let purpose = events::PaymentPurpose::from_parts( + payment_preimage.clone(), + payment_data.payment_secret, + payment_context.clone(), + ); check_total_value!(purpose); }, OnionPayload::Spontaneous(preimage) => { @@ -5560,10 +5561,13 @@ where } }, hash_map::Entry::Occupied(inbound_payment) => { - if let OnionPayload::Spontaneous(_) = claimable_htlc.onion_payload { - log_trace!(self.logger, "Failing new keysend HTLC with payment_hash {} because we already have an inbound payment with the same payment hash", &payment_hash); - fail_htlc!(claimable_htlc, payment_hash); - } + let payment_context = match claimable_htlc.onion_payload { + OnionPayload::Spontaneous(_) => { + log_trace!(self.logger, "Failing new keysend HTLC with payment_hash {} because we already have an inbound payment with the same payment hash", &payment_hash); + fail_htlc!(claimable_htlc, payment_hash); + }, + OnionPayload::Invoice { ref payment_context, .. } => payment_context, + }; let payment_data = payment_data.unwrap(); if inbound_payment.get().payment_secret != payment_data.payment_secret { log_trace!(self.logger, "Failing new HTLC with payment_hash {} as it didn't match our expected payment secret.", &payment_hash); @@ -5573,10 +5577,11 @@ where &payment_hash, payment_data.total_msat, inbound_payment.get().min_value_msat.unwrap()); fail_htlc!(claimable_htlc, payment_hash); } else { - let purpose = events::PaymentPurpose::Bolt11InvoicePayment { - payment_preimage: inbound_payment.get().payment_preimage, - payment_secret: payment_data.payment_secret, - }; + let purpose = events::PaymentPurpose::from_parts( + inbound_payment.get().payment_preimage, + payment_data.payment_secret, + payment_context.clone(), + ); let payment_claimable_generated = check_total_value!(purpose); if payment_claimable_generated { inbound_payment.remove_entry(); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 6a0eb6cb3b1..0676f574fa3 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -43,7 +43,7 @@ use bitcoin::network::constants::Network; use core::time::Duration; use crate::blinded_path::{BlindedPath, IntroductionNode}; -use crate::events::{Event, MessageSendEventsProvider, PaymentPurpose}; +use crate::events::{Event, MessageSendEventsProvider}; use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, Retry, self}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; @@ -154,12 +154,11 @@ fn route_bolt12_payment<'a, 'b, 'c>( fn claim_bolt12_payment<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>]) { let recipient = &path[path.len() - 1]; match get_event!(recipient, Event::PaymentClaimable) { - Event::PaymentClaimable { - purpose: PaymentPurpose::Bolt11InvoicePayment { - payment_preimage: Some(payment_preimage), .. - }, .. - } => claim_payment(node, path, payment_preimage), - _ => panic!(), + Event::PaymentClaimable { purpose, .. } => match purpose.preimage() { + Some(payment_preimage) => claim_payment(node, path, payment_preimage), + None => panic!("No preimage in Event::PaymentClaimable"), + }, + _ => panic!("No Event::PaymentClaimable"), }; } From b06b2c787a1a9065872e0ad09a2e849b215d57f5 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 26 Mar 2024 20:19:32 -0500 Subject: [PATCH 15/17] Test for PaymentContext in offers_tests.rs --- lightning/src/ln/offers_tests.rs | 52 +++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 0676f574fa3..4910f38f311 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -43,7 +43,8 @@ use bitcoin::network::constants::Network; use core::time::Duration; use crate::blinded_path::{BlindedPath, IntroductionNode}; -use crate::events::{Event, MessageSendEventsProvider}; +use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; +use crate::events::{Event, MessageSendEventsProvider, PaymentPurpose}; use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, Retry, self}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; @@ -151,15 +152,28 @@ fn route_bolt12_payment<'a, 'b, 'c>( do_pass_along_path(args); } -fn claim_bolt12_payment<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>]) { +fn claim_bolt12_payment<'a, 'b, 'c>( + node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext +) { let recipient = &path[path.len() - 1]; - match get_event!(recipient, Event::PaymentClaimable) { - Event::PaymentClaimable { purpose, .. } => match purpose.preimage() { - Some(payment_preimage) => claim_payment(node, path, payment_preimage), - None => panic!("No preimage in Event::PaymentClaimable"), - }, + let payment_purpose = match get_event!(recipient, Event::PaymentClaimable) { + Event::PaymentClaimable { purpose, .. } => purpose, _ => panic!("No Event::PaymentClaimable"), }; + let payment_preimage = match payment_purpose.preimage() { + Some(preimage) => preimage, + None => panic!("No preimage in Event::PaymentClaimable"), + }; + match payment_purpose { + PaymentPurpose::Bolt12OfferPayment { payment_context, .. } => { + assert_eq!(PaymentContext::Bolt12Offer(payment_context), expected_payment_context); + }, + PaymentPurpose::Bolt12RefundPayment { payment_context, .. } => { + assert_eq!(PaymentContext::Bolt12Refund(payment_context), expected_payment_context); + }, + _ => panic!("Unexpected payment purpose: {:?}", payment_purpose), + } + claim_payment(node, path, payment_preimage); } fn extract_invoice_request<'a, 'b, 'c>( @@ -367,9 +381,11 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); let offer = alice.node - .create_offer_builder("coffee".to_string()).unwrap() + .create_offer_builder("coffee".to_string()) + .unwrap() .amount_msats(10_000_000) .build().unwrap(); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() }); assert_ne!(offer.signing_pubkey(), alice_id); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -413,7 +429,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { route_bolt12_payment(david, &[charlie, bob, alice], &invoice); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(david, &[charlie, bob, alice]); + claim_bolt12_payment(david, &[charlie, bob, alice], payment_context); expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id); } @@ -472,6 +488,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { } expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); connect_peers(alice, charlie); @@ -495,7 +512,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { route_bolt12_payment(david, &[charlie, bob, alice], &invoice); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(david, &[charlie, bob, alice]); + claim_bolt12_payment(david, &[charlie, bob, alice], payment_context); expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id); } @@ -520,6 +537,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { .create_offer_builder("coffee".to_string()).unwrap() .amount_msats(10_000_000) .build().unwrap(); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() }); assert_ne!(offer.signing_pubkey(), alice_id); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -552,7 +570,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice]); + claim_bolt12_payment(bob, &[alice], payment_context); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -590,6 +608,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -608,7 +627,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice]); + claim_bolt12_payment(bob, &[alice], payment_context); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -634,6 +653,7 @@ fn pays_for_offer_without_blinded_paths() { .clear_paths() .amount_msats(10_000_000) .build().unwrap(); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() }); assert_eq!(offer.signing_pubkey(), alice_id); assert!(offer.paths().is_empty()); @@ -651,7 +671,7 @@ fn pays_for_offer_without_blinded_paths() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice]); + claim_bolt12_payment(bob, &[alice], payment_context); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -684,6 +704,7 @@ fn pays_for_refund_without_blinded_paths() { assert!(refund.paths().is_empty()); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -695,7 +716,7 @@ fn pays_for_refund_without_blinded_paths() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice]); + claim_bolt12_payment(bob, &[alice], payment_context); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -1068,12 +1089,13 @@ fn fails_paying_invoice_more_than_once() { david.onion_messenger.handle_onion_message(&charlie_id, &onion_message); // David pays the first invoice + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let invoice1 = extract_invoice(david, &onion_message); route_bolt12_payment(david, &[charlie, bob, alice], &invoice1); expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(david, &[charlie, bob, alice]); + claim_bolt12_payment(david, &[charlie, bob, alice], payment_context); expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id); disconnect_peers(alice, &[charlie]); From 8367e021103493b811b9366e2355bd09e91b4836 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 28 Mar 2024 21:31:10 -0500 Subject: [PATCH 16/17] Include InvoiceRequest fields in PaymentContext When receiving a payment, it's useful to know information about the InvoiceRequest. Include this data in PaymentContext::Bolt12Offer so users can display information about an inbound payment (e.g., the payer note). --- lightning/src/blinded_path/payment.rs | 8 ++ lightning/src/ln/channelmanager.rs | 1 + lightning/src/ln/offers_tests.rs | 38 ++++++- lightning/src/offers/invoice_request.rs | 138 +++++++++++++++++++++++- 4 files changed, 176 insertions(+), 9 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index e243d478bd9..ec441c18c98 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -12,6 +12,7 @@ use crate::ln::channelmanager::CounterpartyForwardingInfo; use crate::ln::features::BlindedHopFeatures; use crate::ln::msgs::DecodeError; use crate::offers::invoice::BlindedPayInfo; +use crate::offers::invoice_request::InvoiceRequestFields; use crate::offers::offer::OfferId; use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer}; @@ -140,6 +141,12 @@ pub struct Bolt12OfferContext { /// /// [`Offer`]: crate::offers::offer::Offer pub offer_id: OfferId, + + /// Fields from an [`InvoiceRequest`] sent for a [`Bolt12Invoice`]. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + pub invoice_request: InvoiceRequestFields, } /// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`]. @@ -409,6 +416,7 @@ impl Readable for UnknownPaymentContext { impl_writeable_tlv_based!(Bolt12OfferContext, { (0, offer_id, required), + (2, invoice_request, required), }); impl_writeable_tlv_based!(Bolt12RefundContext, {}); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 63fa82be63e..8bf08c1e023 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10367,6 +10367,7 @@ where let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: invoice_request.offer_id, + invoice_request: invoice_request.fields(), }); let payment_paths = match self.create_blinded_payment_paths( amount_msats, payment_secret, payment_context diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 4910f38f311..75a2e290f39 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -46,11 +46,12 @@ use crate::blinded_path::{BlindedPath, IntroductionNode}; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::events::{Event, MessageSendEventsProvider, PaymentPurpose}; use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, Retry, self}; +use crate::ln::features::InvoiceRequestFeatures; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_error::InvoiceError; -use crate::offers::invoice_request::InvoiceRequest; +use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; use crate::offers::parse::Bolt12SemanticError; use crate::onion_message::messenger::PeeledOnion; use crate::onion_message::offers::OffersMessage; @@ -385,7 +386,6 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { .unwrap() .amount_msats(10_000_000) .build().unwrap(); - let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() }); assert_ne!(offer.signing_pubkey(), alice_id); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -408,6 +408,16 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { alice.onion_messenger.handle_onion_message(&bob_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_id: invoice_request.payer_id(), + amount_msats: None, + features: InvoiceRequestFeatures::empty(), + quantity: None, + payer_note_truncated: None, + }, + }); assert_eq!(invoice_request.amount_msats(), None); assert_ne!(invoice_request.payer_id(), david_id); assert_eq!(reply_path.unwrap().introduction_node, IntroductionNode::NodeId(charlie_id)); @@ -537,7 +547,6 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { .create_offer_builder("coffee".to_string()).unwrap() .amount_msats(10_000_000) .build().unwrap(); - let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() }); assert_ne!(offer.signing_pubkey(), alice_id); assert!(!offer.paths().is_empty()); for path in offer.paths() { @@ -552,6 +561,16 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { alice.onion_messenger.handle_onion_message(&bob_id, &onion_message); let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_id: invoice_request.payer_id(), + amount_msats: None, + features: InvoiceRequestFeatures::empty(), + quantity: None, + payer_note_truncated: None, + }, + }); assert_eq!(invoice_request.amount_msats(), None); assert_ne!(invoice_request.payer_id(), bob_id); assert_eq!(reply_path.unwrap().introduction_node, IntroductionNode::NodeId(bob_id)); @@ -653,7 +672,6 @@ fn pays_for_offer_without_blinded_paths() { .clear_paths() .amount_msats(10_000_000) .build().unwrap(); - let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: offer.id() }); assert_eq!(offer.signing_pubkey(), alice_id); assert!(offer.paths().is_empty()); @@ -664,6 +682,18 @@ fn pays_for_offer_without_blinded_paths() { let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); alice.onion_messenger.handle_onion_message(&bob_id, &onion_message); + let (invoice_request, _) = extract_invoice_request(alice, &onion_message); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_id: invoice_request.payer_id(), + amount_msats: None, + features: InvoiceRequestFeatures::empty(), + quantity: None, + payer_note_truncated: None, + }, + }); + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); bob.onion_messenger.handle_onion_message(&alice_id, &onion_message); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 07a117233fa..9157613fcd9 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -76,8 +76,8 @@ use crate::offers::offer::{Offer, OfferContents, OfferId, OfferTlvStream, OfferT use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::signer::{Metadata, MetadataMaterial}; -use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; -use crate::util::string::PrintableString; +use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, SeekReadable, WithoutLength, Writeable, Writer}; +use crate::util::string::{PrintableString, UntrustedString}; #[cfg(not(c_bindings))] use { @@ -872,6 +872,24 @@ impl VerifiedInvoiceRequest { invoice_request_respond_with_derived_signing_pubkey_methods!(self, self.inner, InvoiceBuilder); #[cfg(c_bindings)] invoice_request_respond_with_derived_signing_pubkey_methods!(self, self.inner, InvoiceWithDerivedSigningPubkeyBuilder); + + pub(crate) fn fields(&self) -> InvoiceRequestFields { + let InvoiceRequestContents { + payer_id, + inner: InvoiceRequestContentsWithoutPayerId { + payer: _, offer: _, chain: _, amount_msats, features, quantity, payer_note + }, + } = &self.inner.contents; + + InvoiceRequestFields { + payer_id: *payer_id, + amount_msats: *amount_msats, + features: features.clone(), + quantity: *quantity, + payer_note_truncated: payer_note.clone() + .map(|mut s| { s.truncate(PAYER_NOTE_LIMIT); UntrustedString(s) }), + } + } } impl InvoiceRequestContents { @@ -1100,9 +1118,68 @@ impl TryFrom for InvoiceRequestContents { } } +/// Fields sent in an [`InvoiceRequest`] message to include in [`PaymentContext::Bolt12Offer`]. +/// +/// [`PaymentContext::Bolt12Offer`]: crate::blinded_path::payment::PaymentContext::Bolt12Offer +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InvoiceRequestFields { + /// A possibly transient pubkey used to sign the invoice request. + pub payer_id: PublicKey, + + /// The amount to pay in msats (i.e., the minimum lightning-payable unit for [`chain`]), which + /// must be greater than or equal to [`Offer::amount`], converted if necessary. + /// + /// [`chain`]: InvoiceRequest::chain + pub amount_msats: Option, + + /// Features pertaining to requesting an invoice. + pub features: InvoiceRequestFeatures, + + /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`]. + pub quantity: Option, + + /// A payer-provided note which will be seen by the recipient and reflected back in the invoice + /// response. Truncated to [`PAYER_NOTE_LIMIT`] characters. + pub payer_note_truncated: Option, +} + +/// The maximum number of characters included in [`InvoiceRequestFields::payer_note_truncated`]. +pub const PAYER_NOTE_LIMIT: usize = 512; + +impl Writeable for InvoiceRequestFields { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + write_tlv_fields!(writer, { + (0, self.payer_id, required), + (2, self.amount_msats.map(|v| HighZeroBytesDroppedBigSize(v)), option), + (4, WithoutLength(&self.features), required), + (6, self.quantity.map(|v| HighZeroBytesDroppedBigSize(v)), option), + (8, self.payer_note_truncated.as_ref().map(|s| WithoutLength(&s.0)), option), + }); + Ok(()) + } +} + +impl Readable for InvoiceRequestFields { + fn read(reader: &mut R) -> Result { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, payer_id, required), + (2, amount_msats, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), + (4, features, (option, encoding: (InvoiceRequestFeatures, WithoutLength))), + (6, quantity, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), + (8, payer_note_truncated, (option, encoding: (String, WithoutLength))), + }); + let features = features.unwrap_or(InvoiceRequestFeatures::empty()); + + Ok(InvoiceRequestFields { + payer_id: payer_id.0.unwrap(), amount_msats, features, quantity, + payer_note_truncated: payer_note_truncated.map(|s| UntrustedString(s)), + }) + } +} + #[cfg(test)] mod tests { - use super::{InvoiceRequest, InvoiceRequestTlvStreamRef, SIGNATURE_TAG, UnsignedInvoiceRequest}; + use super::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest}; use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; @@ -1129,8 +1206,8 @@ mod tests { use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::test_utils::*; - use crate::util::ser::{BigSize, Writeable}; - use crate::util::string::PrintableString; + use crate::util::ser::{BigSize, Readable, Writeable}; + use crate::util::string::{PrintableString, UntrustedString}; #[test] fn builds_invoice_request_with_defaults() { @@ -2166,4 +2243,55 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), } } + + #[test] + fn copies_verified_invoice_request_fields() { + let desc = "foo".to_string(); + let node_id = recipient_pubkey(); + let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); + let entropy = FixedEntropy {}; + let secp_ctx = Secp256k1::new(); + + #[cfg(c_bindings)] + use crate::offers::offer::OfferWithDerivedMetadataBuilder as OfferBuilder; + let offer = OfferBuilder + ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + .chain(Network::Testnet) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap(); + assert_eq!(offer.signing_pubkey(), node_id); + + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .chain(Network::Testnet).unwrap() + .amount_msats(1001).unwrap() + .quantity(1).unwrap() + .payer_note("0".repeat(PAYER_NOTE_LIMIT * 2)) + .build().unwrap() + .sign(payer_sign).unwrap(); + match invoice_request.verify(&expanded_key, &secp_ctx) { + Ok(invoice_request) => { + let fields = invoice_request.fields(); + assert_eq!(invoice_request.offer_id, offer.id()); + assert_eq!( + fields, + InvoiceRequestFields { + payer_id: payer_pubkey(), + amount_msats: Some(1001), + features: InvoiceRequestFeatures::empty(), + quantity: Some(1), + payer_note_truncated: Some(UntrustedString("0".repeat(PAYER_NOTE_LIMIT))), + } + ); + + let mut buffer = Vec::new(); + fields.write(&mut buffer).unwrap(); + + let deserialized_fields: InvoiceRequestFields = + Readable::read(&mut buffer.as_slice()).unwrap(); + assert_eq!(deserialized_fields, fields); + }, + Err(_) => panic!("unexpected error"), + } + } } From 478911d42a8b47020511bdcb793756723f3801be Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 17 Apr 2024 16:45:45 -0500 Subject: [PATCH 17/17] Update PaymentPurpose's payment_preimage docs Clarify that payment_preimage should only be used to claim a payment when handling Event::PaymentClaimable, not Event::PaymentClaimed. --- lightning/src/events/mod.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 2716532e099..d34db885f7c 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -53,8 +53,9 @@ pub enum PaymentPurpose { /// A payment for a BOLT 11 invoice. Bolt11InvoicePayment { /// The preimage to the payment_hash, if the payment hash (and secret) were fetched via - /// [`ChannelManager::create_inbound_payment`]. If provided, this can be handed directly to - /// [`ChannelManager::claim_funds`]. + /// [`ChannelManager::create_inbound_payment`]. When handling [`Event::PaymentClaimable`], + /// this can be passed directly to [`ChannelManager::claim_funds`] to claim the payment. No + /// action is needed when seen in [`Event::PaymentClaimed`]. /// /// [`ChannelManager::create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment /// [`ChannelManager::claim_funds`]: crate::ln::channelmanager::ChannelManager::claim_funds @@ -75,8 +76,9 @@ pub enum PaymentPurpose { /// /// [`Offer`]: crate::offers::offer::Offer Bolt12OfferPayment { - /// The preimage to the payment hash. If provided, this can be handed directly to - /// [`ChannelManager::claim_funds`]. + /// The preimage to the payment hash. When handling [`Event::PaymentClaimable`], this can be + /// passed directly to [`ChannelManager::claim_funds`], if provided. No action is needed + /// when seen in [`Event::PaymentClaimed`]. /// /// [`ChannelManager::claim_funds`]: crate::ln::channelmanager::ChannelManager::claim_funds payment_preimage: Option, @@ -96,8 +98,9 @@ pub enum PaymentPurpose { /// /// [`Refund`]: crate::offers::refund::Refund Bolt12RefundPayment { - /// The preimage to the payment hash. If provided, this can be handed directly to - /// [`ChannelManager::claim_funds`]. + /// The preimage to the payment hash. When handling [`Event::PaymentClaimable`], this can be + /// passed directly to [`ChannelManager::claim_funds`], if provided. No action is needed + /// when seen in [`Event::PaymentClaimed`]. /// /// [`ChannelManager::claim_funds`]: crate::ln::channelmanager::ChannelManager::claim_funds payment_preimage: Option,