Skip to content
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

[RFC] Implement a way to do BOLT 12 Proof of Payment #3593

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add the bolt12 invoice to the PaymentSend event
This commit make two things possible:

1. make persistent BOLT12 invoice through PendingOutboundPayment

This commit prepares the code to pass down the BOLT12 invoice
inside the `PaymentSent` event.

To achieve this, the `bolt12` field has been added to the
`PendingOutboundPayment::Retryable` enum, allowing it to be
attached to the `PaymentSent` event when the payment is completed.

2. To enable proof of payment, we need to share the bolt12 invoice with the library user.

This is already possible if we `manually_handle_bolt12_invoices`, but
this approach requires a significant amount of work from the user.

This commit adds the bolt12 invoice to the PaymentSend event when
the payment is completed. This allows the user to always have the option to
perform proof of payment.

Link: #3344
Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
vincenzopalazzo committed Mar 21, 2025
commit 6dfb817781d0633e9c949a8d47c01aca4039421c
31 changes: 30 additions & 1 deletion lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
@@ -949,6 +949,17 @@ pub enum Event {
///
/// [`Route::get_total_fees`]: crate::routing::router::Route::get_total_fees
fee_paid_msat: Option<u64>,
/// The BOLT 12 invoice that was paid. `None` if the payment was a non BOLT 12 payment.
///
/// The BOLT 12 invoice is useful for proof of payment because it contains the
/// payment hash. A third party can verify that the payment was made by
/// showing the invoice and confirming that the payment hash matches
/// the hash of the payment preimage.
Comment on lines +954 to +957
Copy link
Contributor

Choose a reason for hiding this comment

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

This won't provide proof-of-payment if a static invoice is provided, could amend the docs for that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, this is the docs for the older version - now should be it, let me know how looks like now, and if you want to do some more iteration on it

///
/// However, the [`PaidInvoice`] can also be of type [`StaticInvoice`], which
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like CI wants you to add a link to StaticInvoice. Alternately, you can import StaticInvoice in this file + not use the full import path in the PaidInvoice variant.

/// is a special [`Bolt12Invoice`] where proof of payment is not possible.
/// For more details, see the `async_payments` specification.
Copy link
Contributor

Choose a reason for hiding this comment

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

The async payments spec isn't merged so since we can't easily link to anything it's probably best to remove this line to avoid confusion. Otherwise these docs look good!

bolt12_invoice: Option<PaidInvoice>,
},
/// Indicates an outbound payment failed. Individual [`Event::PaymentPathFailed`] events
/// provide failure information for each path attempt in the payment, including retries.
@@ -1556,14 +1567,15 @@ impl Writeable for Event {
(13, payment_id, option),
});
},
&Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref amount_msat, ref fee_paid_msat } => {
&Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref amount_msat, ref fee_paid_msat, ref bolt12_invoice } => {
2u8.write(writer)?;
write_tlv_fields!(writer, {
(0, payment_preimage, required),
(1, payment_hash, required),
(3, payment_id, option),
(5, fee_paid_msat, option),
(7, amount_msat, option),
(9, bolt12_invoice, option),
});
},
&Event::PaymentPathFailed {
@@ -1898,12 +1910,14 @@ impl MaybeReadable for Event {
let mut payment_id = None;
let mut amount_msat = None;
let mut fee_paid_msat = None;
let mut bolt12_invoice = None;
read_tlv_fields!(reader, {
(0, payment_preimage, required),
(1, payment_hash, option),
(3, payment_id, option),
(5, fee_paid_msat, option),
(7, amount_msat, option),
(9, bolt12_invoice, option),
});
if payment_hash.is_none() {
payment_hash = Some(PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()));
@@ -1914,6 +1928,7 @@ impl MaybeReadable for Event {
payment_hash: payment_hash.unwrap(),
amount_msat,
fee_paid_msat,
bolt12_invoice,
}))
};
f()
@@ -2438,3 +2453,17 @@ impl<T: EventHandler> EventHandler for Arc<T> {
self.deref().handle_event(event)
}
}

