BOLT 12: payer proofs#1295
Conversation
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
jkczyz
left a comment
There was a problem hiding this comment.
Some minor comments from a first pass. Should have more feedback once we attempt to implement this.
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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
vincenzopalazzo
left a comment
There was a problem hiding this comment.
Thanks for this spec! I am finishing to implement it in ldk and have some feedback from implementation experience:
- The nonce hash notation needs clarification (see inline comment)
- Test vectors would be extremely valuable for cross-implementation compatibility. Wondering if you already had some draft implementation where we can compare the tests vectors?
Happy to provide my implementation's test vectors once the ambiguities are resolved.
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
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
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
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
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
Add a Rust CLI tool that generates and verifies test vectors for BOLT 12 payer proofs as specified in lightning/bolts#1295. The tool uses the rust-lightning implementation from lightningdevkit/rust-lightning#4297. Features: - Generate deterministic test vectors with configurable seed - Verify test vectors from JSON files - Support for basic proofs, proofs with notes, and invalid test cases - Uses refund flow for explicit payer key control Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A PayerProof cryptographically proves that a BOLT 12 invoice was paid by demonstrating possession of the payment preimage, a valid invoice signature over a merkle root (via selective disclosure), and a payer signature proving who authorized the payment. Key components: - `PayerProofBuilder`: constructs proofs with selective disclosure, supporting both direct signing and derived key signing from `ExpandedKey` + `Nonce` - `PayerProof`: verifiable proof with bech32 encoding (lnp HRP) - Selective disclosure via merkle tree: omitted TLV fields are represented by minimized markers and missing hashes, allowing verification of the invoice signature without revealing all fields - `compute_selective_disclosure` / `reconstruct_merkle_root`: build and verify merkle proofs using n-node in-place tree traversal
Implements payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A payer proof cryptographically demonstrates that a BOLT 12 invoice was paid using selective disclosure of invoice fields, the payment preimage, and signatures from both the invoice issuer and the payer.
Implements payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A payer proof cryptographically demonstrates that a BOLT 12 invoice was paid using selective disclosure of invoice fields, the payment preimage, and signatures from both the invoice issuer and the payer.
Implement payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A payer proof cryptographically demonstrates that a BOLT 12 invoice was paid using selective disclosure of invoice fields, the payment preimage, and signatures from both the invoice issuer and the payer. The selective disclosure mechanism uses a merkle tree over the invoice's TLV fields, allowing the payer to reveal only chosen fields while proving the full invoice was signed by the issuer. Privacy-preserving omitted markers hide the actual TLV type numbers of undisclosed fields. PayerProofBuilder provides two signing modes: an explicit signing function for callers with direct key access, and automatic key re-derivation from ExpandedKey + Nonce + PaymentId for the common case where invoice requests used deriving_signing_pubkey. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A payer proof cryptographically demonstrates that a BOLT 12 invoice was paid using selective disclosure of invoice fields, the payment preimage, and signatures from both the invoice issuer and the payer. The selective disclosure mechanism uses a merkle tree over the invoice's TLV fields, allowing the payer to reveal only chosen fields while proving the full invoice was signed by the issuer. Privacy-preserving omitted markers hide the actual TLV type numbers of undisclosed fields. PayerProofBuilder provides two signing modes: an explicit signing function for callers with direct key access, and automatic key re-derivation from ExpandedKey + Nonce + PaymentId for the common case where invoice requests used deriving_signing_pubkey. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A payer proof cryptographically demonstrates that a BOLT 12 invoice was paid using selective disclosure of invoice fields, the payment preimage, and signatures from both the invoice issuer and the payer. The selective disclosure mechanism uses a merkle tree over the invoice's TLV fields, allowing the payer to reveal only chosen fields while proving the full invoice was signed by the issuer. Privacy-preserving omitted markers hide the actual TLV type numbers of undisclosed fields. PayerProofBuilder provides two signing modes: an explicit signing function for callers with direct key access, and automatic key re-derivation from ExpandedKey + Nonce + PaymentId for the common case where invoice requests used deriving_signing_pubkey. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I created a tool for the test vectors https://github.com/vincenzopalazzo/payer-proof-test-vectors, and this is the most recent one https://github.com/vincenzopalazzo/payer-proof-test-vectors/blob/main/test_vectors.json |
Implement payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A payer proof cryptographically demonstrates that a BOLT 12 invoice was paid using selective disclosure of invoice fields, the payment preimage, and signatures from both the invoice issuer and the payer. The selective disclosure mechanism uses a merkle tree over the invoice's TLV fields, allowing the payer to reveal only chosen fields while proving the full invoice was signed by the issuer. Privacy-preserving omitted markers hide the actual TLV type numbers of undisclosed fields. PayerProofBuilder provides two signing modes: an explicit signing function for callers with direct key access, and automatic key re-derivation from ExpandedKey + Nonce + PaymentId for the common case where invoice requests used deriving_signing_pubkey. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A payer proof cryptographically demonstrates that a BOLT 12 invoice was paid using selective disclosure of invoice fields, the payment preimage, and signatures from both the invoice issuer and the payer. The selective disclosure mechanism uses a merkle tree over the invoice's TLV fields, allowing the payer to reveal only chosen fields while proving the full invoice was signed by the issuer. Privacy-preserving omitted markers hide the actual TLV type numbers of undisclosed fields. PayerProofBuilder provides two signing modes: an explicit signing function for callers with direct key access, and automatic key re-derivation from ExpandedKey + Nonce + PaymentId for the common case where invoice requests used deriving_signing_pubkey. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement payer proofs for BOLT 12 invoices as specified in lightning/bolts#1295. A payer proof cryptographically demonstrates that a BOLT 12 invoice was paid using selective disclosure of invoice fields, the payment preimage, and signatures from both the invoice issuer and the payer. The selective disclosure mechanism uses a merkle tree over the invoice's TLV fields, allowing the payer to reveal only chosen fields while proving the full invoice was signed by the issuer. Privacy-preserving omitted markers hide the actual TLV type numbers of undisclosed fields. PayerProofBuilder provides two signing modes: an explicit signing function for callers with direct key access, and automatic key re-derivation from ExpandedKey + Nonce + PaymentId for the common case where invoice requests used deriving_signing_pubkey. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
As specified in lightning/bolts#1295
As specified in lightning/bolts#1295
d6dbb9d to
1553230
Compare
Test vectors, fixed and expanded to cover more tests:
Successful cases:
full_disclosure
minimal_disclosure
with_note
left_subtree_omitted
empty_proof_omitted_tlvs_explicit
Failing cases:
missing_invreq_payer_id
missing_invoice_payment_hash
missing_invoice_node_id
missing_signature
missing_proof_preimage
missing_proof_missing_hashes
missing_proof_leaf_hashes
missing_proof_signature
wrong_proof_preimage
proof_omitted_tlvs_not_ascending
proof_omitted_tlvs_contains_zero
proof_omitted_tlvs_contains_signature_field
proof_omitted_tlvs_contains_proof_field
proof_omitted_tlvs_contains_high_field
proof_omitted_tlvs_contains_included_tlv_field
proof_omitted_tlvs_not_sequential
proof_leaf_hashes_too_few
proof_leaf_hashes_too_many
proof_missing_hashes_too_few
proof_missing_hashes_too_many
wrong_invoice_signature
wrong_proof_signature
contains_invreq_metadata
1553230 to
93c7f98
Compare
I've buffed up the test vectors more, updated with latest names etc. How do they look? |
As specified in lightning/bolts#1295
Add the payer proof types, selective disclosure merkle support, parsing, and tests for constructing and validating BOLT 12 payer proofs from invoices. This implements the payer proof extension to BOLT 12 as specified in lightning/bolts#1295. Missing hashes in a proof are emitted in the DFS traversal order defined by the spec. The BOLT 12 payer proof spec test vectors from bolt12/payer-proof-test.json (full disclosure, minimal disclosure, with payer note, and left-subtree omitted) validate the end-to-end output. The parser rejects unknown even TLVs in every sub-stream range (offer, invoice request, invoice, payer-proof/signature, and the three experimental ranges) via the `tlv_stream!` macro's unknown-even fallback, and rejects types in the unused gap between the signature range and the experimental ranges via the all-bytes-consumed check in `ParsedMessage::try_from`. Co-Authored-By: Rusty Russell <rusty@rustcorp.com.au> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Drop the dummy tlv0: the first invoice TLV will serve just as well. @t-bast 2. Don't accidentally put 240 in the `proof_omitted_tlvs` (not possible today, since 239 isn't defined, but could be in future. @yyforyongyu 3. Remove TODO we've TODOne. @yyforyongyu
|
Fixed bad field printing, removed dummy tlv0 for simplicity. How do test vectors look now? You can see the C code in my branch: ElementsProject/lightning#9144 Note: LLMs are really good at divining differences between two code bases, if you get different results for test vectors... |
As specified in lightning/bolts#1295
Good idea, it's just simpler to use the first TLV in all cases, regardless of whether it's an
They work for me now, eclair's implementation agrees with them 👍 |
Add the payer proof types, selective disclosure merkle support, parsing, and tests for constructing and validating BOLT 12 payer proofs from invoices. This implements the payer proof extension to BOLT 12 as specified in lightning/bolts#1295. Missing hashes in a proof are emitted in the DFS traversal order defined by the spec. The BOLT 12 payer proof spec test vectors from bolt12/payer-proof-test.json (full disclosure, minimal disclosure, with payer note, and left-subtree omitted) validate the end-to-end output. The parser rejects unknown even TLVs in every sub-stream range (offer, invoice request, invoice, payer-proof/signature, and the three experimental ranges) via the `tlv_stream!` macro's unknown-even fallback, and rejects types in the unused gap between the signature range and the experimental ranges via the all-bytes-consumed check in `ParsedMessage::try_from`. Co-Authored-By: Rusty Russell <rusty@rustcorp.com.au> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the payer proof types, selective disclosure merkle support, parsing, and tests for constructing and validating BOLT 12 payer proofs from invoices. This implements the payer proof extension to BOLT 12 as specified in lightning/bolts#1295. Missing hashes in a proof are emitted in the DFS traversal order defined by the spec. The BOLT 12 payer proof spec test vectors from bolt12/payer-proof-test.json (full disclosure, minimal disclosure, with payer note, and left-subtree omitted) validate the end-to-end output. The parser rejects unknown even TLVs in every sub-stream range (offer, invoice request, invoice, payer-proof/signature, and the three experimental ranges) via the `tlv_stream!` macro's unknown-even fallback, and rejects types in the unused gap between the signature range and the experimental ranges via the all-bytes-consumed check in `ParsedMessage::try_from`. Co-Authored-By: Rusty Russell <rusty@rustcorp.com.au> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| - `proof_omitted_tlvs` contains 0. | ||
| - `proof_omitted_tlvs` contains number outside both ranges 1 to 239 and 1000000000 to 3999999999. | ||
| - `proof_omitted_tlvs` contains the number of an included TLV field. | ||
| - `proof_omitted_tlvs` is not one greater than: |
There was a problem hiding this comment.
This reader rule contradicts the writer's marker number rule above. The writer emits 1000000000 as the marker number when the last proof_omitted_tlvs entry is 239, but this rule rejects any entry that is not one greater than an included TLV number or the previous entry.
1000000000 is neither: it cannot be <included TLV> + 1, since that would require an included TLV of 999999999, which is outside both valid ranges. So a reader applying this rule literally rejects a proof_omitted_tlvs that the writer rule mandates.
This is latent across implementations (CLN / eclair / LDK) because no test vector has enough consecutive omitted TLVs to reach the 239 -> 1000000000 jump.
Suggest a carve-out mirroring the writer, e.g. also allow an entry equal to 1000000000 where the previous entry is 239.
Add the payer proof types, selective disclosure merkle support, parsing, and tests for constructing and validating BOLT 12 payer proofs from invoices. This implements the payer proof extension to BOLT 12 as specified in lightning/bolts#1295. Missing hashes in a proof are emitted in the DFS traversal order defined by the spec. The BOLT 12 payer proof spec test vectors from bolt12/payer-proof-test.json (full disclosure, minimal disclosure, with payer note, and left-subtree omitted) validate the end-to-end output. The parser rejects unknown even TLVs in every sub-stream range (offer, invoice request, invoice, payer-proof/signature, and the three experimental ranges) via the `tlv_stream!` macro's unknown-even fallback, and rejects types in the unused gap between the signature range and the experimental ranges via the all-bytes-consumed check in `ParsedMessage::try_from`. Co-Authored-By: Rusty Russell <rusty@rustcorp.com.au> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ontradiction This test deliberately fails. It documents the contradiction between the BOLT 12 PR lightningdevkit#1295 writer rule (lines 1041-1049, which mandates the 239 -> 1000000000 marker jump for `proof_omitted_tlvs`) and the reader rule (lines 1065-1067, which rejects any entry that is not equal to `prev + 1` or `included + 1`). The test feeds `compute_omitted_markers`' output to a stripped-down literal transcription of reader rule 1065-1067 and asserts the sequence is accepted -- which it is not, demonstrating the spec contradiction. LDK's actual reader follows the writer rule's intent via the shared `next_marker` helper, so LDK itself round-trips fine. This test exists purely as executable documentation of the spec-text issue raised in: lightning/bolts#1295 (comment) It will pass once the spec amendment lands; at that point either update the inline literal-rule transcription or drop the test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a discussion-only sketch for a payee-side proof of payment, mirroring the machinery proposed in lightning/bolts#1295 (payer proofs by rustyrussell) but signed by invoice_node_id. Design summary: - Selective disclosure reuses the BOLT 12 merkle commitment used by #1295 (proof_omitted_tlvs / proof_missing_hashes / proof_leaf_hashes), so a single library can verify either side. - Verifier challenge (proof_verifier_nonce + optional proof_statement) is required, signed by the payee under tagged_hash('lightning/payee_proof/v1', merkle_root || payment_hash || verifier_nonce || sha256(statement)). - Signing key is invoice_node_id (BOLT 12 TLV 176): either the merchant's offer_issuer_id, or the blinded final_node_id when the invoice was served over offer_paths. The blinded-path case gives the proof privacy-for-free (no de-anonymisation of the merchant's persistent node id). - Wire encoding: bech32 lnpp1... mirroring #1295's lnp1... This solves Open Question lightning#1 ('Proof of payment') of bLIP-0056 modulo one remaining issue: in the blinded-path case the payee must keep the blinded private key alive (or deterministically re-derive it) to re-sign proofs on demand, which is non-trivial for stateless-inbound payees like LDK. That question is left open in the sketch. Not a numbered bLIP yet — the intent is to circulate the design, decide whether it belongs as a BOLT 12 sibling section to #1295 or as bLIP-0057, then promote.
Needs: