Skip to content

Commit ee08962

Browse files
event: expose BOLT12 invoice in PaymentSuccessful for proof of payment
This patch adds the `bolt12_invoice` field to the `PaymentSuccessful` event, enabling users to obtain proof of payment for BOLT12 transactions. Problem: Previously, after a successful BOLT12 payment, users had no way to access the paid invoice data. This made it impossible to provide proof of payment to third parties, who need both the payment preimage and the original invoice to verify that sha256(preimage) matches the invoice's payment_hash. Solution: Add a `bolt12_invoice: Option<Vec<u8>>` field to `PaymentSuccessful` that contains the serialized BOLT12 invoice bytes. The invoice is serialized using LDK's standard encoding, which can be parsed back using `Bolt12Invoice::try_from(bytes)` in native Rust, or by hex-encoding the bytes and using `Bolt12Invoice.from_str()` in FFI bindings. Design decisions: - Store as `Vec<u8>` rather than the complex `PaidBolt12Invoice` type to avoid UniFFI limitations with objects in enum variants - Return `None` for `StaticInvoice` (async payments) since proof of payment is not possible for those payment types anyway - Use TLV tag 7 for serialization, maintaining backward compatibility with existing persisted events This implementation follows the maintainer guidance from PR #563 to expose the invoice via the event rather than storing it in the payment store. Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
1 parent bbefa73 commit ee08962

File tree

3 files changed

+103
-2
lines changed

3 files changed

+103
-2
lines changed

bindings/ldk_node.udl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ enum VssHeaderProviderError {
400400

401401
[Enum]
402402
interface Event {
403-
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat);
403+
PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat, sequence<u8>? bolt12_invoice);
404404
PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason);
405405
PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence<CustomTlvRecord> custom_records);
406406
PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline, sequence<CustomTlvRecord> custom_records);

src/event.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ use bitcoin::secp256k1::PublicKey;
1616
use bitcoin::{Amount, OutPoint};
1717
use lightning::events::bump_transaction::BumpTransactionEvent;
1818
use lightning::events::{
19-
ClosureReason, Event as LdkEvent, PaymentFailureReason, PaymentPurpose, ReplayEvent,
19+
ClosureReason, Event as LdkEvent, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose,
20+
ReplayEvent,
2021
};
2122
use lightning::impl_writeable_tlv_based_enum;
2223
use lightning::ln::channelmanager::PaymentId;
@@ -75,6 +76,17 @@ pub enum Event {
7576
payment_preimage: Option<PaymentPreimage>,
7677
/// The total fee which was spent at intermediate hops in this payment.
7778
fee_paid_msat: Option<u64>,
79+
/// The BOLT12 invoice that was paid, serialized as bytes.
80+
///
81+
/// This is useful for proof of payment. A third party can verify that the payment was made
82+
/// by checking that the `payment_hash` in the invoice matches `sha256(payment_preimage)`.
83+
///
84+
/// Will be `None` for non-BOLT12 payments, or for async payments (`StaticInvoice`)
85+
/// where proof of payment is not possible.
86+
///
87+
/// To parse the invoice in native Rust, use `Bolt12Invoice::try_from(bytes)`.
88+
/// In FFI bindings, hex-encode the bytes and use `Bolt12Invoice.from_str(hex_string)`.
89+
bolt12_invoice: Option<Vec<u8>>,
7890
},
7991
/// A sent payment has failed.
8092
PaymentFailed {
@@ -264,6 +276,7 @@ impl_writeable_tlv_based_enum!(Event,
264276
(1, fee_paid_msat, option),
265277
(3, payment_id, option),
266278
(5, payment_preimage, option),
279+
(7, bolt12_invoice, option),
267280
},
268281
(1, PaymentFailed) => {
269282
(0, payment_hash, option),
@@ -1022,6 +1035,7 @@ where
10221035
payment_preimage,
10231036
payment_hash,
10241037
fee_paid_msat,
1038+
bolt12_invoice,
10251039
..
10261040
} => {
10271041
let payment_id = if let Some(id) = payment_id {
@@ -1062,11 +1076,20 @@ where
10621076
hex_utils::to_string(&payment_preimage.0)
10631077
);
10641078
});
1079+
1080+
// Serialize the BOLT12 invoice to bytes for proof of payment.
1081+
// Only Bolt12Invoice supports proof of payment; StaticInvoice does not.
1082+
let bolt12_invoice_bytes = bolt12_invoice.and_then(|inv| match inv {
1083+
PaidBolt12Invoice::Bolt12Invoice(invoice) => Some(invoice.encode()),
1084+
PaidBolt12Invoice::StaticInvoice(_) => None,
1085+
});
1086+
10651087
let event = Event::PaymentSuccessful {
10661088
payment_id: Some(payment_id),
10671089
payment_hash,
10681090
payment_preimage: Some(payment_preimage),
10691091
fee_paid_msat,
1092+
bolt12_invoice: bolt12_invoice_bytes,
10701093
};
10711094

