Commit to payment_metadata in inbound payment HMAC
What changed, and why it matters
This change cryptographically ties a piece of invoice data called payment_metadata to the payment secret. Before, a sender could alter that metadata in flight and the receiver would still accept the payment. Now, if the metadata is changed, the payment will be rejected. The change is a security improvement, but it breaks compatibility for existing invoices that already include payment_metadata when nodes upgrade or downgrade.
Review any deployed systems that issue or receive BOLT 11 invoices with payment_metadata, because existing invoices will fail after this upgrade. Ensure all nodes in a payment path that use this feature are upgraded together. Consider whether the metadata should be encrypted before being placed in the invoice, as the commit message and new docs note it is exposed to the sender.
Security signals we found
Cryptographic binding of payment_metadata to payment secret via HMAC
Preimage derivation now includes payment_metadata
Verification now rejects payments where metadata was tampered with
Backwards-compatibility break for existing metadata-bearing invoices
New public API parameter for payment_metadata in invoice creation and inbound payment registration
Evidence from the diff
The commit modifies rust-lightning’s inbound payment HMAC so that payment_metadata (when present) is included in the HMAC input used to derive the payment secret and preimage. The metadata length is prepended as a little-endian u64 before the metadata bytes. This binds the metadata to the payment cryptographically: verify() and get_payment_preimage() now require the same metadata that was used at creation. Existing payments without metadata remain unaffected. The change is a breaking compatibility change for any BOLT 11 invoice that includes payment_metadata.
Changed components
lightning/src/ln/inbound_payment.rslightning/src/ln/channelmanager.rslightning/src/ln/invoice_utils.rslightning/src/ln/bolt11_payment_tests.rslightning/src/ln/max_payment_path_len_tests.rslightning/src/ln/payment_tests.rslightning/src/ln/functional_tests.rslightning/src/ln/functional_test_utils.rsfuzz/src/chanmon_consistency.rsfuzz/src/full_stack.rslightning-liquidity/tests/lsps2_integration_tests.rsInspect captured patch +183 / −63
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 8a90dc9..2667b73 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 e79bef7..58509bb 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<dyn Logger + MaybeSend + MaybeSync>
},
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 fbff2ea..92e6b33 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 8c2ac15..733e26d 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 9920be8..a05d620 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<Bolt11Invoice, SignOrCreationError<()>> {
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<PaymentHash>,
+
+ /// 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<Vec<u8>>,
}
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<u64>, invoice_expiry_delta_secs: u32,
- min_final_cltv_expiry_delta: Option<u16>,
+ min_final_cltv_expiry_delta: Option<u16>, 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<u64>,
invoice_expiry_delta_secs: u32, min_final_cltv_expiry: Option<u16>,
+ payment_metadata: Option<&[u8]>,
) -> Result<PaymentSecret, ()> {
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<PaymentPreimage, APIError> {
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<T>(
@@ -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 f89fdd0..3dd3018 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 8bbb9b9..7393f35 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 b525185..b81c111 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<ES: EntropySource>(
keys: &ExpandedKey, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32,
entropy_source: &ES, current_time: u64, min_final_cltv_expiry_delta: Option<u16>,
+ payment_metadata: Option<&[u8]>,
) -> Result<(PaymentHash, PaymentSecret), ()> {
let info_bytes = construct_info_bytes(
min_value_msat,
@@ -175,6 +176,10 @@ pub fn create<ES: EntropySource>(
let mut hmac = HmacEngine::<Sha256>::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<ES: EntropySource>(
pub fn create_from_hash(
keys: &ExpandedKey, min_value_msat: Option<u64>, payment_hash: PaymentHash,
invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option<u16>,
+ payment_metadata: Option<&[u8]>,
) -> Result<PaymentSecret, ()> {
let info_bytes = construct_info_bytes(
min_value_msat,
@@ -211,6 +217,10 @@ pub fn create_from_hash(
let mut hmac = HmacEngine::<Sha256>::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<L: Logger>(
- 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<PaymentPreimage>, Option<u16>), ()> {
let (iv_bytes, info_bytes) = decrypt_info(payment_data.payment_secret, keys);
@@ -375,6 +385,10 @@ pub(super) fn verify<L: Logger>(
let mut hmac = HmacEngine::<Sha256>::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<L: Logger>(
}
},
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<L: Logger>(
}
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<PaymentPreimage, APIError> {
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<PaymentPreimage, [u8; 32]> {
let mut hmac = HmacEngine::<Sha256>::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 63ad110..564203b 100644
--- a/lightning/src/ln/invoice_utils.rs
+++ b/lightning/src/ln/invoice_utils.rs
@@ -191,6 +191,7 @@ fn _create_phantom_invoice<ES: EntropySource, NS: NodeSigner, L: Logger>(
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<ES: EntropySource, NS: NodeSigner, L: Logger>(
&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 0515a52..17580b0 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 e80fcea..2eb5d4e 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 0000000..5e13e13
--- /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.
Why this scored 60/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.