Skip to content

Encrypt payment_metadata when we build the payment secret - #4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt
TheBlueMatt requested a review from tnullMay 20, 2026 20:50
@ldk-reviews-bot

ldk-reviews-bot commented May 20, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMattTheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented May 20, 2026

Copy link
Copy Markdown
Collaborator

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/max_payment_path_len_tests.rs
Comment threadlightning/src/ln/inbound_payment.rs

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@codecov

codecovBot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/inbound_payment.rs91.22%5 Missing ⚠️
lightning/src/ln/channelmanager.rs83.33%2 Missing ⚠️
lightning/src/ln/invoice_utils.rs92.85%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4628 +/- ##
==========================================
+ Coverage 86.58% 86.69% +0.10% 
==========================================
Files 159 159 Lines 110498 110604 +106 Branches 110498 110604 +106 ==========================================
+ Hits 95678 95888 +210 + Misses 12281 12198 -83 + Partials 2539 2518 -21 
FlagCoverage Δ
fuzzing-fake-hashes7.02% <24.13%> (+0.40%)⬆️
fuzzing-real-hashes23.28% <29.88%> (+0.13%)⬆️
tests86.25% <91.66%> (+0.05%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.
Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs+++ b/lightning/src/ln/channelmanager.rs@@ -22134,3 +22134,4 @@ pub mod bench {
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();diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs+++ b/lightning/src/sign/mod.rs@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
use crate::ln::chan_utils;

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt
TheBlueMatt merged commit b7f58cd into lightningdevkit:mainMay 22, 2026
23 of 24 checks passed

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge ACK.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@ldk-claude-review-bot@tnull@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Encrypt `payment_metadata` when we build the payment secret by TheBlueMatt · Pull Request #4628 · lightningdevkit/rust-lightning · GitHub
Skip to content

Encrypt payment_metadata when we build the payment secret - #4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt
TheBlueMatt requested a review from tnullMay 20, 2026 20:50
@ldk-reviews-bot

ldk-reviews-bot commented May 20, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMattTheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented May 20, 2026

Copy link
Copy Markdown
Collaborator

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/max_payment_path_len_tests.rs
Comment threadlightning/src/ln/inbound_payment.rs

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@codecov

codecovBot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/inbound_payment.rs91.22%5 Missing ⚠️
lightning/src/ln/channelmanager.rs83.33%2 Missing ⚠️
lightning/src/ln/invoice_utils.rs92.85%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4628 +/- ##
==========================================
+ Coverage 86.58% 86.69% +0.10% 
==========================================
Files 159 159 Lines 110498 110604 +106 Branches 110498 110604 +106 ==========================================
+ Hits 95678 95888 +210 + Misses 12281 12198 -83 + Partials 2539 2518 -21 
FlagCoverage Δ
fuzzing-fake-hashes7.02% <24.13%> (+0.40%)⬆️
fuzzing-real-hashes23.28% <29.88%> (+0.13%)⬆️
tests86.25% <91.66%> (+0.05%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.
Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs+++ b/lightning/src/ln/channelmanager.rs@@ -22134,3 +22134,4 @@ pub mod bench {
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();diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs+++ b/lightning/src/sign/mod.rs@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
use crate::ln::chan_utils;

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt
TheBlueMatt merged commit b7f58cd into lightningdevkit:mainMay 22, 2026
23 of 24 checks passed

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge ACK.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@ldk-claude-review-bot@tnull@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Encrypt `payment_metadata` when we build the payment secret by TheBlueMatt · Pull Request #4628 · lightningdevkit/rust-lightning · GitHub
Skip to content

Encrypt payment_metadata when we build the payment secret - #4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt
TheBlueMatt requested a review from tnullMay 20, 2026 20:50
@ldk-reviews-bot

ldk-reviews-bot commented May 20, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMattTheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented May 20, 2026

Copy link
Copy Markdown
Collaborator

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/max_payment_path_len_tests.rs
Comment threadlightning/src/ln/inbound_payment.rs

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@codecov

codecovBot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/inbound_payment.rs91.22%5 Missing ⚠️
lightning/src/ln/channelmanager.rs83.33%2 Missing ⚠️
lightning/src/ln/invoice_utils.rs92.85%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4628 +/- ##
==========================================
+ Coverage 86.58% 86.69% +0.10% 
==========================================
Files 159 159 Lines 110498 110604 +106 Branches 110498 110604 +106 ==========================================
+ Hits 95678 95888 +210 + Misses 12281 12198 -83 + Partials 2539 2518 -21 
FlagCoverage Δ
fuzzing-fake-hashes7.02% <24.13%> (+0.40%)⬆️
fuzzing-real-hashes23.28% <29.88%> (+0.13%)⬆️
tests86.25% <91.66%> (+0.05%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.
Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs+++ b/lightning/src/ln/channelmanager.rs@@ -22134,3 +22134,4 @@ pub mod bench {
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();diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs+++ b/lightning/src/sign/mod.rs@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
use crate::ln::chan_utils;

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt
TheBlueMatt merged commit b7f58cd into lightningdevkit:mainMay 22, 2026
23 of 24 checks passed

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge ACK.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@ldk-claude-review-bot@tnull@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Encrypt `payment_metadata` when we build the payment secret by TheBlueMatt · Pull Request #4628 · lightningdevkit/rust-lightning · GitHub
Skip to content

Encrypt payment_metadata when we build the payment secret - #4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt
TheBlueMatt requested a review from tnullMay 20, 2026 20:50
@ldk-reviews-bot

ldk-reviews-bot commented May 20, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMattTheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented May 20, 2026

Copy link
Copy Markdown
Collaborator

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/max_payment_path_len_tests.rs
Comment threadlightning/src/ln/inbound_payment.rs

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@codecov

codecovBot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/inbound_payment.rs91.22%5 Missing ⚠️
lightning/src/ln/channelmanager.rs83.33%2 Missing ⚠️
lightning/src/ln/invoice_utils.rs92.85%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4628 +/- ##
==========================================
+ Coverage 86.58% 86.69% +0.10% 
==========================================
Files 159 159 Lines 110498 110604 +106 Branches 110498 110604 +106 ==========================================
+ Hits 95678 95888 +210 + Misses 12281 12198 -83 + Partials 2539 2518 -21 
FlagCoverage Δ
fuzzing-fake-hashes7.02% <24.13%> (+0.40%)⬆️
fuzzing-real-hashes23.28% <29.88%> (+0.13%)⬆️
tests86.25% <91.66%> (+0.05%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.
Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs+++ b/lightning/src/ln/channelmanager.rs@@ -22134,3 +22134,4 @@ pub mod bench {
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();diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs+++ b/lightning/src/sign/mod.rs@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
use crate::ln::chan_utils;

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt
TheBlueMatt merged commit b7f58cd into lightningdevkit:mainMay 22, 2026
23 of 24 checks passed

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge ACK.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@ldk-claude-review-bot@tnull@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Encrypt `payment_metadata` when we build the payment secret by TheBlueMatt · Pull Request #4628 · lightningdevkit/rust-lightning · GitHub
Skip to content

Encrypt payment_metadata when we build the payment secret - #4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt
TheBlueMatt requested a review from tnullMay 20, 2026 20:50
@ldk-reviews-bot

ldk-reviews-bot commented May 20, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMattTheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented May 20, 2026

Copy link
Copy Markdown
Collaborator

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/max_payment_path_len_tests.rs
Comment threadlightning/src/ln/inbound_payment.rs

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@codecov

codecovBot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/inbound_payment.rs91.22%5 Missing ⚠️
lightning/src/ln/channelmanager.rs83.33%2 Missing ⚠️
lightning/src/ln/invoice_utils.rs92.85%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4628 +/- ##
==========================================
+ Coverage 86.58% 86.69% +0.10% 
==========================================
Files 159 159 Lines 110498 110604 +106 Branches 110498 110604 +106 ==========================================
+ Hits 95678 95888 +210 + Misses 12281 12198 -83 + Partials 2539 2518 -21 
FlagCoverage Δ
fuzzing-fake-hashes7.02% <24.13%> (+0.40%)⬆️
fuzzing-real-hashes23.28% <29.88%> (+0.13%)⬆️
tests86.25% <91.66%> (+0.05%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.
Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs+++ b/lightning/src/ln/channelmanager.rs@@ -22134,3 +22134,4 @@ pub mod bench {
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();diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs+++ b/lightning/src/sign/mod.rs@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
use crate::ln::chan_utils;

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt
TheBlueMatt merged commit b7f58cd into lightningdevkit:mainMay 22, 2026
23 of 24 checks passed

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge ACK.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@ldk-claude-review-bot@tnull@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Encrypt `payment_metadata` when we build the payment secret by TheBlueMatt · Pull Request #4628 · lightningdevkit/rust-lightning · GitHub
Skip to content

Encrypt payment_metadata when we build the payment secret - #4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt
TheBlueMatt requested a review from tnullMay 20, 2026 20:50
@ldk-reviews-bot

ldk-reviews-bot commented May 20, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMattTheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented May 20, 2026

Copy link
Copy Markdown
Collaborator

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/max_payment_path_len_tests.rs
Comment threadlightning/src/ln/inbound_payment.rs

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@codecov

codecovBot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/inbound_payment.rs91.22%5 Missing ⚠️
lightning/src/ln/channelmanager.rs83.33%2 Missing ⚠️
lightning/src/ln/invoice_utils.rs92.85%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4628 +/- ##
==========================================
+ Coverage 86.58% 86.69% +0.10% 
==========================================
Files 159 159 Lines 110498 110604 +106 Branches 110498 110604 +106 ==========================================
+ Hits 95678 95888 +210 + Misses 12281 12198 -83 + Partials 2539 2518 -21 
FlagCoverage Δ
fuzzing-fake-hashes7.02% <24.13%> (+0.40%)⬆️
fuzzing-real-hashes23.28% <29.88%> (+0.13%)⬆️
tests86.25% <91.66%> (+0.05%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.
Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs+++ b/lightning/src/ln/channelmanager.rs@@ -22134,3 +22134,4 @@ pub mod bench {
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();diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs+++ b/lightning/src/sign/mod.rs@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
use crate::ln::chan_utils;

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt
TheBlueMatt merged commit b7f58cd into lightningdevkit:mainMay 22, 2026
23 of 24 checks passed

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge ACK.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@ldk-claude-review-bot@tnull@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Encrypt `payment_metadata` when we build the payment secret by TheBlueMatt · Pull Request #4628 · lightningdevkit/rust-lightning · GitHub
Skip to content

Encrypt payment_metadata when we build the payment secret - #4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt
TheBlueMatt requested a review from tnullMay 20, 2026 20:50
@ldk-reviews-bot

ldk-reviews-bot commented May 20, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMattTheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented May 20, 2026

Copy link
Copy Markdown
Collaborator

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/max_payment_path_len_tests.rs
Comment threadlightning/src/ln/inbound_payment.rs

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@codecov

codecovBot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/inbound_payment.rs91.22%5 Missing ⚠️
lightning/src/ln/channelmanager.rs83.33%2 Missing ⚠️
lightning/src/ln/invoice_utils.rs92.85%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4628 +/- ##
==========================================
+ Coverage 86.58% 86.69% +0.10% 
==========================================
Files 159 159 Lines 110498 110604 +106 Branches 110498 110604 +106 ==========================================
+ Hits 95678 95888 +210 + Misses 12281 12198 -83 + Partials 2539 2518 -21 
FlagCoverage Δ
fuzzing-fake-hashes7.02% <24.13%> (+0.40%)⬆️
fuzzing-real-hashes23.28% <29.88%> (+0.13%)⬆️
tests86.25% <91.66%> (+0.05%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.
Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs+++ b/lightning/src/ln/channelmanager.rs@@ -22134,3 +22134,4 @@ pub mod bench {
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();diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs+++ b/lightning/src/sign/mod.rs@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
use crate::ln::chan_utils;

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt
TheBlueMatt merged commit b7f58cd into lightningdevkit:mainMay 22, 2026
23 of 24 checks passed

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge ACK.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@ldk-claude-review-bot@tnull@joostjager
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Encrypt `payment_metadata` when we build the payment secret by TheBlueMatt · Pull Request #4628 · lightningdevkit/rust-lightning · GitHub
Skip to content

Encrypt payment_metadata when we build the payment secret - #4628

Merged
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally
May 22, 2026
Merged

Encrypt payment_metadata when we build the payment secret#4628
TheBlueMatt merged 2 commits into
lightningdevkit:mainfrom
TheBlueMatt:2026-05-encrypt-metadata-internally

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

In 657ac8f we started committing to the payment_metadata in the payment_secret. We'd largely assumed that downstream code could simply encrypt the payment_metadata itself before passing it to lightning and decrypt before reading it from lightning. However, this presents a challenge - we'd very much love for that downstream code to avoid adding any extra bytes to its payment_metadata if at all possible, but it doesn't have a great way to get a decent IV without simply shoving it in the encrypted payment_metadata.

Instead, here, we encrypt and decrypt the payment_metadata internally in lightning. This allows us to reuse the IV that is used for lightning-generated payment_hashes as the IV for the encrypted payment_metadata as well. Sadly, we don't have any similar IV for user-provided payment_hashes. In that case, we simply accept the limitations and document that users must avoid encrypting multiple payment_metadatas for payments with the same payment_hash. This avoids padding the size of the payment_metadata and should generally not be a material concern - payment_hash reuse should generally not exist anyway, and if it does it should only be in cases where its "the same payment" being retried after failure, at which point payment_metadata should hopefully be the same.

@TheBlueMatt
TheBlueMatt requested a review from tnullMay 20, 2026 20:50
@ldk-reviews-bot

ldk-reviews-bot commented May 20, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@TheBlueMattTheBlueMatt added this to the 0.3 milestone May 20, 2026
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
@ldk-claude-review-bot

ldk-claude-review-bot commented May 20, 2026

Copy link
Copy Markdown
Collaborator

I've thoroughly re-reviewed the entire PR diff, reading all key files. Let me check the memory for any prior issues that might now be resolved or still pending.

The diff is clean. The prior review already captured the meaningful issues, and I verified:

  • Encrypt-then-MAC ordering is correct for both LdkPaymentHash and UserPaymentHash paths
  • HMAC inputs during create/create_from_hash match those in verify (including the metadata length commitment and the IV for UserPaymentHash)
  • ChaCha20 key/nonce/counter extraction in apply_chacha20 is consistent with prior inline usage
  • In-place decryption via Option<&mut Vec<u8>> correctly propagates decrypted metadata to PaymentClaimable events
  • Borrow patterns (e.g., payment_metadata.as_deref().map(Vec::as_slice) before later if let Some(metadata) = payment_metadata) are sound
  • HKDF 8-key expansion follows RFC 5869
  • New do_payment_metadata_end_to_end test covers all three creation paths with encryption round-trip assertions
  • SpontaneousPayment metadata rejection is a correct hardening
  • BOLT 12 and phantom invoice paths correctly pass None for metadata

No new issues found beyond the prior review.

Review Summary

No new issues found beyond those flagged in the prior review pass.

Prior comment status

  • inbound_payment.rs:232 (undocumented 16-byte overhead) — Still valid. create_from_hash appends a 16-byte IV to the encrypted metadata but this is not mentioned in the create_inbound_payment_for_hash docs.
  • payment_tests.rs:1541 (stale function name) — Still valid but outside diff hunk range.
  • channelmanager.rs:15089 (doc typo) — Resolved in current state.
  • channelmanager.rs:14526 (stale comment) — Resolved.
  • max_payment_path_len_tests.rs:109 (IV overflow) — No longer applicable; test switched to create_inbound_payment (same-length encryption).

Verification notes

  • Crypto correctness verified: encrypt-then-MAC ordering, HMAC input consistency, ChaCha20 parameter extraction, nonce uniqueness, metadata length commitment.
  • Edge cases verified: empty metadata (Some(vec![]) vs None), missing metadata on send, extra metadata on send — all correctly rejected by HMAC.
  • In-place decryption correctly strips the appended IV for UserPaymentHash and preserves length for LdkPaymentHash.
  • No timing side channels introduced beyond the pre-existing method-type leakage noted in existing comments.

Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
}

if let Some(metadata) = payment_metadata {
ChaCha20::new_from_block(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following the PaymentMetadata / EncryptedPaymentMetadata state pattern we introduced in lightningdevkit/ldk-node#899?

We intentionally did that to improve readability and to use the type system to ensure we can't ever leak an unencrypted raw Vec<u8> into the metadata field.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing much opportunity to do this. lightning-invoice can't switch types as it has to handle counterparty data, so we have to make it a Vec<u8> again almost immediately. We could do it in PendingHTLCRouting::Receive/ReceiveKeysend but the structure in process_receive_htlcs is a bit annoying and I'm not entirely clear its worth it just for the inbound edge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, okay, I think I would still prefer a bit more structured/typed approach but at the very least it would be good to make this some dedicated utility methods, if only to isolate all the unwraps in a single place rather than sprinkling them everywhere (and reviewers getting used to reading "unwrap").

Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/inbound_payment.rs Outdated
Comment threadlightning/src/ln/channelmanager.rs Outdated
Comment threadlightning/src/ln/max_payment_path_len_tests.rs
Comment threadlightning/src/ln/inbound_payment.rs

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is very failing right now.

@codecov

codecovBot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.69%. Comparing base (1743b99) to head (4fac0fe).
⚠️ Report is 16 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/inbound_payment.rs91.22%5 Missing ⚠️
lightning/src/ln/channelmanager.rs83.33%2 Missing ⚠️
lightning/src/ln/invoice_utils.rs92.85%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4628 +/- ##
==========================================
+ Coverage 86.58% 86.69% +0.10% 
==========================================
Files 159 159 Lines 110498 110604 +106 Branches 110498 110604 +106 ==========================================
+ Hits 95678 95888 +210 + Misses 12281 12198 -83 + Partials 2539 2518 -21 
FlagCoverage Δ
fuzzing-fake-hashes7.02% <24.13%> (+0.40%)⬆️
fuzzing-real-hashes23.28% <29.88%> (+0.13%)⬆️
tests86.25% <91.66%> (+0.05%)⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

joostjager
joostjager previously approved these changes May 22, 2026

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, aside from rustfmt failing

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to squash. rustfmt and bench are unhappy, but otherwise fine by me!

In 657ac8f we started committing
to the `payment_metadata` in the `payment_secret`. We'd largely
assumed that downstream code could simply encrypt the
`payment_metadata` itself before passing it to `lightning` and
decrypt before reading it from `lightning`. However, this presents
a challenge - we'd very much love for that downstream code to avoid
adding any extra bytes to its `payment_metadata` if at all
possible, but it doesn't have a great way to get a decent IV
without simply shoving it in the encrypted `payment_metadata`.
Instead, here, we encrypt and decrypt the `payment_metadata`
internally in `lightning`. This allows us to reuse the IV that is
used for `lightning`-generated `payment_hash`es as the IV for the
encrypted `payment_metadata` as well. Sadly, we don't have any
similar IV for user-provided `payment_hash`es. In that case, we
simply accept the limitations and document that users must avoid
encrypting multiple `payment_metadata`s for payments with the same
`payment_hash`. This avoids padding the size of the
`payment_metadata` and should generally not be a material concern -
`payment_hash` reuse should generally not exist anyway, and if it
does it should only be in cases where its "the same payment" being
retried after failure, at which point `payment_metadata` should
hopefully be the same.
Most of our `chacha20` calls don't actually care about the concept
of ChaCha20's "seek" vs "nonce" - we just want to use the full
128 bits of nonce space as nonce. Here we unify those calls to
keep a consistent API and consolidate the `unwrap`s to one place.
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Fixed and squashed:

$ git diff-tree -U1 cfbf274d56 4fac0fe1c1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index a0da238311..2adb0a1ca5 100644
--- a/lightning/src/ln/channelmanager.rs+++ b/lightning/src/ln/channelmanager.rs@@ -22134,3 +22134,4 @@ pub mod bench {
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();diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs
index 8e682baa43..3adc638029 100644
--- a/lightning/src/sign/mod.rs+++ b/lightning/src/sign/mod.rs@@ -40,3 +40,3 @@ use lightning_invoice::RawBolt11Invoice;
use crate::chain::transaction::OutPoint;
-use crate::crypto::utils::{apply_chacha20 ,hkdf_extract_expand_twice, sign, sign_with_aux_rand};+use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand};
use crate::ln::chan_utils;

@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

No material changes since @tnull said "otherwise fine by me", so just gonna land.

@TheBlueMatt
TheBlueMatt merged commit b7f58cd into lightningdevkit:mainMay 22, 2026
23 of 24 checks passed

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge ACK.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@TheBlueMatt@ldk-reviews-bot@ldk-claude-review-bot@tnull@joostjager