/// The BOLT 12 invoice that was paid, surfaced in [`Event::PaymentSent::bolt12_invoice`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PaidInvoice {
///
Bolt12Invoice(crate::offers::invoice::Bolt12Invoice),
///
StaticInvoice(crate::offers::static_invoice::StaticInvoice),
}

impl_writeable_tlv_based_enum!(PaidInvoice,
{0, Bolt12Invoice} => (),
{2, StaticInvoice} => (),
);
27 changes: 17 additions & 10 deletions lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch, chainmonitor::Persist};
use crate::chain::channelmonitor::ChannelMonitor;
use crate::chain::transaction::OutPoint;
use crate::events::{ClaimedHTLC, ClosureReason, Event, HTLCDestination, PathFailure, PaymentPurpose, PaymentFailureReason};
use crate::events::{ClaimedHTLC, ClosureReason, Event, HTLCDestination, PaidInvoice, PathFailure, PaymentFailureReason, PaymentPurpose};
use crate::events::bump_transaction::{BumpTransactionEvent, BumpTransactionEventHandler, Wallet, WalletSource};
use crate::ln::types::ChannelId;
use crate::types::payment::{PaymentPreimage, PaymentHash, PaymentSecret};
@@ -2294,7 +2294,7 @@ macro_rules! expect_payment_claimed {
pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM=CM>>(node: &H,
expected_payment_preimage: PaymentPreimage, expected_fee_msat_opt: Option<Option<u64>>,
expect_per_path_claims: bool, expect_post_ev_mon_update: bool,
) {
) -> Option<PaidInvoice> {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like we don't currently check that this is set for any static invoices, could you add a check in one of the calls to claim_payment_along_route in async_payments_tests.rs`?

let events = node.node().get_and_clear_pending_events();
let expected_payment_hash = PaymentHash(
bitcoin::hashes::sha256::Hash::hash(&expected_payment_preimage.0).to_byte_array());
@@ -2306,8 +2306,11 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM=CM>>(node: &H,
if expect_post_ev_mon_update {
check_added_monitors(node, 1);
}
// We return the invoice because some test may want to check the invoice details.
#[allow(unused_assignments)]
let mut invoice = None;
let expected_payment_id = match events[0] {
Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref amount_msat, ref fee_paid_msat } => {
Event::PaymentSent { ref payment_id, ref payment_preimage, ref payment_hash, ref amount_msat, ref fee_paid_msat, ref bolt12_invoice } => {
assert_eq!(expected_payment_preimage, *payment_preimage);
assert_eq!(expected_payment_hash, *payment_hash);
assert!(amount_msat.is_some());
@@ -2316,6 +2319,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM=CM>>(node: &H,
} else {
assert!(fee_paid_msat.is_some());
}
invoice = bolt12_invoice.clone();
payment_id.unwrap()
},
_ => panic!("Unexpected event"),
@@ -2331,19 +2335,20 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM=CM>>(node: &H,
}
}
}
invoice
}

#[macro_export]
macro_rules! expect_payment_sent {
($node: expr, $expected_payment_preimage: expr) => {
$crate::expect_payment_sent!($node, $expected_payment_preimage, None::<u64>, true);
$crate::expect_payment_sent!($node, $expected_payment_preimage, None::<u64>, true)
};
($node: expr, $expected_payment_preimage: expr, $expected_fee_msat_opt: expr) => {
$crate::expect_payment_sent!($node, $expected_payment_preimage, $expected_fee_msat_opt, true);
$crate::expect_payment_sent!($node, $expected_payment_preimage, $expected_fee_msat_opt, true)
};
($node: expr, $expected_payment_preimage: expr, $expected_fee_msat_opt: expr, $expect_paths: expr) => {
$crate::ln::functional_test_utils::expect_payment_sent(&$node, $expected_payment_preimage,
$expected_fee_msat_opt.map(|o| Some(o)), $expect_paths, true);
$expected_fee_msat_opt.map(|o| Some(o)), $expect_paths, true)
}
}

