From e91090affc74655f9cf24baad7db24813e2393bd Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 27 Mar 2026 11:24:21 +0000 Subject: [PATCH 1/3] Refer to payment info as `info` in `inbound_payment` not `metadata` `payment_metadata` is a separate concept at the BOLT 11 layer (similar to payment secret, but arbitrary-sized) and at the BOLT 12 layer, so referring to payment information as "payment metadata" is confusing. Instead, use simply "payment info". --- lightning/src/ln/inbound_payment.rs | 127 ++++++++++++++-------------- 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index a7597701768..b52518584f7 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -28,10 +28,10 @@ use crate::util::logger::Logger; use crate::prelude::*; pub(crate) const IV_LEN: usize = 16; -const METADATA_LEN: usize = 16; -const METADATA_KEY_LEN: usize = 32; +const INFO_LEN: usize = 16; +const INFO_KEY_LEN: usize = 32; const AMT_MSAT_LEN: usize = 8; -// Used to shift the payment type bits to take up the top 3 bits of the metadata bytes, or to +// Used to shift the payment type bits to take up the top 3 bits of the info bytes, or to // retrieve said payment type bits. const METHOD_TYPE_OFFSET: usize = 5; @@ -40,20 +40,20 @@ const METHOD_TYPE_OFFSET: usize = 5; /// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key #[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)] pub struct ExpandedKey { - /// The key used to encrypt the bytes containing the payment metadata (i.e. the amount and + /// The key used to encrypt the bytes containing the payment info (i.e. the amount and /// expiry, included for payment verification on decryption). - metadata_key: [u8; 32], - /// The key used to authenticate an LDK-provided payment hash and metadata as previously + info_key: [u8; 32], + /// The key used to authenticate an LDK-provided payment hash and info as previously /// registered with LDK. ldk_pmt_hash_key: [u8; 32], - /// The key used to authenticate a user-provided payment hash and metadata as previously + /// The key used to authenticate a user-provided payment hash and info as previously /// registered with LDK. user_pmt_hash_key: [u8; 32], /// The base key used to derive signing keys and authenticate messages for BOLT 12 Offers. offers_base_key: [u8; 32], /// The key used to encrypt message metadata for BOLT 12 Offers. offers_encryption_key: [u8; 32], - /// The key used to authenticate spontaneous payments' metadata as previously registered with LDK + /// The key used to authenticate spontaneous payments' info as previously registered with LDK /// for inclusion in a blinded path. spontaneous_pmt_key: [u8; 32], /// The key used to authenticate phantom-node-shared blinded paths as generated by us. Note @@ -68,7 +68,7 @@ impl ExpandedKey { /// It is recommended to cache this value and not regenerate it for each new inbound payment. pub fn new(key_material: [u8; 32]) -> ExpandedKey { let ( - metadata_key, + info_key, ldk_pmt_hash_key, user_pmt_hash_key, offers_base_key, @@ -77,7 +77,7 @@ impl ExpandedKey { phantom_node_blinded_path_key, ) = hkdf_extract_expand_7x(b"LDK Inbound Payment Key Expansion", &key_material); Self { - metadata_key, + info_key, ldk_pmt_hash_key, user_pmt_hash_key, offers_base_key, @@ -133,7 +133,7 @@ impl Method { } } -fn min_final_cltv_expiry_delta_from_metadata(bytes: [u8; METADATA_LEN]) -> u16 { +fn min_final_cltv_expiry_delta_from_info(bytes: [u8; INFO_LEN]) -> u16 { let expiry_bytes = &bytes[AMT_MSAT_LEN..]; u16::from_be_bytes([expiry_bytes[0], expiry_bytes[1]]) } @@ -156,7 +156,7 @@ pub fn create( keys: &ExpandedKey, min_value_msat: Option, invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64, min_final_cltv_expiry_delta: Option, ) -> Result<(PaymentHash, PaymentSecret), ()> { - let metadata_bytes = construct_metadata_bytes( + let info_bytes = construct_info_bytes( min_value_msat, if min_final_cltv_expiry_delta.is_some() { Method::LdkPaymentHashCustomFinalCltv @@ -174,11 +174,11 @@ pub fn create( let mut hmac = HmacEngine::::new(&keys.ldk_pmt_hash_key); hmac.input(&iv_bytes); - hmac.input(&metadata_bytes); + hmac.input(&info_bytes); let payment_preimage_bytes = Hmac::from_engine(hmac).to_byte_array(); let ldk_pmt_hash = PaymentHash(Sha256::hash(&payment_preimage_bytes).to_byte_array()); - let payment_secret = construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key); + let payment_secret = construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key); Ok((ldk_pmt_hash, payment_secret)) } @@ -196,7 +196,7 @@ pub fn create_from_hash( keys: &ExpandedKey, min_value_msat: Option, payment_hash: PaymentHash, invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option, ) -> Result { - let metadata_bytes = construct_metadata_bytes( + let info_bytes = construct_info_bytes( min_value_msat, if min_final_cltv_expiry_delta.is_some() { Method::UserPaymentHashCustomFinalCltv @@ -209,21 +209,21 @@ pub fn create_from_hash( )?; let mut hmac = HmacEngine::::new(&keys.user_pmt_hash_key); - hmac.input(&metadata_bytes); + hmac.input(&info_bytes); hmac.input(&payment_hash.0); let hmac_bytes = Hmac::from_engine(hmac).to_byte_array(); let mut iv_bytes = [0 as u8; IV_LEN]; iv_bytes.copy_from_slice(&hmac_bytes[..IV_LEN]); - Ok(construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key)) + Ok(construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key)) } pub(crate) fn create_for_spontaneous_payment( keys: &ExpandedKey, min_value_msat: Option, invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option, ) -> Result { - let metadata_bytes = construct_metadata_bytes( + let info_bytes = construct_info_bytes( min_value_msat, Method::SpontaneousPayment, invoice_expiry_delta_secs, @@ -232,13 +232,13 @@ pub(crate) fn create_for_spontaneous_payment( )?; let mut hmac = HmacEngine::::new(&keys.spontaneous_pmt_key); - hmac.input(&metadata_bytes); + hmac.input(&info_bytes); let hmac_bytes = Hmac::from_engine(hmac).to_byte_array(); let mut iv_bytes = [0 as u8; IV_LEN]; iv_bytes.copy_from_slice(&hmac_bytes[..IV_LEN]); - Ok(construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key)) + Ok(construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key)) } pub(crate) fn calculate_absolute_expiry( @@ -252,10 +252,10 @@ pub(crate) fn calculate_absolute_expiry( highest_seen_timestamp + invoice_expiry_delta_secs as u64 + 7200 } -fn construct_metadata_bytes( +fn construct_info_bytes( min_value_msat: Option, payment_type: Method, invoice_expiry_delta_secs: u32, highest_seen_timestamp: u64, min_final_cltv_expiry_delta: Option, -) -> Result<[u8; METADATA_LEN], ()> { +) -> Result<[u8; INFO_LEN], ()> { if min_value_msat.is_some() && min_value_msat.unwrap() > MAX_VALUE_MSAT { return Err(()); } @@ -290,29 +290,28 @@ fn construct_metadata_bytes( expiry_bytes[1] |= bytes[1]; } - let mut metadata_bytes: [u8; METADATA_LEN] = [0; METADATA_LEN]; + let mut info_bytes: [u8; INFO_LEN] = [0; INFO_LEN]; - metadata_bytes[..AMT_MSAT_LEN].copy_from_slice(&min_amt_msat_bytes); - metadata_bytes[AMT_MSAT_LEN..].copy_from_slice(&expiry_bytes); + info_bytes[..AMT_MSAT_LEN].copy_from_slice(&min_amt_msat_bytes); + info_bytes[AMT_MSAT_LEN..].copy_from_slice(&expiry_bytes); - Ok(metadata_bytes) + Ok(info_bytes) } fn construct_payment_secret( - iv_bytes: &[u8; IV_LEN], metadata_bytes: &[u8; METADATA_LEN], - metadata_key: &[u8; METADATA_KEY_LEN], + iv_bytes: &[u8; IV_LEN], info_bytes: &[u8; INFO_LEN], info_key: &[u8; INFO_KEY_LEN], ) -> PaymentSecret { let mut payment_secret_bytes: [u8; 32] = [0; 32]; - let (iv_slice, encrypted_metadata_slice) = payment_secret_bytes.split_at_mut(IV_LEN); + let (iv_slice, encrypted_info_slice) = payment_secret_bytes.split_at_mut(IV_LEN); iv_slice.copy_from_slice(iv_bytes); - encrypted_metadata_slice.copy_from_slice(metadata_bytes); + encrypted_info_slice.copy_from_slice(info_bytes); ChaCha20::new_from_block( - Key::new(*metadata_key), + Key::new(*info_key), Nonce::new(iv_bytes[4..].try_into().unwrap()), u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), ) - .apply_keystream(encrypted_metadata_slice); + .apply_keystream(encrypted_info_slice); PaymentSecret(payment_secret_bytes) } @@ -320,13 +319,13 @@ fn construct_payment_secret( /// Check that an inbound payment's `payment_data` field is sane. /// /// LDK does not store any data for pending inbound payments. Instead, we construct our payment -/// secret (and, if supplied by LDK, our payment preimage) to include encrypted metadata about the -/// payment. +/// secret (and, if supplied by LDK, our payment preimage) to include encrypted information about +/// the payment. /// -/// For payments without a custom `min_final_cltv_expiry_delta`, the metadata is constructed as: +/// For payments without a custom `min_final_cltv_expiry_delta`, the payment info is: /// payment method (3 bits) || payment amount (8 bytes - 3 bits) || expiry (8 bytes) /// -/// For payments including a custom `min_final_cltv_expiry_delta`, the metadata is constructed as: +/// For payments including a custom `min_final_cltv_expiry_delta`, the payment info is: /// payment method (3 bits) || payment amount (8 bytes - 3 bits) || min_final_cltv_expiry_delta (2 bytes) || expiry (6 bytes) /// /// In both cases the result is then encrypted using a key derived from [`NodeSigner::get_expanded_key`]. @@ -339,14 +338,14 @@ fn construct_payment_secret( /// method is called, then the payment method bits mentioned above are represented internally as /// [`Method::LdkPaymentHash`]. If the latter, [`Method::UserPaymentHash`]. /// -/// For the former method, the payment preimage is constructed as an HMAC of payment metadata and -/// random bytes. Because the payment secret is also encoded with these random bytes and metadata -/// (with the metadata encrypted with a block cipher), we're able to authenticate the preimage on +/// For the former method, the payment preimage is constructed as an HMAC of payment info and +/// random bytes. Because the payment secret is also encoded with these random bytes and info +/// (with the info encrypted with a block cipher), we're able to authenticate the preimage on /// payment receipt. /// /// For the latter, the payment secret instead contains an HMAC of the user-provided payment hash -/// and payment metadata (encrypted with a block cipher), allowing us to authenticate the payment -/// hash and metadata on payment receipt. +/// and payment info (encrypted with a block cipher), allowing us to authenticate the payment +/// hash and info on payment receipt. /// /// See [`ExpandedKey`] docs for more info on the individual keys used. /// @@ -357,14 +356,13 @@ pub(super) fn verify( payment_hash: PaymentHash, payment_data: &msgs::FinalOnionHopData, highest_seen_timestamp: u64, keys: &ExpandedKey, logger: &L, ) -> Result<(Option, Option), ()> { - let (iv_bytes, metadata_bytes) = decrypt_metadata(payment_data.payment_secret, keys); + let (iv_bytes, info_bytes) = decrypt_info(payment_data.payment_secret, keys); - let payment_type_res = - Method::from_bits((metadata_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET); + let payment_type_res = Method::from_bits((info_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET); let mut amt_msat_bytes = [0; AMT_MSAT_LEN]; - let mut expiry_bytes = [0; METADATA_LEN - AMT_MSAT_LEN]; - amt_msat_bytes.copy_from_slice(&metadata_bytes[..AMT_MSAT_LEN]); - expiry_bytes.copy_from_slice(&metadata_bytes[AMT_MSAT_LEN..]); + let mut expiry_bytes = [0; INFO_LEN - AMT_MSAT_LEN]; + amt_msat_bytes.copy_from_slice(&info_bytes[..AMT_MSAT_LEN]); + expiry_bytes.copy_from_slice(&info_bytes[AMT_MSAT_LEN..]); // Zero out the bits reserved to indicate the payment type. amt_msat_bytes[0] &= 0b00011111; let mut min_final_cltv_expiry_delta = None; @@ -375,7 +373,7 @@ pub(super) fn verify( match payment_type_res { Ok(Method::UserPaymentHash) | Ok(Method::UserPaymentHashCustomFinalCltv) => { let mut hmac = HmacEngine::::new(&keys.user_pmt_hash_key); - hmac.input(&metadata_bytes[..]); + hmac.input(&info_bytes[..]); hmac.input(&payment_hash.0); if !fixed_time_eq( &iv_bytes, @@ -390,7 +388,7 @@ pub(super) fn verify( } }, Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - match derive_ldk_payment_preimage(payment_hash, &iv_bytes, &metadata_bytes, keys) { + match derive_ldk_payment_preimage(payment_hash, &iv_bytes, &info_bytes, keys) { Ok(preimage) => payment_preimage = Some(preimage), Err(bad_preimage_bytes) => { log_trace!( @@ -405,7 +403,7 @@ pub(super) fn verify( }, Ok(Method::SpontaneousPayment) => { let mut hmac = HmacEngine::::new(&keys.spontaneous_pmt_key); - hmac.input(&metadata_bytes[..]); + hmac.input(&info_bytes[..]); if !fixed_time_eq( &iv_bytes, &Hmac::from_engine(hmac).to_byte_array().split_at_mut(IV_LEN).0, @@ -427,8 +425,7 @@ pub(super) fn verify( match payment_type_res { Ok(Method::UserPaymentHashCustomFinalCltv) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - min_final_cltv_expiry_delta = - Some(min_final_cltv_expiry_delta_from_metadata(metadata_bytes)); + min_final_cltv_expiry_delta = Some(min_final_cltv_expiry_delta_from_info(info_bytes)); // Zero out first two bytes of expiry reserved for `min_final_cltv_expiry_delta`. expiry_bytes[0] &= 0; expiry_bytes[1] &= 0; @@ -455,11 +452,11 @@ pub(super) fn verify( pub(super) fn get_payment_preimage( payment_hash: PaymentHash, payment_secret: PaymentSecret, keys: &ExpandedKey, ) -> Result { - let (iv_bytes, metadata_bytes) = decrypt_metadata(payment_secret, keys); + let (iv_bytes, info_bytes) = decrypt_info(payment_secret, keys); - match Method::from_bits((metadata_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET) { + match Method::from_bits((info_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET) { Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - derive_ldk_payment_preimage(payment_hash, &iv_bytes, &metadata_bytes, keys).map_err( + derive_ldk_payment_preimage(payment_hash, &iv_bytes, &info_bytes, keys).map_err( |bad_preimage_bytes| APIError::APIMisuseError { err: format!( "Payment hash {} did not match decoded preimage {}", @@ -484,34 +481,34 @@ pub(super) fn get_payment_preimage( } } -fn decrypt_metadata( +fn decrypt_info( payment_secret: PaymentSecret, keys: &ExpandedKey, -) -> ([u8; IV_LEN], [u8; METADATA_LEN]) { +) -> ([u8; IV_LEN], [u8; INFO_LEN]) { let mut iv_bytes = [0; IV_LEN]; - let (iv_slice, encrypted_metadata_bytes) = payment_secret.0.split_at(IV_LEN); + let (iv_slice, encrypted_info_bytes) = payment_secret.0.split_at(IV_LEN); iv_bytes.copy_from_slice(iv_slice); - let mut metadata_bytes: [u8; METADATA_LEN] = [0; METADATA_LEN]; - metadata_bytes.copy_from_slice(encrypted_metadata_bytes); + let mut info_bytes: [u8; INFO_LEN] = [0; INFO_LEN]; + info_bytes.copy_from_slice(encrypted_info_bytes); ChaCha20::new_from_block( - Key::new(keys.metadata_key), + Key::new(keys.info_key), Nonce::new(iv_bytes[4..].try_into().unwrap()), u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()), ) - .apply_keystream(&mut metadata_bytes); + .apply_keystream(&mut info_bytes); - (iv_bytes, metadata_bytes) + (iv_bytes, info_bytes) } // Errors if the payment preimage doesn't match `payment_hash`. Returns the bad preimage bytes in // this case. fn derive_ldk_payment_preimage( - payment_hash: PaymentHash, iv_bytes: &[u8; IV_LEN], metadata_bytes: &[u8; METADATA_LEN], + payment_hash: PaymentHash, iv_bytes: &[u8; IV_LEN], info_bytes: &[u8; INFO_LEN], keys: &ExpandedKey, ) -> Result { let mut hmac = HmacEngine::::new(&keys.ldk_pmt_hash_key); hmac.input(iv_bytes); - hmac.input(metadata_bytes); + hmac.input(info_bytes); let decoded_payment_preimage = Hmac::from_engine(hmac).to_byte_array(); if !fixed_time_eq(&payment_hash.0, &Sha256::hash(&decoded_payment_preimage).to_byte_array()) { return Err(decoded_payment_preimage); From 657ac8f58e51af74c610375cb65cdad6f7a18c6b Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 11 May 2026 00:08:11 +0000 Subject: [PATCH 2/3] Commit to payment_metadata in inbound payment HMAC When payment_metadata is set in a BOLT 11 invoice, users expect to receive it back as-is in the payment onion. In order to ensure it isn't tampered with, they presumably will add an HMAC, or worse, not add one and forget that it can be tampered with. Instead, here we include it in the HMAC computation for the payment secret. This ensures that the sender must relay the correct metadata for the payment to be accepted by the receiver, binding the metadata to the payment cryptographically. The metadata is only included in the HMAC when present, so existing payments without metadata continue to verify correctly. However, this does break receiving payments with metadata today. On an upgrade this seems acceptable to me given we have seen almost no use of payment metadata in practice. Co-Authored-By: Claude Opus 4.6 (1M context) --- fuzz/src/chanmon_consistency.rs | 2 +- fuzz/src/full_stack.rs | 5 +- .../tests/lsps2_integration_tests.rs | 2 +- lightning/src/ln/bolt11_payment_tests.rs | 4 +- lightning/src/ln/channelmanager.rs | 49 ++++++++++++---- lightning/src/ln/functional_test_utils.rs | 1 + lightning/src/ln/functional_tests.rs | 38 ++++++++----- lightning/src/ln/inbound_payment.rs | 56 ++++++++++++++----- lightning/src/ln/invoice_utils.rs | 9 ++- .../src/ln/max_payment_path_len_tests.rs | 44 +++++++++++++-- lightning/src/ln/payment_tests.rs | 30 +++++++--- pending_changelog/matt-commit-to-metadata.txt | 6 ++ 12 files changed, 183 insertions(+), 63 deletions(-) create mode 100644 pending_changelog/matt-commit-to-metadata.txt diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 8a90dc93e97..2667b7359b2 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -1370,7 +1370,7 @@ impl PaymentTracker { payment_preimage.0[0..8].copy_from_slice(&self.payment_ctr.to_be_bytes()); let hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); let secret = dest - .create_inbound_payment_for_hash(hash, None, 3600, None) + .create_inbound_payment_for_hash(hash, None, 3600, None, None) .expect("create_inbound_payment_for_hash failed"); assert!(self.payment_preimages.insert(hash, payment_preimage).is_none()); let mut id = PaymentId([0; 32]); diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index e79bef7c5ec..58509bb9b08 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -837,11 +837,10 @@ pub fn do_test(mut data: &[u8], logger: &Arc }, 16 => { let payment_preimage = PaymentPreimage(keys_manager.get_secure_random_bytes()); - let payment_hash = - PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); + let hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); // Note that this may fail - our hashes may collide and we'll end up trying to // double-register the same payment_hash. - let _ = channelmanager.create_inbound_payment_for_hash(payment_hash, None, 1, None); + let _ = channelmanager.create_inbound_payment_for_hash(hash, None, 1, None, None); }, 9 => { for payment in payments_received.drain(..) { diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index fbff2eae4cd..92e6b33ebb6 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -122,7 +122,7 @@ fn create_jit_invoice( let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; let (payment_hash, payment_secret) = node .node - .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) + .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta), None) .map_err(|e| { log_error!(node.logger, "Failed to register inbound payment: {:?}", e); })?; diff --git a/lightning/src/ln/bolt11_payment_tests.rs b/lightning/src/ln/bolt11_payment_tests.rs index 8c2ac155ce7..733e26d0f1b 100644 --- a/lightning/src/ln/bolt11_payment_tests.rs +++ b/lightning/src/ln/bolt11_payment_tests.rs @@ -31,7 +31,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42]; let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(None, 7200, None).unwrap(); + nodes[1].node.create_inbound_payment(None, 7200, None, Some(&payment_metadata)).unwrap(); let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let invoice = InvoiceBuilder::new(Currency::Bitcoin) @@ -98,7 +98,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42]; let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(None, 7200, None).unwrap(); + nodes[1].node.create_inbound_payment(None, 7200, None, Some(&payment_metadata)).unwrap(); let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let invoice = InvoiceBuilder::new(Currency::Bitcoin) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9920be84e6b..a05d620274e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8595,6 +8595,7 @@ impl< let verify_res = inbound_payment::verify( payment_hash, &payment_data, + onion_fields.payment_metadata.as_deref(), self.highest_seen_timestamp.load(Ordering::Acquire) as u64, &self.inbound_payment_key, &self.logger, @@ -14261,7 +14262,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) -> Result> { let Bolt11InvoiceParameters { amount_msats, description, invoice_expiry_delta_secs, min_final_cltv_expiry_delta, - payment_hash, + payment_hash, payment_metadata, } = params; let currency = @@ -14294,6 +14295,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ payment_hash, amount_msats, invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32), min_final_cltv_expiry_delta, + payment_metadata.as_deref(), ) .map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; (payment_hash, payment_secret) @@ -14303,6 +14305,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ .create_inbound_payment( amount_msats, invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32), min_final_cltv_expiry_delta, + payment_metadata.as_deref(), ) .map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))? }, @@ -14341,7 +14344,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ invoice = invoice.private_route(hint); } - let raw_invoice = invoice.build_raw().map_err(|e| SignOrCreationError::CreationError(e))?; + let raw_invoice = if let Some(payment_metadata) = payment_metadata { + invoice.payment_metadata(payment_metadata).build_raw() + } else { + invoice.build_raw() + }.map_err(|e| SignOrCreationError::CreationError(e))?; let signature = self.node_signer.sign_invoice(&raw_invoice, Recipient::Node); raw_invoice @@ -14420,6 +14427,14 @@ pub struct Bolt11InvoiceParameters { /// involving another protocol where the payment hash is also involved outside the scope of /// lightning. pub payment_hash: Option, + + /// The `payment_metadata` to include in the invoice. This is provided back to us in the payment + /// onion by the sender, available as [`RecipientOnionFields::payment_metadata`] via + /// [`Event::PaymentClaimable::onion_fields`]. + /// + /// Note that because it is exposed to the sender in the invoice you should consider encrypting + /// it. It is committed to, however, so cannot be modified by the sender. + pub payment_metadata: Option>, } impl Default for Bolt11InvoiceParameters { @@ -14430,6 +14445,7 @@ impl Default for Bolt11InvoiceParameters { invoice_expiry_delta_secs: None, min_final_cltv_expiry_delta: None, payment_hash: None, + payment_metadata: None, } } } @@ -14921,7 +14937,7 @@ impl< refund, self.list_usable_channels(), |amount_msats, relative_expiry| { - self.create_inbound_payment(Some(amount_msats), relative_expiry, None) + self.create_inbound_payment(Some(amount_msats), relative_expiry, None, None) .map_err(|()| Bolt12SemanticError::InvalidAmount) }, )?; @@ -14964,7 +14980,7 @@ impl< /// [`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, + min_final_cltv_expiry_delta: Option, payment_metadata: Option<&[u8]>, ) -> Result<(PaymentHash, PaymentSecret), ()> { inbound_payment::create( &self.inbound_payment_key, @@ -14973,6 +14989,7 @@ impl< &self.entropy_source, self.highest_seen_timestamp.load(Ordering::Acquire) as u64, min_final_cltv_expiry_delta, + payment_metadata, ) } @@ -14992,6 +15009,9 @@ impl< /// before a [`PaymentClaimable`] event will be generated, ensuring that we do not provide the /// sender "proof-of-payment" unless they have paid the required amount. /// + /// The returned secret commits to the `payment_metadata` and thus the invoice's metadata must + /// match what is provided here. + /// /// `invoice_expiry_delta_secs` describes the number of seconds that the invoice is valid for /// in excess of the current time. This should roughly match the expiry time set in the invoice. /// After this many seconds, we will remove the inbound payment, resulting in any attempts to @@ -15025,6 +15045,7 @@ impl< pub fn create_inbound_payment_for_hash( &self, payment_hash: PaymentHash, min_value_msat: Option, invoice_expiry_delta_secs: u32, min_final_cltv_expiry: Option, + payment_metadata: Option<&[u8]>, ) -> Result { inbound_payment::create_from_hash( &self.inbound_payment_key, @@ -15033,18 +15054,25 @@ impl< invoice_expiry_delta_secs, self.highest_seen_timestamp.load(Ordering::Acquire) as u64, min_final_cltv_expiry, + payment_metadata, ) } - /// Gets an LDK-generated payment preimage from a payment hash and payment secret that were + /// Gets an LDK-generated payment preimage from a payment hash, metadata and secret that were /// previously returned from [`create_inbound_payment`]. /// /// [`create_inbound_payment`]: Self::create_inbound_payment pub fn get_payment_preimage( &self, payment_hash: PaymentHash, payment_secret: PaymentSecret, + payment_metadata: Option<&[u8]>, ) -> Result { let expanded_key = &self.inbound_payment_key; - inbound_payment::get_payment_preimage(payment_hash, payment_secret, expanded_key) + inbound_payment::get_payment_preimage( + payment_hash, + payment_secret, + payment_metadata, + expanded_key, + ) } /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively @@ -17113,7 +17141,8 @@ impl< self.create_inbound_payment( Some(amount_msats), relative_expiry, - None + None, + None, ).map_err(|_| Bolt12SemanticError::InvalidAmount) }; @@ -21325,7 +21354,7 @@ mod tests { // payment verification fails as expected. let mut bad_payment_hash = payment_hash.clone(); bad_payment_hash.0[0] += 1; - match inbound_payment::verify(bad_payment_hash, &payment_data, nodes[0].node.highest_seen_timestamp.load(Ordering::Acquire) as u64, &nodes[0].node.inbound_payment_key, &nodes[0].logger) { + match inbound_payment::verify(bad_payment_hash, &payment_data, None, nodes[0].node.highest_seen_timestamp.load(Ordering::Acquire) as u64, &nodes[0].node.inbound_payment_key, &nodes[0].logger) { Ok(_) => panic!("Unexpected ok"), Err(()) => { nodes[0].logger.assert_log_contains("lightning::ln::inbound_payment", "Failing HTLC with user-generated payment_hash", 1); @@ -21333,7 +21362,7 @@ mod tests { } // Check that using the original payment hash succeeds. - assert!(inbound_payment::verify(payment_hash, &payment_data, nodes[0].node.highest_seen_timestamp.load(Ordering::Acquire) as u64, &nodes[0].node.inbound_payment_key, &nodes[0].logger).is_ok()); + assert!(inbound_payment::verify(payment_hash, &payment_data, None, nodes[0].node.highest_seen_timestamp.load(Ordering::Acquire) as u64, &nodes[0].node.inbound_payment_key, &nodes[0].logger).is_ok()); } fn check_not_connected_to_peer_error( @@ -22006,7 +22035,7 @@ pub mod bench { payment_preimage.0[0..8].copy_from_slice(&payment_count.to_le_bytes()); payment_count += 1; let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); - let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None).unwrap(); + let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None).unwrap(); $node_a.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret, 10_000), PaymentId(payment_hash.0), diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index f89fdd0572b..3dd3018964a 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -2807,6 +2807,7 @@ pub fn get_payment_preimage_hash( min_value_msat, 7200, min_final_cltv_expiry_delta, + None, ) .unwrap(); (payment_preimage, payment_hash, payment_secret) diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 8bbb9b99479..7393f354010 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -293,8 +293,10 @@ pub fn test_duplicate_htlc_different_direction_onchain() { let (payment_preimage, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], 900_000); let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[0], payment_value_msats); - let node_a_payment_secret = - nodes[0].node.create_inbound_payment_for_hash(payment_hash, None, 7200, None).unwrap(); + let node_a_payment_secret = nodes[0] + .node + .create_inbound_payment_for_hash(payment_hash, None, 7200, None, None) + .unwrap(); send_along_route_with_secret( &nodes[1], route, @@ -4157,8 +4159,10 @@ pub fn test_duplicate_payment_hash_one_failure_one_success() { let (our_payment_preimage, dup_payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3]], 900_000); - let payment_secret = - nodes[4].node.create_inbound_payment_for_hash(dup_payment_hash, None, 7200, None).unwrap(); + let payment_secret = nodes[4] + .node + .create_inbound_payment_for_hash(dup_payment_hash, None, 7200, None, None) + .unwrap(); let payment_params = PaymentParameters::from_node_id(node_e_id, TEST_FINAL_CLTV) .with_bolt11_features(nodes[4].node.bolt11_invoice_features()) .unwrap(); @@ -4425,13 +4429,13 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 2nd HTLC (not added - smaller than dust limit + HTLC tx fee): let path_5: &[&[_]] = &[&[&nodes[2], &nodes[3], &nodes[5]]]; let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_1, None, 7200, None).unwrap(); + nodes[5].node.create_inbound_payment_for_hash(hash_1, None, 7200, None, None).unwrap(); let route = route_to_5.clone(); send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_1, payment_secret); // 3rd HTLC (not added - smaller than dust limit + HTLC tx fee): let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_2, None, 7200, None).unwrap(); + nodes[5].node.create_inbound_payment_for_hash(hash_2, None, 7200, None, None).unwrap(); let route = route_to_5; send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_2, payment_secret); @@ -4444,12 +4448,12 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 6th HTLC: let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_3, None, 7200, None).unwrap(); + nodes[5].node.create_inbound_payment_for_hash(hash_3, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route.clone(), path_5, 1000000, hash_3, payment_secret); // 7th HTLC: let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_4, None, 7200, None).unwrap(); + nodes[5].node.create_inbound_payment_for_hash(hash_4, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, 1000000, hash_4, payment_secret); // 8th HTLC: @@ -4458,7 +4462,7 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 9th HTLC (not added - smaller than dust limit + HTLC tx fee): let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], dust_limit_msat); let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_5, None, 7200, None).unwrap(); + nodes[5].node.create_inbound_payment_for_hash(hash_5, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_5, payment_secret); // 10th HTLC (not added - smaller than dust limit + HTLC tx fee): @@ -4467,7 +4471,7 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 11th HTLC: let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], 1000000); let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_6, None, 7200, None).unwrap(); + nodes[5].node.create_inbound_payment_for_hash(hash_6, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, 1000000, hash_6, payment_secret); // Double-check that six of the new HTLC were added @@ -6062,7 +6066,7 @@ pub fn test_check_htlc_underpaying() { let (_, our_payment_hash, _) = get_payment_preimage_hash(&nodes[0], None, None); let our_payment_secret = nodes[1] .node - .create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200, None) + .create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200, None, None) .unwrap(); let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); @@ -7230,7 +7234,7 @@ pub fn test_preimage_storage() { { let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(Some(100_000), 7200, None).unwrap(); + nodes[1].node.create_inbound_payment(Some(100_000), 7200, None, None).unwrap(); let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); @@ -7275,7 +7279,7 @@ pub fn test_bad_secret_hash() { let random_hash = PaymentHash([42; 32]); let random_secret = PaymentSecret([43; 32]); let (our_payment_hash, our_payment_secret) = - nodes[1].node.create_inbound_payment(Some(100_000), 2, None).unwrap(); + nodes[1].node.create_inbound_payment(Some(100_000), 2, None, None).unwrap(); let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); // All the below cases should end up being handled exactly identically, so we macro the @@ -9494,9 +9498,13 @@ fn do_payment_with_custom_min_final_cltv_expiry(valid_delta: bool, use_user_hash } else { let (hash, payment_secret) = nodes[1] .node - .create_inbound_payment(Some(recv_value), 7200, Some(min_cltv_expiry_delta)) + .create_inbound_payment(Some(recv_value), 7200, Some(min_cltv_expiry_delta), None) .unwrap(); - (hash, nodes[1].node.get_payment_preimage(hash, payment_secret).unwrap(), payment_secret) + ( + hash, + nodes[1].node.get_payment_preimage(hash, payment_secret, None).unwrap(), + payment_secret, + ) }; let route = get_route!(nodes[0], payment_parameters, recv_value).unwrap(); let onion = RecipientOnionFields::secret_only(payment_secret, recv_value); diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index b52518584f7..b81c111f7a1 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -155,6 +155,7 @@ fn min_final_cltv_expiry_delta_from_info(bytes: [u8; INFO_LEN]) -> u16 { pub fn create( keys: &ExpandedKey, min_value_msat: Option, invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64, min_final_cltv_expiry_delta: Option, + payment_metadata: Option<&[u8]>, ) -> Result<(PaymentHash, PaymentSecret), ()> { let info_bytes = construct_info_bytes( min_value_msat, @@ -175,6 +176,10 @@ pub fn create( let mut hmac = HmacEngine::::new(&keys.ldk_pmt_hash_key); hmac.input(&iv_bytes); hmac.input(&info_bytes); + if let Some(metadata) = payment_metadata { + hmac.input(&(metadata.len() as u64).to_le_bytes()); + hmac.input(metadata); + } let payment_preimage_bytes = Hmac::from_engine(hmac).to_byte_array(); let ldk_pmt_hash = PaymentHash(Sha256::hash(&payment_preimage_bytes).to_byte_array()); @@ -195,6 +200,7 @@ pub fn create( pub fn create_from_hash( keys: &ExpandedKey, min_value_msat: Option, payment_hash: PaymentHash, invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option, + payment_metadata: Option<&[u8]>, ) -> Result { let info_bytes = construct_info_bytes( min_value_msat, @@ -211,6 +217,10 @@ pub fn create_from_hash( let mut hmac = HmacEngine::::new(&keys.user_pmt_hash_key); hmac.input(&info_bytes); hmac.input(&payment_hash.0); + if let Some(metadata) = payment_metadata { + hmac.input(&(metadata.len() as u64).to_le_bytes()); + hmac.input(metadata); + } let hmac_bytes = Hmac::from_engine(hmac).to_byte_array(); let mut iv_bytes = [0 as u8; IV_LEN]; @@ -353,8 +363,8 @@ fn construct_payment_secret( /// [`create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment /// [`create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash pub(super) fn verify( - payment_hash: PaymentHash, payment_data: &msgs::FinalOnionHopData, highest_seen_timestamp: u64, - keys: &ExpandedKey, logger: &L, + payment_hash: PaymentHash, payment_data: &msgs::FinalOnionHopData, + payment_metadata: Option<&[u8]>, highest_seen_timestamp: u64, keys: &ExpandedKey, logger: &L, ) -> Result<(Option, Option), ()> { let (iv_bytes, info_bytes) = decrypt_info(payment_data.payment_secret, keys); @@ -375,6 +385,10 @@ pub(super) fn verify( let mut hmac = HmacEngine::::new(&keys.user_pmt_hash_key); hmac.input(&info_bytes[..]); hmac.input(&payment_hash.0); + if let Some(metadata) = payment_metadata { + hmac.input(&(metadata.len() as u64).to_le_bytes()); + hmac.input(metadata); + } if !fixed_time_eq( &iv_bytes, &Hmac::from_engine(hmac).to_byte_array().split_at_mut(IV_LEN).0, @@ -388,7 +402,13 @@ pub(super) fn verify( } }, Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - match derive_ldk_payment_preimage(payment_hash, &iv_bytes, &info_bytes, keys) { + match derive_ldk_payment_preimage( + payment_hash, + &iv_bytes, + &info_bytes, + payment_metadata, + keys, + ) { Ok(preimage) => payment_preimage = Some(preimage), Err(bad_preimage_bytes) => { log_trace!( @@ -450,21 +470,27 @@ pub(super) fn verify( } pub(super) fn get_payment_preimage( - payment_hash: PaymentHash, payment_secret: PaymentSecret, keys: &ExpandedKey, + payment_hash: PaymentHash, payment_secret: PaymentSecret, payment_metadata: Option<&[u8]>, + keys: &ExpandedKey, ) -> Result { let (iv_bytes, info_bytes) = decrypt_info(payment_secret, keys); match Method::from_bits((info_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET) { Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - derive_ldk_payment_preimage(payment_hash, &iv_bytes, &info_bytes, keys).map_err( - |bad_preimage_bytes| APIError::APIMisuseError { - err: format!( - "Payment hash {} did not match decoded preimage {}", - &payment_hash, - log_bytes!(bad_preimage_bytes) - ), - }, + derive_ldk_payment_preimage( + payment_hash, + &iv_bytes, + &info_bytes, + payment_metadata, + keys, ) + .map_err(|bad_preimage_bytes| APIError::APIMisuseError { + err: format!( + "Payment hash {} did not match decoded preimage {}", + &payment_hash, + log_bytes!(bad_preimage_bytes) + ), + }) }, Ok(Method::UserPaymentHash) | Ok(Method::UserPaymentHashCustomFinalCltv) => { Err(APIError::APIMisuseError { @@ -504,11 +530,15 @@ fn decrypt_info( // this case. fn derive_ldk_payment_preimage( payment_hash: PaymentHash, iv_bytes: &[u8; IV_LEN], info_bytes: &[u8; INFO_LEN], - keys: &ExpandedKey, + payment_metadata: Option<&[u8]>, keys: &ExpandedKey, ) -> Result { let mut hmac = HmacEngine::::new(&keys.ldk_pmt_hash_key); hmac.input(iv_bytes); hmac.input(info_bytes); + if let Some(metadata) = payment_metadata { + hmac.input(&(metadata.len() as u64).to_le_bytes()); + hmac.input(metadata); + } let decoded_payment_preimage = Hmac::from_engine(hmac).to_byte_array(); if !fixed_time_eq(&payment_hash.0, &Sha256::hash(&decoded_payment_preimage).to_byte_array()) { return Err(decoded_payment_preimage); diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 63ad110bba0..564203bf524 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -191,6 +191,7 @@ fn _create_phantom_invoice( invoice_expiry_delta_secs, duration_since_epoch.as_secs(), min_final_cltv_expiry_delta, + None, ) .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; (payment_hash, payment_secret) @@ -202,6 +203,7 @@ fn _create_phantom_invoice( &entropy_source, duration_since_epoch.as_secs(), min_final_cltv_expiry_delta, + None, ) .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))? }; @@ -670,7 +672,8 @@ mod test { let (payment_hash, payment_secret) = (invoice.payment_hash(), *invoice.payment_secret()); - let preimage = nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap(); + let preimage = + nodes[1].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap(); // Invoice SCIDs should always use inbound SCID aliases over the real channel ID, if one is // available. @@ -1255,7 +1258,7 @@ mod test { let payment_preimage = if user_generated_pmt_hash { user_payment_preimage } else { - nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap() + nodes[1].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap() }; assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64); @@ -1363,7 +1366,7 @@ mod test { let payment_amt = 20_000; let (payment_hash, _payment_secret) = - nodes[1].node.create_inbound_payment(Some(payment_amt), 3600, None).unwrap(); + nodes[1].node.create_inbound_payment(Some(payment_amt), 3600, None, None).unwrap(); let route_hints = vec![nodes[1].node.get_phantom_route_hints(), nodes[2].node.get_phantom_route_hints()]; diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index 0515a5290d7..17580b09b95 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -32,7 +32,7 @@ use crate::routing::router::{ }; use crate::sign::NodeSigner; use crate::types::features::BlindedHopFeatures; -use crate::types::payment::PaymentSecret; +use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::util::errors::APIError; use crate::util::ser::Writeable; use crate::util::test_utils; @@ -80,9 +80,33 @@ fn large_payment_metadata() { - final_payload_len_without_metadata; let mut payment_metadata = vec![42; max_metadata_len]; + let mut counter = 42; + macro_rules! get_payment_hash { + ($node: expr, $metadata: expr) => {{ + let payment_preimage = PaymentPreimage([counter; 32]); + #[allow(unused_assignments)] + { + counter += 1; + } + let payment_hash: PaymentHash = payment_preimage.into(); + let payment_secret = $node + .node + .create_inbound_payment_for_hash( + payment_hash, + Some(amt_msat), + 7200, + None, + Some($metadata), + ) + .unwrap(); + (payment_hash, payment_preimage, payment_secret) + }}; + } + // Check that the maximum-size metadata is sendable. - let (mut route_0_1, payment_hash, payment_preimage, payment_secret) = - get_route_and_payment_hash!(&nodes[0], &nodes[1], amt_msat); + let (payment_hash, payment_preimage, payment_secret) = + get_payment_hash!(nodes[1], &payment_metadata); + let (mut route_0_1, ..) = get_route_and_payment_hash!(&nodes[0], &nodes[1], amt_msat); let mut max_sized_onion = RecipientOnionFields { payment_secret: Some(payment_secret), payment_metadata: Some(payment_metadata.clone()), @@ -112,14 +136,17 @@ fn large_payment_metadata() { // Check that the payment parameter for max path length will prevent us from routing past our // next-hop peer given the payment_metadata size. - let (mut route_0_2, payment_hash_2, payment_preimage_2, payment_secret_2) = - get_route_and_payment_hash!(&nodes[0], &nodes[2], amt_msat); + + let (payment_hash_2, _, payment_secret_2) = + get_payment_hash!(nodes[2], &max_sized_onion.payment_metadata.as_ref().unwrap()); + let (mut route_0_2, ..) = get_route_and_payment_hash!(&nodes[0], &nodes[2], amt_msat); let mut route_params_0_2 = route_0_2.route_params.clone().unwrap(); route_params_0_2.payment_params.max_path_length = 1; nodes[0].router.expect_find_route_query(route_params_0_2); + max_sized_onion.payment_secret = Some(payment_secret_2); let id = PaymentId(payment_hash_2.0); - let route_params = route_0_2.route_params.clone().unwrap(); + let mut route_params = route_0_2.route_params.clone().unwrap(); let err = nodes[0] .node .send_payment(payment_hash_2, max_sized_onion.clone(), id, route_params, Retry::Attempts(0)) @@ -130,6 +157,9 @@ fn large_payment_metadata() { let mut too_large_onion = max_sized_onion.clone(); too_large_onion.payment_metadata.as_mut().map(|mut md| md.push(42)); too_large_onion.total_mpp_amount_msat = MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; + let (payment_hash_2, _, payment_secret_2) = + get_payment_hash!(nodes[2], &too_large_onion.payment_metadata.as_ref().unwrap()); + too_large_onion.payment_secret = Some(payment_secret_2); // First confirm we'll fail to create the onion packet directly. let secp_ctx = Secp256k1::signing_only(); @@ -164,6 +194,8 @@ fn large_payment_metadata() { // If we remove enough payment_metadata bytes to allow for 2 hops, we're now able to send to // nodes[2]. let two_hop_metadata = vec![42; max_metadata_len - INTERMED_PAYLOAD_LEN_ESTIMATE]; + let (payment_hash_2, payment_preimage_2, payment_secret_2) = + get_payment_hash!(nodes[2], &two_hop_metadata); let mut onion_allowing_2_hops = RecipientOnionFields { payment_secret: Some(payment_secret_2), payment_metadata: Some(two_hop_metadata.clone()), diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index e80fcea33aa..2eb5d4ee85c 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -1548,7 +1548,7 @@ fn get_ldk_payment_preimage() { let amt_msat = 60_000; let expiry_secs = 60 * 60; let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(Some(amt_msat), expiry_secs, None).unwrap(); + nodes[1].node.create_inbound_payment(Some(amt_msat), expiry_secs, None, None).unwrap(); let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) @@ -1561,7 +1561,8 @@ fn get_ldk_payment_preimage() { check_added_monitors(&nodes[0], 1); // Make sure to use `get_payment_preimage` - let preimage = Some(nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap()); + let preimage = + Some(nodes[1].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap()); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let event = events.pop().unwrap(); @@ -2305,7 +2306,7 @@ fn do_test_intercepted_payment(test: InterceptTest) { let route = get_route(&nodes[0], &route_params).unwrap(); let (hash, payment_secret) = - nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None).unwrap(); + nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None, None).unwrap(); let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment_with_route(route.clone(), hash, onion, id).unwrap(); @@ -2414,7 +2415,8 @@ fn do_test_intercepted_payment(test: InterceptTest) { do_commitment_signed_dance(&nodes[2], &nodes[1], commitment, false, true); expect_and_process_pending_htlcs(&nodes[2], false); - let preimage = Some(nodes[2].node.get_payment_preimage(hash, payment_secret).unwrap()); + let preimage = + Some(nodes[2].node.get_payment_preimage(hash, payment_secret, None).unwrap()); expect_payment_claimable!(&nodes[2], hash, payment_secret, amt_msat, preimage, node_c_id); let path: &[&[_]] = &[&[&nodes[1], &nodes[2]]]; @@ -2541,7 +2543,7 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { .unwrap(); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); let (payment_hash, payment_secret) = - nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None).unwrap(); + nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None, None).unwrap(); let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); @@ -2597,7 +2599,7 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { // Claim the payment and check that the skimmed fee is as expected. let payment_preimage = - nodes[2].node.get_payment_preimage(payment_hash, payment_secret).unwrap(); + nodes[2].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap(); let events = nodes[2].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); match events[0] { @@ -4885,10 +4887,20 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) { // Pay more than half of each channel's max, requiring MPP let amt_msat = 750_000_000; - let (payment_preimage, payment_hash, payment_secret) = - get_payment_preimage_hash(&nodes[3], Some(amt_msat), None); - let payment_id = PaymentId(payment_hash.0); let payment_metadata = vec![44, 49, 52, 142]; + let payment_preimage = PaymentPreimage([42; 32]); + let payment_hash: PaymentHash = payment_preimage.into(); + let payment_secret = nodes[3] + .node + .create_inbound_payment_for_hash( + payment_hash, + Some(amt_msat), + 7200, + None, + Some(&payment_metadata), + ) + .unwrap(); + let payment_id = PaymentId(payment_hash.0); let payment_params = PaymentParameters::from_node_id(node_d_id, TEST_FINAL_CLTV) .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) diff --git a/pending_changelog/matt-commit-to-metadata.txt b/pending_changelog/matt-commit-to-metadata.txt new file mode 100644 index 00000000000..5e13e134f88 --- /dev/null +++ b/pending_changelog/matt-commit-to-metadata.txt @@ -0,0 +1,6 @@ +# Backwards compat + * Payment metadata is now committed to in the HMAC used to build payment secrets. + As such, any existing BOLT 11 invoices issued with payment metadata will be + implicitly invalidated on upgrade and any BOLT 11 invoices issued with payment + metadata will be invalidated on downgrade. If this is problematic for you + please reach out. From 44828f7260a490d133fd63f37a57e931b71f48a8 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 6 May 2026 19:21:29 +0000 Subject: [PATCH 3/3] Default to requiring `payment_metadata` when building BOLT 11s Now that we commit to payment metadata fields and require them implicitly as a part of payments, we should match that in `lightning-invoice` - instead marking them as required by default. --- lightning-invoice/src/lib.rs | 30 ++++++++++++++++-------------- lightning-invoice/tests/ser_de.rs | 2 -- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 4ee9acb5f27..6c18e600b55 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -880,11 +880,10 @@ impl { /// Sets the payment metadata. /// - /// By default features are set to *optionally* allow the sender to include the payment metadata. - /// If you wish to require that the sender include the metadata (and fail to parse the invoice if - /// they don't support payment metadata fields), you need to call - /// [`InvoiceBuilder::require_payment_metadata`] after this. - pub fn payment_metadata( + /// This marks the payment metadata as optional, allowing a legacy sender that doesn't + /// understand payment metadata to ignore it. Note that LDK by default commits to the payment + /// metadata in its payment secret, implicitly making it required. + pub fn optional_payment_metadata( mut self, payment_metadata: Vec, ) -> InvoiceBuilder { self.tagged_fields.push(TaggedField::PaymentMetadata(payment_metadata)); @@ -902,20 +901,23 @@ impl } self.set_flags() } -} -impl - InvoiceBuilder -{ - /// Sets forwarding of payment metadata as required. A reader of the invoice which does not - /// support sending payment metadata will fail to read the invoice. - pub fn require_payment_metadata(mut self) -> InvoiceBuilder { - for field in self.tagged_fields.iter_mut() { + /// Sets the payment metadata. + /// + /// By default features are set to *require* the sender to include the payment metadata. + /// If you wish to support legacy senders that ignore the metadata, you can call + /// [`InvoiceBuilder::optional_payment_metadata`] instead. Note that LDK by default commits to + /// the payment metadata in its payment secret, implicitly making it required. + pub fn payment_metadata( + self, payment_metadata: Vec, + ) -> InvoiceBuilder { + let mut res = self.optional_payment_metadata(payment_metadata); + for field in res.tagged_fields.iter_mut() { if let TaggedField::Features(f) = field { f.set_payment_metadata_required(); } } - self + res } } diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index 353878a9c52..be173912a78 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -418,7 +418,6 @@ fn get_test_tuples() -> Vec<(String, SignedRawBolt11Invoice, bool, bool)> { )) .description("payment metadata inside".to_owned()) .payment_metadata(>::from_hex("01fafaf0").unwrap()) - .require_payment_metadata() .payee_pub_key(PublicKey::from_slice(&>::from_hex( "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad" ).unwrap()).unwrap()) @@ -450,7 +449,6 @@ fn get_test_tuples() -> Vec<(String, SignedRawBolt11Invoice, bool, bool)> { )) .description("payment metadata inside".to_owned()) .payment_metadata(>::from_hex("01fafaf0").unwrap()) - .require_payment_metadata() .payment_secret(PaymentSecret([0x11; 32])) .build_raw() .unwrap()