Skip to content

Commit 2dd5107

Browse files
vincenzopalazzorustyrussellclaude
committed
feat(offers): add BOLT 12 payer proof primitives
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 parent a4c898c commit 2dd5107

15 files changed

Lines changed: 3556 additions & 99 deletions

fuzz/src/process_onion_failure.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
122122
first_hop_htlc_msat: 0,
123123
payment_id,
124124
bolt12_invoice: None,
125+
payment_nonce: None,
125126
};
126127

127128
let failure_len = get_u16!();

lightning/src/events/mod.rs

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ use crate::ln::outbound_payment::RecipientOnionFields;
3232
use crate::ln::types::ChannelId;
3333
use crate::offers::invoice::Bolt12Invoice;
3434
use crate::offers::invoice_request::InvoiceRequest;
35+
use crate::offers::nonce::Nonce;
36+
use crate::offers::payer_proof::Bolt12InvoiceType;
37+
pub use crate::offers::payer_proof::PaidBolt12Invoice;
3538
use crate::offers::static_invoice::StaticInvoice;
3639
use crate::onion_message::messenger::Responder;
3740
use crate::routing::gossip::NetworkUpdate;
@@ -1205,17 +1208,13 @@ pub enum Event {
12051208
///
12061209
/// [`Route::get_total_fees`]: crate::routing::router::Route::get_total_fees
12071210
fee_paid_msat: Option<u64>,
1208-
/// The BOLT 12 invoice that was paid. `None` if the payment was a non BOLT 12 payment.
1211+
/// The paid BOLT 12 invoice bundled with the data needed to construct a
1212+
/// [`PayerProof`], which selectively discloses invoice fields to prove payment to a
1213+
/// third party.
12091214
///
1210-
/// The BOLT 12 invoice is useful for proof of payment because it contains the
1211-
/// payment hash. A third party can verify that the payment was made by
1212-
/// showing the invoice and confirming that the payment hash matches
1213-
/// the hash of the payment preimage.
1215+
/// `None` for non-BOLT 12 payments.
12141216
///
1215-
/// However, the [`PaidBolt12Invoice`] can also be of type [`StaticInvoice`], which
1216-
/// is a special [`Bolt12Invoice`] where proof of payment is not possible.
1217-
///
1218-
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
1217+
/// [`PayerProof`]: crate::offers::payer_proof::PayerProof
12191218
bolt12_invoice: Option<PaidBolt12Invoice>,
12201219
},
12211220
/// Indicates an outbound payment failed. Individual [`Event::PaymentPathFailed`] events
@@ -2107,13 +2106,16 @@ impl Writeable for Event {
21072106
ref bolt12_invoice,
21082107
} => {
21092108
2u8.write(writer)?;
2109+
let invoice_type = bolt12_invoice.as_ref().map(|paid| paid.invoice_type());
2110+
let payment_nonce = bolt12_invoice.as_ref().and_then(|paid| paid.nonce());
21102111
write_tlv_fields!(writer, {
21112112
(0, payment_preimage, required),
21122113
(1, payment_hash, required),
21132114
(3, payment_id, option),
21142115
(5, fee_paid_msat, option),
21152116
(7, amount_msat, option),
2116-
(9, bolt12_invoice, option),
2117+
(9, invoice_type, option),
2118+
(11, payment_nonce, option),
21172119
});
21182120
},
21192121
&Event::PaymentPathFailed {
@@ -2605,20 +2607,25 @@ impl MaybeReadable for Event {
26052607
let mut payment_id = None;
26062608
let mut amount_msat = None;
26072609
let mut fee_paid_msat = None;
2608-
let mut bolt12_invoice = None;
2610+
let mut invoice_type: Option<Bolt12InvoiceType> = None;
2611+
let mut payment_nonce: Option<Nonce> = None;
26092612
read_tlv_fields!(reader, {
26102613
(0, payment_preimage, required),
26112614
(1, payment_hash, option),
26122615
(3, payment_id, option),
26132616
(5, fee_paid_msat, option),
26142617
(7, amount_msat, option),
2615-
(9, bolt12_invoice, option),
2618+
(9, invoice_type, option),
2619+
(11, payment_nonce, option),
26162620
});
26172621
if payment_hash.is_none() {
26182622
payment_hash = Some(PaymentHash(
26192623
Sha256::hash(&payment_preimage.0[..]).to_byte_array(),
26202624
));
26212625
}
2626+
let bolt12_invoice = invoice_type.map(|invoice| {
2627+
PaidBolt12Invoice::new(invoice, payment_preimage, payment_nonce)
2628+
});
26222629
Ok(Some(Event::PaymentSent {
26232630
payment_id,
26242631
payment_preimage,
@@ -3278,19 +3285,3 @@ impl<T: EventHandler> EventHandler for Arc<T> {
32783285
self.deref().handle_event(event)
32793286
}
32803287
}
3281-
3282-
/// The BOLT 12 invoice that was paid, surfaced in [`Event::PaymentSent::bolt12_invoice`].
3283-
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3284-
pub enum PaidBolt12Invoice {
3285-
/// The BOLT 12 invoice specified by the BOLT 12 specification,
3286-
/// allowing the user to perform proof of payment.
3287-
Bolt12Invoice(Bolt12Invoice),
3288-
/// The Static invoice, used in the async payment specification update proposal,
3289-
/// where the user cannot perform proof of payment.
3290-
StaticInvoice(StaticInvoice),
3291-
}
3292-
3293-
impl_writeable_tlv_based_enum!(PaidBolt12Invoice,
3294-
{0, Bolt12Invoice} => (),
3295-
{2, StaticInvoice} => (),
3296-
);

lightning/src/ln/async_payments_tests.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentTlvs};
1616
use crate::blinded_path::payment::{DummyTlvs, PaymentContext};
1717
use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS};
1818
use crate::events::{
19-
Event, EventsProvider, HTLCHandlingFailureReason, HTLCHandlingFailureType, PaidBolt12Invoice,
19+
Event, EventsProvider, HTLCHandlingFailureReason, HTLCHandlingFailureType,
2020
PaymentFailureReason, PaymentPurpose,
2121
};
2222
use crate::ln::blinded_payment_tests::{fail_blinded_htlc_backwards, get_blinded_route_parameters};
@@ -1000,7 +1000,7 @@ fn ignore_duplicate_invoice() {
10001000
let keysend_preimage = extract_payment_preimage(&claimable_ev);
10011001
let (res, _) =
10021002
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
1003-
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice.clone())));
1003+
assert_eq!(res.as_ref().and_then(|paid| paid.static_invoice()), Some(&static_invoice));
10041004

