') + ')', '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('^' + ".*" + ', '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" + ', '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('^' + ".*" + ', '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); } })(); })(); Add BOLT 12 payer proof support by vincenzopalazzo · Pull Request #845 · lightningdevkit/ldk-node · GitHub
Skip to content

Add BOLT 12 payer proof support - #845

Closed
vincenzopalazzo wants to merge 2 commits into
lightningdevkit:mainfrom
vincenzopalazzo:macros/bolt12-pop
Closed

Add BOLT 12 payer proof support#845
vincenzopalazzo wants to merge 2 commits into
lightningdevkit:mainfrom
vincenzopalazzo:macros/bolt12-pop

Conversation

@vincenzopalazzo

@vincenzopalazzovincenzopalazzo commented Mar 25, 2026

Copy link
Copy Markdown
Member

Summary

Adds support for building BOLT 12 payer proofs for previously succeeded outbound BOLT 12 payments:

  • Persist the paid BOLT 12 invoice on PaymentKind::Bolt12Offer / PaymentKind::Bolt12Refund, set from Event::PaymentSent once the payment succeeds.
  • Expose Bolt12Payment::create_payer_proof, which builds a payer proof from the persisted invoice and payment preimage. PayerProofOptions controls which optional invoice fields (offer description, issuer, amount, creation time, extra TLV types) are selectively disclosed, plus an optional proof note.
  • Wire the new PayerProof type and error variants (PayerProofCreationFailed, PayerProofUnavailable, InvalidPayerProof) through the UniFFI surface.

Since the proof is built purely from data persisted in the payment store, payer proofs survive restarts without any additional storage: no new KV namespace and no in-memory side store. Payments that completed via a static invoice (async payments) do not support payer proofs and are rejected with PayerProofUnavailable.

The payer proof primitives are already present in the rust-lightning revision we pin, so this PR does not patch LDK to a fork — Cargo.toml is untouched.

Verification

  • cargo fmt --all -- --check
  • cargo check --lib --tests --benches, cargo check --lib --features uniffi
  • cargo clippy --lib -- -A warnings -D clippy::unwrap_used -A clippy::tabs_in_doc_comments
  • cargo test --lib, cargo test --test upgrade_downgrade_tests
  • cargo test --test integration_tests_rust simple_bolt12_send_receive — extended to build a payer proof from the persisted payment and assert the payment hash, disclosed amount, proof note, and that undisclosed fields stay absent
  • Python UniFFI bindings generation (scripts/uniffi_bindgen_generate_python.sh)

Disclosure

This PR was prepared with AI assistance (Claude Code).

🤖 Generated with Claude Code

@ldk-reviews-bot

ldk-reviews-bot commented Mar 25, 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.

@vincenzopalazzo
vincenzopalazzo marked this pull request as draft March 25, 2026 17:13
@vincenzopalazzovincenzopalazzo changed the title Add BOLT12 payer proof supportWIP: Add BOLT12 payer proof supportMar 25, 2026
@vincenzopalazzo
vincenzopalazzoforce-pushed the macros/bolt12-pop branch 2 times, most recently from 18e0c72 to 312b662CompareMarch 26, 2026 15:42
@tnull
tnull self-requested a review March 26, 2026 17:28
@vincenzopalazzo
vincenzopalazzoforce-pushed the macros/bolt12-pop branch 4 times, most recently from 781d3ee to 0aaab83CompareMarch 26, 2026 18:46
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @tnull! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadsrc/payment/store.rs
(3, quantity, option),
(4, secret, option),
(6, offer_id, required),
(8, bolt12_invoice, option),

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.

As mentioned elsewhere, we probably don't want to do this, at least not before we can use #811

Comment threadsrc/lib.rs Outdated
scorer: Arc<Mutex<Scorer>>,
peer_store: Arc<PeerStore<Arc<Logger>>>,
payment_store: Arc<PaymentStore>,
payer_proof_context_store: Arc<PayerProofContextStore>,

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.

No, I'd rather not add yet another store just for this. Can we store this in the payment metadata store post #811?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

yes OFC, I wanted to have a runnable version in a fork, but currently I am interesting in you giving some feedback on the payer proof API, if you think are good enough!

Thanks after ldk 0.3 I can wait for #811 and vibe code another integration :)

@vincenzopalazzo
vincenzopalazzoforce-pushed the macros/bolt12-pop branch 2 times, most recently from 351cf83 to 82de0c7CompareAugust 11, 2026 10:10
@vincenzopalazzovincenzopalazzo changed the title WIP: Add BOLT12 payer proof supportAdd BOLT 12 payer proof supportAug 11, 2026
@vincenzopalazzo
vincenzopalazzo marked this pull request as ready for review August 11, 2026 17:08
vincenzopalazzoand others added 2 commits August 11, 2026 21:06
Add a `bolt12_invoice` field to `PaymentKind::Bolt12Offer` and
`PaymentKind::Bolt12Refund`, and set it from `Event::PaymentSent` when a
BOLT 12 payment succeeds.
Besides being useful on its own, this gives us everything we need to
build a payer proof for a past payment without keeping any additional
state around: the invoice is persisted alongside the payment and thus
survives restarts.
This commit was written with AI assistance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expose `Bolt12Payment::create_payer_proof`, which builds a BOLT 12 payer
proof for a previously succeeded outbound BOLT 12 payment, with
`PayerProofOptions` controlling which optional invoice fields are
selectively disclosed.
The proof is built purely from data we already persist in the payment
store: the paid BOLT 12 invoice recorded on `PaymentKind::Bolt12Offer` /
`PaymentKind::Bolt12Refund` and the payment preimage. That means payer
proofs survive restarts and we don't need a second, node-lifetime-only
store to keep the invoice context around.
Payments that completed via a static invoice, i.e., async payments, don't
support payer proofs and are rejected with `PayerProofUnavailable`.
Also wires the new `PayerProof` type and the two new error variants
through the UniFFI surface.
This commit was written with AI assistance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tnull
tnull requested review from tnull and removed request for valentinewallaceAugust 12, 2026 10:33
@tnulltnull added this to the 0.8 milestone Aug 12, 2026
@tnulltnull removed this from the 0.8 milestone Aug 12, 2026
@vincenzopalazzo

Copy link
Copy Markdown
MemberAuthor

Closing this in favor or #1045

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.

3 participants

@vincenzopalazzo@ldk-reviews-bot@tnull