-
Notifications
You must be signed in to change notification settings - Fork 427
[RFC] Add BOLT 12 payer proof primitives #4297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[RFC] Add BOLT 12 payer proof primitives #4297
Conversation
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.
|
👋 Hi! I see this is a draft PR. |
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
TheBlueMatt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few notes, though I didn't dig into the code at a particularly low level.
| const TLV_INVREQ_PAYER_ID: u64 = 88; | ||
| const TLV_INVOICE_PAYMENT_HASH: u64 = 168; | ||
| const TLV_INVOICE_FEATURES: u64 = 174; | ||
| const TLV_INVOICE_NODE_ID: u64 = 176; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: most of these constants can/should be reused elsewhere in the code
| struct PayerProofContents { | ||
| payer_id: PublicKey, | ||
| payment_hash: PaymentHash, | ||
| invoice_node_id: PublicKey, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we refer to this as the "issuer signing pubkey" elsewhere to avoid overloading with the "node id" (the p2p thing) cause we (at a minimum, I guess CLN does not) try hard to avoid using the node id again in bolt 12.
| self.included_types.insert(164); | ||
| self | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably worth having a generic "select by id" thing (maybe requiring it be in the experimental range?), given we'll add support for custom TLVs eventually.
| } | ||
|
|
||
| /// Sign the proof with the payer's key to create a complete proof. | ||
| pub fn sign<F>(self, sign_fn: F, note: Option<&str>) -> Result<PayerProof, PayerProofError> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also have a signing method which uses the automated payer key derivation from an ExpandedKey.
| break; | ||
| } | ||
|
|
||
| let left_positions: Vec<_> = (0..num_leaves).step_by(step).collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha, please don't collect to build a vec.
| #[derive(Clone, Debug, PartialEq)] | ||
| pub struct SelectiveDisclosure { | ||
| /// Nonce hashes for included TLVs (in TLV type order). | ||
| pub leaf_hashes: Vec<sha256::Hash>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused, don't we need the tlv id in this? This doesn't look sufficient.
| let mut is_included: Vec<bool> = vec![false; num_leaves]; | ||
| let mut min_types: Vec<u64> = vec![u64::MAX; num_leaves]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to avoid the unnecessary allocations here.
| let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes())); | ||
|
|
||
| let mut hashes: Vec<Option<sha256::Hash>> = vec![None; num_leaves]; | ||
| let mut is_included: Vec<bool> = vec![false; num_leaves]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, there's lot of allocations that we should be able to avoid.
| #[allow(dead_code)] | ||
| leaf_hashes: Vec<sha256::Hash>, | ||
| #[allow(dead_code)] | ||
| omitted_tlvs: Vec<u64>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be revealed, only the presence of N TLV(s) between X (the included TLV below) and Y (the included TLV above) should be required, no? Its a bit of a privacy leak to reveal the set of TLVs in some cases, I can imagine.
| invoice_created_at: Option<Duration>, | ||
| #[allow(dead_code)] | ||
| invoice_features: Option<Bolt12InvoiceFeatures>, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We presumably want a place to store custom included TLVs.
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:
This PR adds the core building blocks:
This is explicitly a PoC to validate the API surface - the spec itself is still being refined. Looking for feedback on:
cc @TheBlueMatt @jkczyz