10051005
// After paying the static invoice, check that regular invoice received from async recipient is ignored.
10061006
match sender.onion_messenger.peel_onion_message(&invoice_om) {
@@ -1085,7 +1085,7 @@ fn ignore_duplicate_invoice() {
10851085

10861086
// After paying invoice, check that static invoice is ignored.
10871087
let res = claim_payment(sender, route[0], payment_preimage);
1088-
assert_eq!(res, Some(PaidBolt12Invoice::Bolt12Invoice(invoice)));
1088+
assert_eq!(res.as_ref().and_then(|paid| paid.bolt12_invoice()), Some(&invoice));
10891089

10901090
sender.onion_messenger.handle_onion_message(always_online_node_id, &static_invoice_om);
10911091
let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(sender.node);
@@ -1156,7 +1156,7 @@ fn async_receive_flow_success() {
11561156
let keysend_preimage = extract_payment_preimage(&claimable_ev);
11571157
let (res, _) =
11581158
claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage));
1159-
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
1159+
assert_eq!(res.as_ref().and_then(|paid| paid.static_invoice()), Some(&static_invoice));
11601160
}
11611161

11621162
#[test]
@@ -1238,7 +1238,7 @@ fn async_payment_delivers_payment_metadata() {
12381238
let keysend_preimage = extract_payment_preimage(&claimable_ev);
12391239
let (res, _) =
12401240
claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage));
1241-
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
1241+
assert_eq!(res.and_then(|paid| paid.static_invoice().cloned()), Some(static_invoice));
12421242
}
12431243

