Encrypt `payment_metadata` when we build the payment secret
What changed, and why it matters
This commit changes how Lightning Dev Kit handles optional extra data attached to incoming payments (called payment_metadata). Previously, LDK expected downstream applications to encrypt that data themselves before handing it to LDK. Now LDK encrypts and decrypts it internally, reusing random values already generated for payment secrets. The goal is to avoid making invoices larger by adding separate encryption overhead. For payments where the user supplies their own payment hash, a fresh random value is appended to the encrypted metadata, with a documented warning that reusing the same payment hash with different metadata is unsafe. The change is a privacy/usability improvement, not a fix for an active vulnerability, and it includes new tests covering the three ways metadata can be created.
Reviewers should confirm that the ChaCha20 nonce/IV construction is never reused for the same key under realistic usage, especially for the LDK-generated payment_hash path where the IV is deterministic from the payment_secret. Verify that the HMAC over encrypted metadata is computed before encryption and after decryption consistently, and that callers cannot accidentally pass plaintext metadata into the new API. Consider whether the user-hash IV-appended format needs a version or length prefix for future compatibility. No urgent security patch is indicated, but downstream users should update to the new API and stop pre-encrypting metadata.
Security signals we found
New symmetric encryption key derived via HKDF-SHA256
ChaCha20 used as a stream cipher for metadata confidentiality
Metadata is included in the existing payment_secret HMAC, providing integrity
User-supplied payment_hash path appends a random IV, with documented reuse limitation
Spontaneous payments now explicitly reject payment_metadata
API rename get_payment_preimage -> get_payment_preimage_decrypt_metadata
Evidence from the diff
The patch moves payment_metadata encryption from callers into LDK’s inbound_payment module. It extends the existing HKDF key expansion from 7 to 8 outputs to add a dedicated metadata_enc_key. For LDK-generated payment hashes, the metadata is XORed with a ChaCha20 keystream keyed by metadata_enc_key and using the same 16-byte IV that already seeds the payment_secret/payment_hash derivation. For user-supplied payment hashes, where no reusable IV exists, a fresh 16-byte IV is appended to the ciphertext after encryption. During verification, the metadata is authenticated by the existing HMAC over the (now encrypted) metadata and then decrypted in place before being exposed to the caller. The public API changes: create_inbound_payment and create_inbound_payment_for_hash now return the encrypted metadata as an additional tuple element; get_payment_preimage is renamed to get_payment_preimage_decrypt_metadata and decrypts a caller-provided metadata buffer in place. The commit also adds end-to-end tests for metadata via Bolt11 invoice, create_inbound_payment, and create_inbound_payment_for_hash.
Changed components
lightning/src/ln/inbound_payment.rslightning/src/ln/channelmanager.rslightning/src/crypto/utils.rslightning/src/ln/invoice_utils.rslightning/src/ln/bolt11_payment_tests.rslightning/src/ln/payment_tests.rslightning/src/ln/max_payment_path_len_tests.rsInspect captured patch +403 / −125
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 4ff0e4a..72134ab 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -1369,7 +1369,7 @@ impl PaymentTracker {
let mut payment_preimage = PaymentPreimage([0; 32]);
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
+ let (secret, _no_metadata) = dest
.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());
diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs
index 92e6b33..d361215 100644
--- a/lightning-liquidity/tests/lsps2_integration_tests.rs
+++ b/lightning-liquidity/tests/lsps2_integration_tests.rs
@@ -120,7 +120,7 @@ fn create_jit_invoice(
) -> Result<Bolt11Invoice, ()> {
// LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual.
let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2;
- let (payment_hash, payment_secret) = node
+ let (payment_hash, payment_secret, _) = node
.node
.create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta), None)
.map_err(|e| {
diff --git a/lightning/src/crypto/utils.rs b/lightning/src/crypto/utils.rs
index 88911b0..749f7d4 100644
--- a/lightning/src/crypto/utils.rs
+++ b/lightning/src/crypto/utils.rs
@@ -22,7 +22,7 @@ macro_rules! hkdf_extract_expand {
let (k1, k2, _) = hkdf_extract_expand!($salt, $ikm);
(k1, k2)
}};
- ($salt: expr, $ikm: expr, 7) => {{
+ ($salt: expr, $ikm: expr, 8) => {{
let (k1, k2, prk) = hkdf_extract_expand!($salt, $ikm);
let mut hmac = HmacEngine::<Sha256>::new(&prk[..]);
@@ -50,7 +50,12 @@ macro_rules! hkdf_extract_expand {
hmac.input(&[7; 1]);
let k7 = Hmac::from_engine(hmac).to_byte_array();
- (k1, k2, k3, k4, k5, k6, k7)
+ let mut hmac = HmacEngine::<Sha256>::new(&prk[..]);
+ hmac.input(&k7);
+ hmac.input(&[8; 1]);
+ let k8 = Hmac::from_engine(hmac).to_byte_array();
+
+ (k1, k2, k3, k4, k5, k6, k7, k8)
}};
}
@@ -58,10 +63,10 @@ pub fn hkdf_extract_expand_twice(salt: &[u8], ikm: &[u8]) -> ([u8; 32], [u8; 32]
hkdf_extract_expand!(salt, ikm, 2)
}
-pub fn hkdf_extract_expand_7x(
+pub fn hkdf_extract_expand_8x(
salt: &[u8], ikm: &[u8],
-) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) {
- hkdf_extract_expand!(salt, ikm, 7)
+) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) {
+ hkdf_extract_expand!(salt, ikm, 8)
}
#[inline]
diff --git a/lightning/src/ln/bolt11_payment_tests.rs b/lightning/src/ln/bolt11_payment_tests.rs
index 733e26d..3e0ebbb 100644
--- a/lightning/src/ln/bolt11_payment_tests.rs
+++ b/lightning/src/ln/bolt11_payment_tests.rs
@@ -30,8 +30,10 @@ 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, Some(&payment_metadata)).unwrap();
+ let (payment_hash, payment_secret, encrypted_metadata) = nodes[1]
+ .node
+ .create_inbound_payment(None, 7200, None, Some(payment_metadata.clone()))
+ .unwrap();
let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
@@ -41,7 +43,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() {
.duration_since_epoch(timestamp)
.min_final_cltv_expiry_delta(144)
.amount_milli_satoshis(50_000)
- .payment_metadata(payment_metadata.clone())
+ .payment_metadata(encrypted_metadata.unwrap())
.build_raw()
.unwrap();
let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap();
@@ -97,8 +99,10 @@ 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, Some(&payment_metadata)).unwrap();
+ let (payment_hash, payment_secret, encrypted_metadata) = nodes[1]
+ .node
+ .create_inbound_payment(None, 7200, None, Some(payment_metadata.clone()))
+ .unwrap();
let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
let invoice = InvoiceBuilder::new(Currency::Bitcoin)
@@ -107,7 +111,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() {
.payment_secret(payment_secret)
.duration_since_epoch(timestamp)
.min_final_cltv_expiry_delta(144)
- .payment_metadata(payment_metadata.clone())
+ .payment_metadata(encrypted_metadata.unwrap())
.build_raw()
.unwrap();
let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap();
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 25c86b3..2adb0a1 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -8462,7 +8462,7 @@ impl<
payment_data,
payment_context,
phantom_shared_secret,
- onion_fields,
+ mut onion_fields,
has_recipient_created_payment_secret,
invoice_request_opt,
trampoline_shared_secret,
@@ -8603,7 +8603,7 @@ impl<
let verify_res = inbound_payment::verify(
payment_hash,
&payment_data,
- onion_fields.payment_metadata.as_deref(),
+ onion_fields.payment_metadata.as_mut(),
self.highest_seen_timestamp.load(Ordering::Acquire) as u64,
&self.inbound_payment_key,
&self.logger,
@@ -14372,24 +14372,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
}
- let (payment_hash, payment_secret) = match payment_hash {
+ let (payment_hash, payment_secret, payment_metadata) = match payment_hash {
Some(payment_hash) => {
- let payment_secret = self
+ let (payment_secret, payment_metadata) = self
.create_inbound_payment_for_hash(
payment_hash, amount_msats,
invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32),
min_final_cltv_expiry_delta,
- payment_metadata.as_deref(),
+ payment_metadata,
)
.map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
- (payment_hash, payment_secret)
+ (payment_hash, payment_secret, payment_metadata)
},
None => {
self
.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(),
+ payment_metadata,
)
.map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?
},
@@ -14516,8 +14516,7 @@ pub struct Bolt11InvoiceParameters {
/// 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.
+ /// The metadata itself is encrypted and HMAC'd before being stored in the BOLT 11 invoice.
pub payment_metadata: Option<Vec<u8>>,
}
@@ -15023,6 +15022,7 @@ impl<
|amount_msats, relative_expiry| {
self.create_inbound_payment(Some(amount_msats), relative_expiry, None, None)
.map_err(|()| Bolt12SemanticError::InvalidAmount)
+ .map(|(preimage, secret, _no_metadata)| (preimage, secret))
},
None,
)?;
@@ -15033,8 +15033,8 @@ impl<
Ok(invoice)
}
- /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing
- /// to pay us.
+ /// Gets a payment secret, payment hash, and encrypts the `payment_metadata` for use in an
+ /// invoice given to a third party wishing to pay us.
///
/// This differs from [`create_inbound_payment_for_hash`] only in that it generates the
/// [`PaymentHash`] and [`PaymentPreimage`] for you.
@@ -15065,8 +15065,8 @@ 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>, payment_metadata: Option<&[u8]>,
- ) -> Result<(PaymentHash, PaymentSecret), ()> {
+ min_final_cltv_expiry_delta: Option<u16>, payment_metadata: Option<Vec<u8>>,
+ ) -> Result<(PaymentHash, PaymentSecret, Option<Vec<u8>>), ()> {
inbound_payment::create(
&self.inbound_payment_key,
min_value_msat,
@@ -15078,8 +15078,8 @@ impl<
)
}
- /// Gets a [`PaymentSecret`] for a given [`PaymentHash`], for which the payment preimage is
- /// stored external to LDK.
+ /// Gets a [`PaymentSecret`] for a given [`PaymentHash`] (for which the payment preimage is
+ /// stored external to LDK) and encrypts the `payment_metadata`.
///
/// A [`PaymentClaimable`] event will only be generated if the [`PaymentSecret`] matches a
/// payment secret fetched via this method or [`create_inbound_payment`], and which is at least
@@ -15115,41 +15115,34 @@ impl<
/// Note that a malicious eavesdropper can intuit whether an inbound payment was created by
/// `create_inbound_payment` or `create_inbound_payment_for_hash` based on runtime.
///
- /// # Note
- ///
- /// If you register an inbound payment with this method, then serialize the `ChannelManager`, then
- /// deserialize it with a node running 0.0.103 and earlier, the payment will fail to be received.
- ///
/// Errors if `min_value_msat` is greater than total bitcoin supply.
///
- /// If `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable
- /// on versions of LDK prior to 0.0.114.
- ///
/// [`create_inbound_payment`]: Self::create_inbound_payment
/// [`PaymentClaimable`]: events::Event::PaymentClaimable
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, ()> {
+ payment_metadata: Option<Vec<u8>>,
+ ) -> Result<(PaymentSecret, Option<Vec<u8>>), ()> {
inbound_payment::create_from_hash(
&self.inbound_payment_key,
min_value_msat,
payment_hash,
invoice_expiry_delta_secs,
+ &self.entropy_source,
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, metadata and secret that were
- /// previously returned from [`create_inbound_payment`].
+ /// Gets an LDK-generated payment preimage from a payment hash and secret and decrypts the
+ /// metadata (if any) that were previously returned from [`create_inbound_payment`].
///
/// [`create_inbound_payment`]: Self::create_inbound_payment
- pub fn get_payment_preimage(
+ pub fn get_payment_preimage_decrypt_metadata(
&self, payment_hash: PaymentHash, payment_secret: PaymentSecret,
- payment_metadata: Option<&[u8]>,
+ payment_metadata: Option<&mut [u8]>,
) -> Result<PaymentPreimage, APIError> {
let expanded_key = &self.inbound_payment_key;
inbound_payment::get_payment_preimage(
@@ -17235,7 +17228,9 @@ impl<
relative_expiry,
None,
None,
- ).map_err(|_| Bolt12SemanticError::InvalidAmount)
+ )
+ .map_err(|_| Bolt12SemanticError::InvalidAmount)
+ .map(|(preimage, secret, _no_metadata)| (preimage, secret))
};
let (result, context) = match invoice_request {
@@ -22137,7 +22132,8 @@ 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, None).unwrap();
+ let (payment_secret, _no_payment_metadata) =
+ $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 bbb184d..ac6f137 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -2800,7 +2800,7 @@ pub fn get_payment_preimage_hash(
let payment_preimage = PaymentPreimage([*payment_count; 32]);
*payment_count += 1;
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array());
- let payment_secret = recipient
+ let (payment_secret, _) = recipient
.node
.create_inbound_payment_for_hash(
payment_hash,
diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs
index 7393f35..37dd518 100644
--- a/lightning/src/ln/functional_tests.rs
+++ b/lightning/src/ln/functional_tests.rs
@@ -293,7 +293,7 @@ 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]
+ let (node_a_payment_secret, _) = nodes[0]
.node
.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None)
.unwrap();
@@ -4159,7 +4159,7 @@ 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]
+ let (payment_secret, _) = nodes[4]
.node
.create_inbound_payment_for_hash(dup_payment_hash, None, 7200, None, None)
.unwrap();
@@ -4428,13 +4428,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 =
+ let (payment_secret, _) =
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 =
+ let (payment_secret, _) =
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);
@@ -4447,12 +4447,12 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno
let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], 1000000);
// 6th HTLC:
- let payment_secret =
+ let (payment_secret, _) =
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 =
+ let (payment_secret, _) =
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);
@@ -4461,7 +4461,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 =
+ let (payment_secret, _) =
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);
@@ -4470,7 +4470,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 =
+ let (payment_secret, _) =
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);
@@ -6064,7 +6064,7 @@ pub fn test_check_htlc_underpaying() {
.unwrap();
let (_, our_payment_hash, _) = get_payment_preimage_hash(&nodes[0], None, None);
- let our_payment_secret = nodes[1]
+ let (our_payment_secret, _) = nodes[1]
.node
.create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200, None, None)
.unwrap();
@@ -7233,7 +7233,7 @@ pub fn test_preimage_storage() {
create_announced_chan_between_nodes(&nodes, 0, 1);
{
- let (payment_hash, payment_secret) =
+ let (payment_hash, payment_secret, _) =
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);
@@ -7278,7 +7278,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) =
+ let (our_payment_hash, our_payment_secret, _) =
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);
@@ -9496,13 +9496,16 @@ fn do_payment_with_custom_min_final_cltv_expiry(valid_delta: bool, use_user_hash
get_payment_preimage_hash(&nodes[1], Some(recv_value), Some(min_cltv_expiry_delta));
(hash, payment_preimage, payment_secret)
} else {
- let (hash, payment_secret) = nodes[1]
+ let (hash, payment_secret, _) = nodes[1]
.node
.create_inbound_payment(Some(recv_value), 7200, Some(min_cltv_expiry_delta), None)
.unwrap();
(
hash,
- nodes[1].node.get_payment_preimage(hash, payment_secret, None).unwrap(),
+ nodes[1]
+ .node
+ .get_payment_preimage_decrypt_metadata(hash, payment_secret, None)
+ .unwrap(),
payment_secret,
)
};
diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs
index b81c111..40b0477 100644
--- a/lightning/src/ln/inbound_payment.rs
+++ b/lightning/src/ln/inbound_payment.rs
@@ -15,7 +15,7 @@ use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::{Hash, HashEngine};
use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce};
-use crate::crypto::utils::hkdf_extract_expand_7x;
+use crate::crypto::utils::hkdf_extract_expand_8x;
use crate::ln::msgs;
use crate::ln::msgs::MAX_VALUE_MSAT;
use crate::offers::nonce::Nonce as LocalNonce;
@@ -60,6 +60,8 @@ pub struct ExpandedKey {
/// that this is not used for blinded paths that are not expected to be shared across nodes
/// participating in a "phantom node".
pub(crate) phantom_node_blinded_path_key: [u8; 32],
+ /// The key used to encrypt payment metadata.
+ metadata_enc_key: [u8; 32],
}
impl ExpandedKey {
@@ -75,7 +77,8 @@ impl ExpandedKey {
offers_encryption_key,
spontaneous_pmt_key,
phantom_node_blinded_path_key,
- ) = hkdf_extract_expand_7x(b"LDK Inbound Payment Key Expansion", &key_material);
+ metadata_enc_key,
+ ) = hkdf_extract_expand_8x(b"LDK Inbound Payment Key Expansion", &key_material);
Self {
info_key,
ldk_pmt_hash_key,
@@ -84,6 +87,7 @@ impl ExpandedKey {
offers_encryption_key,
spontaneous_pmt_key,
phantom_node_blinded_path_key,
+ metadata_enc_key,
}
}
@@ -150,13 +154,16 @@ fn min_final_cltv_expiry_delta_from_info(bytes: [u8; INFO_LEN]) -> u16 {
/// Note that if `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable
/// on versions of LDK prior to 0.0.114.
///
+/// Returns an encrypted copy of the `payment_metadata` (if any) which must be included as a part of
+/// validation.
+///
/// [phantom node payments]: crate::sign::PhantomKeysManager
/// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key
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), ()> {
+ mut payment_metadata: Option<Vec<u8>>,
+) -> Result<(PaymentHash, PaymentSecret, Option<Vec<u8>>), ()> {
let info_bytes = construct_info_bytes(
min_value_msat,
if min_final_cltv_expiry_delta.is_some() {
@@ -173,10 +180,19 @@ pub fn create<ES: EntropySource>(
let rand_bytes = entropy_source.get_secure_random_bytes();
iv_bytes.copy_from_slice(&rand_bytes[..IV_LEN]);
+ if let Some(metadata) = payment_metadata.as_mut() {
+ ChaCha20::new_from_block(
+ Key::new(keys.metadata_enc_key),
+ Nonce::new(iv_bytes[4..].try_into().unwrap()),
+ u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()),
+ )
+ .apply_keystream(metadata.as_mut_slice());
+ }
+
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 {
+ if let Some(metadata) = payment_metadata.as_ref() {
hmac.input(&(metadata.len() as u64).to_le_bytes());
hmac.input(metadata);
}
@@ -184,7 +200,7 @@ pub fn create<ES: EntropySource>(
let ldk_pmt_hash = PaymentHash(Sha256::hash(&payment_preimage_bytes).to_byte_array());
let payment_secret = construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key);
- Ok((ldk_pmt_hash, payment_secret))
+ Ok((ldk_pmt_hash, payment_secret, payment_metadata))
}
/// Equivalent to [`crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash`],
@@ -196,12 +212,15 @@ pub fn create<ES: EntropySource>(
/// Note that if `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable
/// on versions of LDK prior to 0.0.114.
///
+/// Returns an encrypted copy of the `payment_metadata` (if any) which must be included as a part of
+/// validation.
+///
/// [phantom node payments]: crate::sign::PhantomKeysManager
-pub fn create_from_hash(
+pub fn create_from_hash<ES: EntropySource>(
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, ()> {
+ invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64,
+ min_final_cltv_expiry_delta: Option<u16>, mut payment_metadata: Option<Vec<u8>>,
+) -> Result<(PaymentSecret, Option<Vec<u8>>), ()> {
let info_bytes = construct_info_bytes(
min_value_msat,
if min_final_cltv_expiry_delta.is_some() {
@@ -214,10 +233,24 @@ pub fn create_from_hash(
min_final_cltv_expiry_delta,
)?;
+ if let Some(metadata) = payment_metadata.as_mut() {
+ let mut iv_bytes = [0 as u8; IV_LEN];
+ let rand_bytes = entropy_source.get_secure_random_bytes();
+ iv_bytes.copy_from_slice(&rand_bytes[..IV_LEN]);
+
+ ChaCha20::new_from_block(
+ Key::new(keys.metadata_enc_key),
+ Nonce::new(iv_bytes[4..16].try_into().unwrap()),
+ u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()),
+ )
+ .apply_keystream(metadata.as_mut_slice());
+ metadata.extend_from_slice(&iv_bytes);
+ }
+
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 {
+ if let Some(metadata) = payment_metadata.as_ref() {
hmac.input(&(metadata.len() as u64).to_le_bytes());
hmac.input(metadata);
}
@@ -226,7 +259,7 @@ pub fn create_from_hash(
let mut iv_bytes = [0 as u8; IV_LEN];
iv_bytes.copy_from_slice(&hmac_bytes[..IV_LEN]);
- Ok(construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key))
+ Ok((construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key), payment_metadata))
}
pub(crate) fn create_for_spontaneous_payment(
@@ -364,7 +397,8 @@ fn construct_payment_secret(
/// [`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,
- payment_metadata: Option<&[u8]>, highest_seen_timestamp: u64, keys: &ExpandedKey, logger: &L,
+ mut payment_metadata: Option<&mut Vec<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);
@@ -385,7 +419,7 @@ 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 {
+ if let Some(metadata) = payment_metadata.as_deref() {
hmac.input(&(metadata.len() as u64).to_le_bytes());
hmac.input(metadata);
}
@@ -399,6 +433,23 @@ pub(super) fn verify<L: Logger>(
&payment_hash
);
return Err(());
+ };
+
+ if let Some(metadata) = payment_metadata.as_mut() {
+ if metadata.len() < IV_LEN {
+ log_trace!(logger, "payment_metadata was shorter than expected IV. Failing HTLC with payment_hash {payment_hash}");
+ return Err(());
+ }
+ let new_len = metadata.len() - IV_LEN;
+ let (metadata_enc, metadata_iv) = metadata.split_at_mut(new_len);
+
+ ChaCha20::new_from_block(
+ Key::new(keys.metadata_enc_key),
+ Nonce::new(metadata_iv[4..16].try_into().unwrap()),
+ u32::from_le_bytes(metadata_iv[..4].try_into().unwrap()),
+ )
+ .apply_keystream(metadata_enc);
+ metadata.truncate(new_len);
}
},
Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => {
@@ -406,7 +457,7 @@ pub(super) fn verify<L: Logger>(
payment_hash,
&iv_bytes,
&info_bytes,
- payment_metadata,
+ payment_metadata.as_deref().map(Vec::as_slice),
keys,
) {
Ok(preimage) => payment_preimage = Some(preimage),
@@ -420,8 +471,21 @@ pub(super) fn verify<L: Logger>(
return Err(());
},
}
+
+ if let Some(metadata) = payment_metadata {
+ ChaCha20::new_from_block(
+ Key::new(keys.metadata_enc_key),
+ Nonce::new(iv_bytes[4..].try_into().unwrap()),
+ u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()),
+ )
+ .apply_keystream(metadata);
+ }
},
Ok(Method::SpontaneousPayment) => {
+ if payment_metadata.is_some() {
+ log_trace!(logger, "Shouldn't have a payment_metadata for a spontaneous payment, failing payment with hash {payment_hash}");
+ return Err(());
+ }
let mut hmac = HmacEngine::<Sha256>::new(&keys.spontaneous_pmt_key);
hmac.input(&info_bytes[..]);
if !fixed_time_eq(
@@ -470,18 +534,18 @@ pub(super) fn verify<L: Logger>(
}
pub(super) fn get_payment_preimage(
- payment_hash: PaymentHash, payment_secret: PaymentSecret, payment_metadata: Option<&[u8]>,
+ payment_hash: PaymentHash, payment_secret: PaymentSecret, payment_metadata: Option<&mut [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(
+ let preimage = derive_ldk_payment_preimage(
payment_hash,
&iv_bytes,
&info_bytes,
- payment_metadata,
+ payment_metadata.as_deref(),
keys,
)
.map_err(|bad_preimage_bytes| APIError::APIMisuseError {
@@ -490,7 +554,17 @@ pub(super) fn get_payment_preimage(
&payment_hash,
log_bytes!(bad_preimage_bytes)
),
- })
+ })?;
+
+ if let Some(metadata) = payment_metadata {
+ ChaCha20::new_from_block(
+ Key::new(keys.metadata_enc_key),
+ Nonce::new(iv_bytes[4..].try_into().unwrap()),
+ u32::from_le_bytes(iv_bytes[..4].try_into().unwrap()),
+ )
+ .apply_keystream(metadata);
+ }
+ Ok(preimage)
},
Ok(Method::UserPaymentHash) | Ok(Method::UserPaymentHashCustomFinalCltv) => {
Err(APIError::APIMisuseError {
diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs
index 564203b..98996fa 100644
--- a/lightning/src/ln/invoice_utils.rs
+++ b/lightning/src/ln/invoice_utils.rs
@@ -184,11 +184,12 @@ fn _create_phantom_invoice<ES: EntropySource, NS: NodeSigner, L: Logger>(
let keys = node_signer.get_expanded_key();
let (payment_hash, payment_secret) = if let Some(payment_hash) = payment_hash {
- let payment_secret = create_from_hash(
+ let (payment_secret, _no_metadata) = create_from_hash(
&keys,
amt_msat,
payment_hash,
invoice_expiry_delta_secs,
+ &entropy_source,
duration_since_epoch.as_secs(),
min_final_cltv_expiry_delta,
None,
@@ -196,7 +197,7 @@ fn _create_phantom_invoice<ES: EntropySource, NS: NodeSigner, L: Logger>(
.map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
(payment_hash, payment_secret)
} else {
- create(
+ let (payment_hash, payment_secret, _no_metadata) = create(
&keys,
amt_msat,
invoice_expiry_delta_secs,
@@ -205,7 +206,8 @@ fn _create_phantom_invoice<ES: EntropySource, NS: NodeSigner, L: Logger>(
min_final_cltv_expiry_delta,
None,
)
- .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?
+ .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
+ (payment_hash, payment_secret)
};
log_trace!(
@@ -672,8 +674,10 @@ 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, None).unwrap();
+ let preimage = nodes[1]
+ .node
+ .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None)
+ .unwrap();
// Invoice SCIDs should always use inbound SCID aliases over the real channel ID, if one is
// available.
@@ -1258,7 +1262,10 @@ mod test {
let payment_preimage = if user_generated_pmt_hash {
user_payment_preimage
} else {
- nodes[1].node.get_payment_preimage(payment_hash, payment_secret, None).unwrap()
+ nodes[1]
+ .node
+ .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None)
+ .unwrap()
};
assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64);
@@ -1365,7 +1372,7 @@ mod test {
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 2, 100000, 10001);
let payment_amt = 20_000;
- let (payment_hash, _payment_secret) =
+ let (payment_hash, _payment_secret, _) =
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 0bf73db..c066f2c 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::{PaymentHash, PaymentPreimage, PaymentSecret};
+use crate::types::payment::PaymentSecret;
use crate::util::errors::APIError;
use crate::util::ser::Writeable;
use crate::util::test_utils;
@@ -80,36 +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
+ let (payment_hash, payment_secret, encrypted_metadata) = $node
.node
- .create_inbound_payment_for_hash(
+ .create_inbound_payment(Some(amt_msat), 7200, None, Some($metadata))
+ .unwrap();
+ let encrypted_metadata = encrypted_metadata.unwrap();
+ let mut metadata_for_preimage = encrypted_metadata.clone();
+ let payment_preimage = $node
+ .node
+ .get_payment_preimage_decrypt_metadata(
payment_hash,
- Some(amt_msat),
- 7200,
- None,
- Some($metadata),
+ payment_secret,
+ Some(metadata_for_preimage.as_mut_slice()),
)
.unwrap();
- (payment_hash, payment_preimage, payment_secret)
+ (payment_hash, payment_preimage, payment_secret, encrypted_metadata)
}};
}
// Check that the maximum-size metadata is sendable.
- let (payment_hash, payment_preimage, payment_secret) =
- get_payment_hash!(nodes[1], &payment_metadata);
+ let (payment_hash, payment_preimage, payment_secret, encrypted_metadata) =
+ get_payment_hash!(nodes[1], payment_metadata.clone());
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()),
+ payment_metadata: Some(encrypted_metadata),
custom_tlvs: Vec::new(),
total_mpp_amount_msat: amt_msat,
};
@@ -126,6 +123,7 @@ fn large_payment_metadata() {
let args =
PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, events.pop().unwrap())
.with_payment_secret(payment_secret)
+ .with_payment_preimage(payment_preimage)
.with_payment_metadata(payment_metadata.clone());
do_pass_along_path(args);
claim_payment_along_route(ClaimAlongRouteArgs::new(
@@ -137,13 +135,14 @@ 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 (payment_hash_2, _, payment_secret_2) =
- get_payment_hash!(nodes[2], &max_sized_onion.payment_metadata.as_ref().unwrap());
+ let (payment_hash_2, _, payment_secret_2, encrypted_metadata_2) =
+ get_payment_hash!(nodes[2], payment_metadata.clone());
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);
+ max_sized_onion.payment_metadata = Some(encrypted_metadata_2);
let id = PaymentId(payment_hash_2.0);
let mut route_params = route_0_2.route_params.clone().unwrap();
@@ -155,10 +154,11 @@ fn large_payment_metadata() {
// If our payment_metadata contains 1 additional byte, we'll fail prior to pathfinding.
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.payment_metadata.as_mut().map(|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());
+ let mut too_large_metadata = payment_metadata.clone();
+ too_large_metadata.push(42);
+ let (payment_hash_2, _, payment_secret_2, _) = get_payment_hash!(nodes[2], too_large_metadata);
too_large_onion.payment_secret = Some(payment_secret_2);
// First confirm we'll fail to create the onion packet directly.
@@ -194,11 +194,11 @@ 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 (payment_hash_2, payment_preimage_2, payment_secret_2, two_hop_encrypted_metadata) =
+ get_payment_hash!(nodes[2], two_hop_metadata.clone());
let mut onion_allowing_2_hops = RecipientOnionFields {
payment_secret: Some(payment_secret_2),
- payment_metadata: Some(two_hop_metadata.clone()),
+ payment_metadata: Some(two_hop_encrypted_metadata),
custom_tlvs: Vec::new(),
total_mpp_amount_msat: amt_msat,
};
@@ -217,6 +217,7 @@ fn large_payment_metadata() {
let args =
PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash_2, events.pop().unwrap())
.with_payment_secret(payment_secret_2)
+ .with_payment_preimage(payment_preimage_2)
.with_payment_metadata(two_hop_metadata);
do_pass_along_path(args);
claim_payment_along_route(ClaimAlongRouteArgs::new(
diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs
index 2eb5d4e..33c7df9 100644
--- a/lightning/src/ln/payment_tests.rs
+++ b/lightning/src/ln/payment_tests.rs
@@ -25,8 +25,9 @@ use crate::ln::channel::{
EXPIRE_PREV_CONFIG_TICKS,
};
use crate::ln::channelmanager::{
- HTLCForwardInfo, PaymentId, PendingAddHTLCInfo, PendingHTLCRouting, RecentPaymentDetails,
- BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, MPP_TIMEOUT_TICKS,
+ Bolt11InvoiceParameters, HTLCForwardInfo, OptionalBolt11PaymentParams, PaymentId,
+ PendingAddHTLCInfo, PendingHTLCRouting, RecentPaymentDetails, BREAKDOWN_TIMEOUT,
+ MIN_CLTV_EXPIRY_DELTA, MPP_TIMEOUT_TICKS,
};
use crate::ln::msgs;
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent};
@@ -52,6 +53,8 @@ use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::{Secp256k1, SecretKey};
+use lightning_invoice::{Bolt11InvoiceDescription, Description};
+
use crate::prelude::*;
use crate::ln::functional_test_utils;
@@ -1547,7 +1550,7 @@ fn get_ldk_payment_preimage() {
let amt_msat = 60_000;
let expiry_secs = 60 * 60;
- let (payment_hash, payment_secret) =
+ let (payment_hash, payment_secret, _) =
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)
@@ -1560,9 +1563,12 @@ fn get_ldk_payment_preimage() {
nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap();
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, None).unwrap());
+ let preimage = Some(
+ nodes[1]
+ .node
+ .get_payment_preimage_decrypt_metadata(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();
@@ -1572,6 +1578,182 @@ fn get_ldk_payment_preimage() {
claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage.unwrap()));
}
+#[derive(Clone, Copy)]
+enum PaymentMetadataSource {
+ Bolt11Invoice,
+ CreateInboundPayment,
+ CreateInboundPaymentForHash,
+}
+
+fn do_payment_metadata_end_to_end(source: PaymentMetadataSource) {
+ // Generate a payment under each source, send a payment for it from another node, and verify
+ // that the `PaymentClaimable` event sees the (decrypted) payment_metadata that was originally
+ // provided. For sources which generate the preimage on our behalf, also check that
+ // `get_payment_preimage_decrypt_metadata` recovers the preimage and decrypts the metadata.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+ create_announced_chan_between_nodes(&nodes, 0, 1);
+
+ let amt_msat = 50_000;
+ let node_b_id = nodes[1].node.get_our_node_id();
+ let plaintext_metadata = vec![0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04, 0x05];
+
+ // Whenever LDK is computing the preimage itself (the `Bolt11Invoice` and
+ // `CreateInboundPayment` cases), `encrypted_metadata` holds the encrypted bytes so we can feed
+ // them back into `get_payment_preimage_decrypt_metadata` below. For the user-hash case we know
+ // the preimage up front so we stash it in `provided_preimage` instead.
+ let (payment_hash, payment_secret, encrypted_metadata, provided_preimage) = match source {
+ PaymentMetadataSource::Bolt11Invoice => {
+ let description =
+ Bolt11InvoiceDescription::Direct(Description::new("test".to_string()).unwrap());
+ let invoice_params = Bolt11InvoiceParameters {
+ amount_msats: Some(amt_msat),
+ description,
+ payment_metadata: Some(plaintext_metadata.clone()),
+ ..Default::default()
+ };
+ let invoice = nodes[1].node.create_bolt11_invoice(invoice_params).unwrap();
+ let payment_hash = invoice.payment_hash();
+ let payment_secret = *invoice.payment_secret();
+ let encrypted_metadata = invoice.payment_metadata().unwrap().clone();
+ // The encryption must produce different bytes than the plaintext for this test to be
+ // meaningful (otherwise the decryption could be a no-op and we wouldn't notice).
+ assert_ne!(encrypted_metadata, plaintext_metadata);
+
+ nodes[0]
+ .node
+ .pay_for_bolt11_invoice(
+ &invoice,
+ PaymentId(payment_hash.0),
+ None,
+ OptionalBolt11PaymentParams::default(),
+ )
+ .unwrap();
+ (payment_hash, payment_secret, encrypted_metadata, None)
+ },
+ PaymentMetadataSource::CreateInboundPayment => {
+ let (payment_hash, payment_secret, encrypted_metadata) = nodes[1]
+ .node
+ .create_inbound_payment(
+ Some(amt_msat),
+ 7200,
+ None,
+ Some(plaintext_metadata.clone()),
+ )
+ .unwrap();
+ let encrypted_metadata = encrypted_metadata.unwrap();
+ assert_ne!(encrypted_metadata, plaintext_metadata);
+
+ let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV)
+ .with_bolt11_features(nodes[1].node.bolt11_invoice_features())
+ .unwrap();
+ let route_params =
+ RouteParameters::from_payment_params_and_value(payment_params, amt_msat);
+ let route = get_route(&nodes[0], &route_params).unwrap();
+ let onion = RecipientOnionFields {
+ payment_secret: Some(payment_secret),
+ payment_metadata: Some(encrypted_metadata.clone()),
+ custom_tlvs: vec![],
+ total_mpp_amount_msat: amt_msat,
+ };
+ nodes[0]
+ .node
+ .send_payment_with_route(route, payment_hash, onion, PaymentId(payment_hash.0))
+ .unwrap();
+ (payment_hash, payment_secret, encrypted_metadata, None)
+ },
+ PaymentMetadataSource::CreateInboundPaymentForHash => {
+ let payment_preimage = PaymentPreimage([0x77; 32]);
+ let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array());
+ let (payment_secret, encrypted_metadata) = nodes[1]
+ .node
+ .create_inbound_payment_for_hash(
+ payment_hash,
+ Some(amt_msat),
+ 7200,
+ None,
+ Some(plaintext_metadata.clone()),
+ )
+ .unwrap();
+ let encrypted_metadata = encrypted_metadata.unwrap();
+ assert_ne!(encrypted_metadata, plaintext_metadata);
+
+ let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV)
+ .with_bolt11_features(nodes[1].node.bolt11_invoice_features())
+ .unwrap();
+ let route_params =
+ RouteParameters::from_payment_params_and_value(payment_params, amt_msat);
+ let route = get_route(&nodes[0], &route_params).unwrap();
+ let onion = RecipientOnionFields {
+ payment_secret: Some(payment_secret),
+ payment_metadata: Some(encrypted_metadata.clone()),
+ custom_tlvs: vec![],
+ total_mpp_amount_msat: amt_msat,
+ };
+ nodes[0]
+ .node
+ .send_payment_with_route(route, payment_hash, onion, PaymentId(payment_hash.0))
+ .unwrap();
+ (payment_hash, payment_secret, encrypted_metadata, Some(payment_preimage))
+ },
+ };
+
+ check_added_monitors(&nodes[0], 1);
+
+ // For sources where LDK derived the preimage, exercise
+ // `get_payment_preimage_decrypt_metadata`: it must recover the preimage *and* decrypt the
+ // metadata buffer in place. For the user-hash source we just use the preimage we picked.
+ let preimage = if let Some(preimage) = provided_preimage {
+ preimage
+ } else {
+ let mut decrypted_metadata = encrypted_metadata.clone();
+ let preimage = nodes[1]
+ .node
+ .get_payment_preimage_decrypt_metadata(
+ payment_hash,
+ payment_secret,
+ Some(decrypted_metadata.as_mut_slice()),
+ )
+ .unwrap();
+ assert_eq!(decrypted_metadata, plaintext_metadata);
+ preimage
+ };
+ assert_eq!(PaymentHash(Sha256::hash(&preimage.0).to_byte_array()), payment_hash);
+
+ let mut events = nodes[0].node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+ let ev = events.pop().unwrap();
+ let path = &[&nodes[1]];
+ let mut args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev)
+ .with_payment_secret(payment_secret)
+ .with_payment_metadata(plaintext_metadata.clone());
+ // Only set the expected preimage when LDK is responsible for surfacing it on the receiver
+ // side (i.e. LDK-derived hashes). For user-supplied hashes, `PaymentClaimable` carries
+ // `payment_preimage: None`.
+ if provided_preimage.is_none() {
+ args = args.with_payment_preimage(preimage);
+ }
+ do_pass_along_path(args);
+ claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage));
+}
+
+#[test]
+fn payment_metadata_end_to_end_bolt11_invoice() {
+ do_payment_metadata_end_to_end(PaymentMetadataSource::Bolt11Invoice);
+}
+
+#[test]
+fn payment_metadata_end_to_end_create_inbound_payment() {
+ do_payment_metadata_end_to_end(PaymentMetadataSource::CreateInboundPayment);
+}
+
+#[test]
+fn payment_metadata_end_to_end_create_inbound_payment_for_hash() {
+ do_payment_metadata_end_to_end(PaymentMetadataSource::CreateInboundPaymentForHash);
+}
+
#[test]
fn sent_probe_is_probe_of_sending_node() {
let chanmon_cfgs = create_chanmon_cfgs(3);
@@ -2305,7 +2487,7 @@ fn do_test_intercepted_payment(test: InterceptTest) {
let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat);
let route = get_route(&nodes[0], &route_params).unwrap();
- let (hash, payment_secret) =
+ let (hash, payment_secret, _) =
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);
@@ -2415,8 +2597,12 @@ 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, None).unwrap());
+ let preimage = Some(
+ nodes[2]
+ .node
+ .get_payment_preimage_decrypt_metadata(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]]];
@@ -2542,7 +2728,7 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) {
.with_bolt11_features(nodes[2].node.bolt11_invoice_features())
.unwrap();
let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat);
- let (payment_hash, payment_secret) =
+ let (payment_hash, payment_secret, _) =
nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None, None).unwrap();
let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat);
@@ -2598,8 +2784,10 @@ 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, None).unwrap();
+ let payment_preimage = nodes[2]
+ .node
+ .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None)
+ .unwrap();
let events = nodes[2].node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
match events[0] {
@@ -4890,14 +5078,14 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) {
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]
+ let (payment_secret, encrypted_metadata) = nodes[3]
.node
.create_inbound_payment_for_hash(
payment_hash,
Some(amt_msat),
7200,
None,
- Some(&payment_metadata),
+ Some(payment_metadata.clone()),
)
.unwrap();
let payment_id = PaymentId(payment_hash.0);
@@ -4910,7 +5098,7 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) {
// Send the MPP payment, delivering the updated commitment state to nodes[1].
let onion = RecipientOnionFields {
payment_secret: Some(payment_secret),
- payment_metadata: Some(payment_metadata),
+ payment_metadata: encrypted_metadata,
custom_tlvs: vec![],
total_mpp_amount_msat: amt_msat,
};
Why this scored 34/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.