Skip to content

Commit to payment_metadata in inbound payment HMAC#4528

Open
TheBlueMatt wants to merge 2 commits intolightningdevkit:mainfrom
TheBlueMatt:2026-03-commit-to-metadata
Open

Commit to payment_metadata in inbound payment HMAC#4528
TheBlueMatt wants to merge 2 commits intolightningdevkit:mainfrom
TheBlueMatt:2026-03-commit-to-metadata

Conversation

@TheBlueMatt
Copy link
Copy Markdown
Collaborator

When payment_metadata is set in a BOLT 11 invoice, users expect to
receive it back as-is in the payment onion. In order to ensure it
isn't tampered with, they presumably will add an HMAC, or worse, not
add one and forget that it can be tampered with.

Instead, here we include it in the HMAC computation for the payment
secret. This ensures that the sender must relay the correct
metadata for the payment to be accepted by the receiver, binding
the metadata to the payment cryptographically.

The metadata is only included in the HMAC when present, so existing
payments without metadata continue to verify correctly. However,
this does break receiving payments with metadata today. On an
upgrade this seems acceptable to me given we have seen almost no
use of payment metadata in practice.

TheBlueMatt and others added 2 commits March 31, 2026 19:18
`payment_metadata` is a separate concept at the BOLT 11 layer
(similar to payment secret, but arbitrary-sized) and at the BOLT 12
layer, so referring to payment information as "payment metadata" is
confusing. Instead, use simply "payment info".
When payment_metadata is set in a BOLT 11 invoice, users expect to
receive it back as-is in the payment onion. In order to ensure it
isn't tampered with, they presumably will add an HMAC, or worse, not
add one and forget that it can be tampered with.

Instead, here we include it in the HMAC computation for the payment
secret. This ensures that the sender must relay the correct
metadata for the payment to be accepted by the receiver, binding
the metadata to the payment cryptographically.

The metadata is only included in the HMAC when present, so existing
payments without metadata continue to verify correctly. However,
this does break receiving payments with metadata today. On an
upgrade this seems acceptable to me given we have seen almost no
use of payment metadata in practice.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Mar 31, 2026

I've assigned @valentinewallace 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.

@@ -0,0 +1,6 @@
# Backwards compat
* Payment metadata is now comitted to in the HMAC used to build payment secrets.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Typo: "comitted" → "committed"

Suggested change
* Payment metadata is now comitted to in the HMAC used to build payment secrets.
* Payment metadata is now committed to in the HMAC used to build payment secrets.

Comment on lines +174 to +176
if let Some(metadata) = payment_metadata {
hmac.input(metadata);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Design consideration: None and Some(&[]) are cryptographically indistinguishable here (and at every other HMAC site in this PR) since hmac.input(&[]) is a no-op. This means:

  • A payment created with payment_metadata = None will verify successfully if the sender sends payment_metadata = Some(vec![]) in the onion (and vice versa).

In practice this seems harmless since empty metadata carries no information, but if you want to formally distinguish "no metadata" from "empty metadata", you could prefix the HMAC input with a flag byte (e.g., hmac.input(&[1u8]) when metadata is Some, hmac.input(&[0u8]) when None).

Comment on lines 14983 to +14985
pub fn get_payment_preimage(
&self, payment_hash: PaymentHash, payment_secret: PaymentSecret,
payment_metadata: Option<&[u8]>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The doc comment for this public API method doesn't mention the new payment_metadata parameter. Callers need to know that if a payment was created with payment_metadata via create_inbound_payment, the same metadata must be provided here for preimage derivation to succeed. Without this documentation, a caller may pass None and get a confusing APIMisuseError.

pub fn create_inbound_payment(
&self, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32,
min_final_cltv_expiry_delta: Option<u16>,
min_final_cltv_expiry_delta: Option<u16>, payment_metadata: Option<&[u8]>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The doc comment for create_inbound_payment doesn't document the new payment_metadata parameter. Worth adding a note explaining that when provided, the metadata is committed to in the payment secret HMAC, so the sender must relay it exactly for payment verification to succeed.

@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

Review Summary

Inline comments posted:

  1. pending_changelog/matt-commit-to-metadata.txt:2 — Typo: "comitted" → "committed"

  2. lightning/src/ln/inbound_payment.rs:174-176None vs Some(&[]) metadata are cryptographically indistinguishable since hmac.input(&[]) is a no-op. This applies to all HMAC sites (create, create_from_hash, verify, derive_ldk_payment_preimage). Not a security issue in practice, but a design consideration.

  3. lightning/src/ln/channelmanager.rs:14983-14985get_payment_preimage public API lacks documentation for the new payment_metadata parameter. Callers who created payments with metadata must pass the same metadata here or get an opaque error.

  4. lightning/src/ln/channelmanager.rs:14904create_inbound_payment public API lacks documentation for the new payment_metadata parameter.

Cross-cutting observations:

  • The core cryptographic change is correct: metadata is consistently included in HMAC computation during creation (create, create_from_hash) and verification (verify, derive_ldk_payment_preimage, get_payment_preimage).

  • create_inbound_payment_for_hash (line 14963-14966) similarly lacks docs for its new payment_metadata parameter.

  • The _create_phantom_invoice function (invoice_utils.rs:194, 207) hardcodes None for payment_metadata. This is fine for now but means phantom invoices cannot use the metadata commitment feature.

  • All test changes correctly thread the new parameter. The get_payment_hash! macro in max_payment_path_len_tests.rs properly generates committed payment secrets for each metadata variant.

Sign up for free to 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.

3 participants