12441244
#[cfg_attr(feature = "std", ignore)]
@@ -2485,7 +2485,7 @@ fn refresh_static_invoices_for_used_offers() {
24852485
let claimable_ev = do_pass_along_path(args).unwrap();
24862486
let keysend_preimage = extract_payment_preimage(&claimable_ev);
24872487
let res = claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
2488-
assert_eq!(res.0, Some(PaidBolt12Invoice::StaticInvoice(updated_invoice)));
2488+
assert_eq!(res.0.as_ref().and_then(|paid| paid.static_invoice()), Some(&updated_invoice));
24892489
}
24902490

24912491
#[cfg_attr(feature = "std", ignore)]
@@ -2820,7 +2820,7 @@ fn invoice_server_is_not_channel_peer() {
28202820
let claimable_ev = do_pass_along_path(args).unwrap();
28212821
let keysend_preimage = extract_payment_preimage(&claimable_ev);
28222822
let res = claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
2823-
assert_eq!(res.0, Some(PaidBolt12Invoice::StaticInvoice(invoice)));
2823+
assert_eq!(res.0.as_ref().and_then(|paid| paid.static_invoice()), Some(&invoice));
28242824
}
28252825

28262826
#[test]
@@ -3063,7 +3063,7 @@ fn async_payment_e2e() {
30633063
let keysend_preimage = extract_payment_preimage(&claimable_ev);
30643064
let (res, _) =
30653065
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
3066-
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
3066+
assert_eq!(res.as_ref().and_then(|paid| paid.static_invoice()), Some(&static_invoice));
30673067
}
30683068

30693069
#[test]
@@ -3303,7 +3303,7 @@ fn intercepted_hold_htlc() {
33033303
let keysend_preimage = extract_payment_preimage(&claimable_ev);
33043304
let (res, _) =
33053305
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
3306-
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
3306+
assert_eq!(res.as_ref().and_then(|paid| paid.static_invoice()), Some(&static_invoice));
33073307
}
33083308

33093309
#[test]
@@ -3553,7 +3553,7 @@ fn release_htlc_races_htlc_onion_decode() {
35533553
let keysend_preimage = extract_payment_preimage(&claimable_ev);
35543554
let (res, _) =
35553555
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
3556-
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
3556+
assert_eq!(res.as_ref().and_then(|paid| paid.static_invoice()), Some(&static_invoice));
35573557
}
35583558

35593559
#[test]
@@ -3717,5 +3717,5 @@ fn async_payment_e2e_release_before_hold_registered() {
37173717
let keysend_preimage = extract_payment_preimage(&claimable_ev);
37183718
let (res, _) =
37193719
claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage));
3720-
assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice)));
3720+
assert_eq!(res.as_ref().and_then(|paid| paid.static_invoice()), Some(&static_invoice));
37213721
}

lightning/src/ln/channel.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17512,6 +17512,7 @@ mod tests {
1751217512
first_hop_htlc_msat: 548,
1751317513
payment_id: PaymentId([42; 32]),
1751417514
bolt12_invoice: None,
17515+
payment_nonce: None,
1751517516
},
1751617517
skimmed_fee_msat: None,
1751717518
blinding_point: None,
@@ -18021,6 +18022,7 @@ mod tests {
1802118022
first_hop_htlc_msat: 0,
1802218023
payment_id: PaymentId([42; 32]),
1802318024
bolt12_invoice: None,
18025+
payment_nonce: None,
1802418026
};
1802518027
let dummy_outbound_output = OutboundHTLCOutput {
1802618028
htlc_id: 0,

0 commit comments

Comments
 (0)