10721095
match self.event_queue.add_event(event).await {

tests/integration_tests_rust.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use ldk_node::payment::{
3333
};
3434
use ldk_node::{Builder, Event, NodeError};
3535
use lightning::ln::channelmanager::PaymentId;
36+
use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice;
3637
use lightning::routing::gossip::{NodeAlias, NodeId};
3738
use lightning::routing::router::RouteParametersConfig;
3839
use lightning_invoice::{Bolt11InvoiceDescription, Description};
@@ -1303,6 +1304,83 @@ async fn simple_bolt12_send_receive() {
13031304
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount));
13041305
}
13051306

1307+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1308+
async fn bolt12_proof_of_payment() {
1309+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1310+
let chain_source = TestChainSource::Esplora(&electrsd);
1311+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
1312+
1313+
let address_a = node_a.onchain_payment().new_address().unwrap();
1314+
let premine_amount_sat = 5_000_000;
1315+
premine_and_distribute_funds(
1316+
&bitcoind.client,
1317+
&electrsd.client,
1318+
vec![address_a],
1319+
Amount::from_sat(premine_amount_sat),
1320+
)
1321+
.await;
1322+
1323+
node_a.sync_wallets().unwrap();
1324+
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await;
1325+
1326+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
1327+
1328+
node_a.sync_wallets().unwrap();
1329+
node_b.sync_wallets().unwrap();
1330+
1331+
expect_channel_ready_event!(node_a, node_b.node_id());
1332+
expect_channel_ready_event!(node_b, node_a.node_id());
1333+
1334+
// Sleep until we broadcasted a node announcement.
1335+
while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() {
1336+
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1337+
}
1338+
1339+
// Sleep one more sec to make sure the node announcement propagates.
1340+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
1341+
1342+
let expected_amount_msat = 100_000_000;
1343+
let offer = node_b
1344+
.bolt12_payment()
1345+
.receive(expected_amount_msat, "proof of payment test", None, Some(1))
1346+
.unwrap();
1347+
let payment_id =
1348+
node_a.bolt12_payment().send(&offer, Some(1), Some("Test".to_string()), None).unwrap();
1349+
1350+
// Wait for payment and verify proof of payment
1351+
match node_a.next_event_async().await {
1352+
Event::PaymentSuccessful {
1353+
payment_id: event_payment_id,
1354+
payment_hash,
1355+
payment_preimage,
1356+
fee_paid_msat: _,
1357+
bolt12_invoice,
1358+
} => {
1359+
assert_eq!(event_payment_id, Some(payment_id));
1360+
1361+
// Verify proof of payment: sha256(preimage) == payment_hash
1362+
let preimage = payment_preimage.expect("preimage should be present");
1363+
let computed_hash = Sha256Hash::hash(&preimage.0);
1364+
assert_eq!(PaymentHash(computed_hash.to_byte_array()), payment_hash);
1365+
1366+
// Verify the BOLT12 invoice is present and contains the correct payment hash
1367+
let invoice_bytes =
1368+
bolt12_invoice.expect("bolt12_invoice should be present for BOLT12 payments");
1369+
let invoice = LdkBolt12Invoice::try_from(invoice_bytes)
1370+
.expect("should be able to parse invoice from bytes");
1371+
assert_eq!(invoice.payment_hash(), payment_hash);
1372+
assert_eq!(invoice.amount_msats(), expected_amount_msat);
1373+
1374+
node_a.event_handled().unwrap();
1375+
},
1376+
ref e => {
1377+
panic!("Unexpected event: {:?}", e);
1378+
},
1379+
}
1380+
1381+
expect_payment_received_event!(node_b, expected_amount_msat);
1382+
}
1383+
13061384
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
13071385
async fn async_payment() {
13081386
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)