@@ -3106,20 +3111,22 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 {

expected_total_fee_msat
}
pub fn claim_payment_along_route(args: ClaimAlongRouteArgs) {
pub fn claim_payment_along_route(args: ClaimAlongRouteArgs) -> Option<PaidInvoice> {
let origin_node = args.origin_node;
let payment_preimage = args.payment_preimage;
let skip_last = args.skip_last;
let expected_total_fee_msat = do_claim_payment_along_route(args);
if !skip_last {
expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat));
expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat))
} else {
None
}
}

pub fn claim_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>], our_payment_preimage: PaymentPreimage) {
pub fn claim_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>], our_payment_preimage: PaymentPreimage) -> Option<PaidInvoice> {
claim_payment_along_route(
ClaimAlongRouteArgs::new(origin_node, &[expected_route], our_payment_preimage)
);
)
}

pub const TEST_FINAL_CLTV: u32 = 70;
30 changes: 17 additions & 13 deletions lightning/src/ln/offers_tests.rs
Original file line number Diff line number Diff line change
@@ -47,7 +47,7 @@ use crate::blinded_path::IntroductionNode;
use crate::blinded_path::message::BlindedMessagePath;
use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext};
use crate::blinded_path::message::{MessageContext, OffersContext};
use crate::events::{ClosureReason, Event, HTLCDestination, PaymentFailureReason, PaymentPurpose};
use crate::events::{ClosureReason, Event, HTLCDestination, PaidInvoice, PaymentFailureReason, PaymentPurpose};
use crate::ln::channelmanager::{Bolt12PaymentError, MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self};
use crate::types::features::Bolt12InvoiceFeatures;
use crate::ln::functional_test_utils::*;
@@ -168,7 +168,7 @@ fn route_bolt12_payment<'a, 'b, 'c>(
}

fn claim_bolt12_payment<'a, 'b, 'c>(
node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext
node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext, invoice: &Bolt12Invoice
) {
let recipient = &path[path.len() - 1];
let payment_purpose = match get_event!(recipient, Event::PaymentClaimable) {
@@ -188,7 +188,11 @@ fn claim_bolt12_payment<'a, 'b, 'c>(
},
_ => panic!("Unexpected payment purpose: {:?}", payment_purpose),
}
claim_payment(node, path, payment_preimage);
if let Some(inv) = claim_payment(node, path, payment_preimage) {
assert_eq!(inv, PaidInvoice::Bolt12Invoice(invoice.to_owned()));
} else {
panic!("Expected PaidInvoice::Bolt12Invoice");
};
}

fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> Nonce {
@@ -597,7 +601,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(david, &[charlie, bob, alice], payment_context);
claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice);
expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id);
}

@@ -680,7 +684,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() {
route_bolt12_payment(david, &[charlie, bob, alice], &invoice);
expect_recent_payment!(david, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(david, &[charlie, bob, alice], payment_context);
claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice);
expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id);
}

@@ -747,7 +751,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(bob, &[alice], payment_context);
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
}

@@ -803,7 +807,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() {
route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(bob, &[alice], payment_context);
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
}

@@ -857,7 +861,7 @@ fn pays_for_offer_without_blinded_paths() {
route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(bob, &[alice], payment_context);
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
}

@@ -900,7 +904,7 @@ fn pays_for_refund_without_blinded_paths() {
route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(bob, &[alice], payment_context);
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
}

@@ -1138,7 +1142,7 @@ fn creates_and_pays_for_offer_with_retry() {
}
route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
claim_bolt12_payment(bob, &[alice], payment_context);
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
}

@@ -1209,7 +1213,7 @@ fn pays_bolt12_invoice_asynchronously() {
route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(bob, &[alice], payment_context);
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);

assert_eq!(
@@ -1289,7 +1293,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() {
route_bolt12_payment(bob, &[alice], &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);

claim_bolt12_payment(bob, &[alice], payment_context);
claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
}

@@ -2145,7 +2149,7 @@ fn fails_paying_invoice_more_than_once() {
assert!(david.node.get_and_clear_pending_msg_events().is_empty());

// Complete paying the first invoice
claim_bolt12_payment(david, &[charlie, bob, alice], payment_context);
claim_bolt12_payment(david, &[charlie, bob, alice], payment_context, &invoice1);
expect_recent_payment!(david, RecentPaymentDetails::Fulfilled, payment_id);
}

Loading