Skip to content

Conversation

@vincenzopalazzo
Copy link
Contributor

This is a first draft implementation of the payer proof extension to BOLT 12 as proposed in lightning/bolts#1295. The goal is to get early feedback on the API design before the spec is finalized.

Payer proofs allow proving that a BOLT 12 invoice was paid by demonstrating possession of:

  • The payment preimage
  • A valid invoice signature over a merkle root
  • The payer's signature

This PR adds the core building blocks:

  • Extends merkle.rs with selective disclosure primitives that allow creating and reconstructing merkle trees with partial TLV disclosure. This enables proving invoice authenticity while omitting sensitive fields.
  • Adds payer_proof.rs with PayerProof, PayerProofBuilder, and UnsignedPayerProof types. The builder pattern allows callers to selectively include invoice fields (description, amount, etc.) in the proof.
  • Implements bech32 encoding/decoding with the lnp prefix and proper TLV stream parsing with validation (ascending order, no duplicates, hash length checks).

This is explicitly a PoC to validate the API surface - the spec itself is still being refined. Looking for feedback on:

  • Whether the builder pattern makes sense for selective disclosure
  • The verification API
  • Integration points with the rest of the offers module

cc @TheBlueMatt @jkczyz

Implements the payer proof extension to BOLT 12 as specified in
lightning/bolts#1295. This allows proving
that a BOLT 12 invoice was paid by demonstrating possession of the
payment preimage, a valid invoice signature, and a payer signature.

Key additions:
- Extend merkle.rs with selective disclosure primitives for creating
  and reconstructing merkle trees with partial TLV disclosure
- Add payer_proof.rs with PayerProof, PayerProofBuilder, and
  UnsignedPayerProof types for building and verifying payer proofs
- Support bech32 encoding with "lnp" prefix
This commit improves the BOLT 12 payer proof implementation:

- Rewrite reconstruct_positions() with a clearer algorithm that tracks
  "continuation vs jump" to reverse the marker encoding. The new
  algorithm is simpler and matches the spec example exactly.

- Fix reconstruct_merkle_root() to correctly handle (None, None) cases
  by propagating None upward instead of pulling from missing_hashes
  immediately. The combined hash is only pulled when an omitted subtree
  meets an included subtree at a higher level.

- Add comprehensive documentation comments explaining:
  - The marker algorithm and how it reverses during reconstruction
  - The missing_hashes order and nonce hash ambiguity

- Add tests for the reconstruction algorithm
This commit addresses issues found during spec review against bolts#1295:

1. Fix missing_hashes ordering to match spec requirement:
   "MUST include the minimal set of merkle hashes... in ascending type order"
   For internal nodes (combined branches), the "type" for ordering is the
   minimum TLV type covered by that subtree. Updated both encoding
   (build_tree_with_disclosure) and decoding (reconstruct_merkle_root).

2. Fix parsing to reject payer proofs containing invreq_metadata (type 0)
   which is prohibited per spec: "MUST NOT include invreq_metadata".

3. Fix payer signature to use proper BOLT 12 tagged hash mechanism:
   - Add PAYER_SIGNATURE_TAG constant
   - Update compute_payer_signature_message to compute:
     inner_msg = SHA256(note || merkle_root)
     final = SHA256(tag_hash || tag_hash || inner_msg)
   This matches the spec's [Signature Calculation] reference.
This commit adds security validations to payer proof parsing:

1. TLV ordering validation: Reject payer proofs with TLVs not in
   strictly ascending order per BOLT 12 requirements. Out-of-order
   TLVs would cause incorrect merkle tree reconstruction.

2. Duplicate TLV detection: Reject payer proofs with duplicate TLV
   types. Previously, duplicates would cause included_records and
   included_types to have inconsistent sizes.

3. Hash length validation: Reject missing_hashes and leaf_hashes
   with lengths not divisible by 32 bytes. Previously, invalid
   lengths were silently truncated.

Adds 4 new unit tests for the validation logic.
@ldk-reviews-bot
Copy link

👋 Hi! I see this is a draft PR.
I'll wait to assign reviewers until you mark it as ready for review.
Just convert it out of draft status when you're ready for review!

@codecov
Copy link

codecov bot commented Jan 5, 2026

Codecov Report

❌ Patch coverage is 59.95829% with 384 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.30%. Comparing base (62c5849) to head (7b9592a).

Files with missing lines Patch % Lines
lightning/src/offers/payer_proof.rs 33.95% 344 Missing and 10 partials ⚠️
lightning/src/offers/merkle.rs 92.90% 25 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4297      +/-   ##
==========================================
- Coverage   89.38%   86.30%   -3.08%     
==========================================
  Files         180      159      -21     
  Lines      139834   102780   -37054     
  Branches   139834   102780   -37054     
==========================================
- Hits       124985    88707   -36278     
+ Misses      12262    11641     -621     
+ Partials     2587     2432     -155     
Flag Coverage Δ
fuzzing 34.82% <0.00%> (-0.39%) ⬇️
tests 85.60% <59.95%> (-3.12%) ⬇️

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.

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.

2 participants