Skip to content

Commit d79de9d

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 lightningdevkit#563 to expose the invoice via the event rather than storing it in the payment store. Signed-off-by: Vincenzo Palazzo <[email protected]>
1 parent bbefa73 commit d79de9d

File tree

3 files changed

+104
-2
lines changed

3 files changed

+104
-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: 25 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;
@@ -54,6 +55,7 @@ use crate::{
5455
UserChannelId,
5556
};
5657

58+
5759
/// An event emitted by [`Node`], which should be handled by the user.
5860
///
5961
/// [`Node`]: [`crate::Node`]
@@ -75,6 +77,17 @@ pub enum Event {
7577
payment_preimage: Option<PaymentPreimage>,
7678
/// The total fee which was spent at intermediate hops in this payment.
7779
fee_paid_msat: Option<u64>,
80+
/// The BOLT12 invoice that was paid, serialized as bytes.
81+
///
82+
/// This is useful for proof of payment. A third party can verify that the payment was made
83+
/// by checking that the `payment_hash` in the invoice matches `sha256(payment_preimage)`.
84+
///
85+
/// Will be `None` for non-BOLT12 payments, or for async payments (`StaticInvoice`)
86+
/// where proof of payment is not possible.
87+
///
88+
/// To parse the invoice in native Rust, use `Bolt12Invoice::try_from(bytes)`.
89+
/// In FFI bindings, hex-encode the bytes and use `Bolt12Invoice.from_str(hex_string)`.
90+
bolt12_invoice: Option<Vec<u8>>,
7891
},
7992
/// A sent payment has failed.
8093
PaymentFailed {
@@ -264,6 +277,7 @@ impl_writeable_tlv_based_enum!(Event,
264277
(1, fee_paid_msat, option),
265278
(3, payment_id, option),
266279
(5, payment_preimage, option),
280+
(7, bolt12_invoice, option),
267281
},
268282
(1, PaymentFailed) => {
269283
(0, payment_hash, option),
@@ -1022,6 +1036,7 @@ where
10221036
payment_preimage,
10231037
payment_hash,
10241038
fee_paid_msat,
1039+
bolt12_invoice,
10251040
..
10261041
} => {
10271042
let payment_id = if let Some(id) = payment_id {
@@ -1062,11 +1077,20 @@ where
10621077
hex_utils::to_string(&payment_preimage.0)
10631078
);
10641079
});
1080+
1081+
// Serialize the BOLT12 invoice to bytes for proof of payment.
1082+
// Only Bolt12Invoice supports proof of payment; StaticInvoice does not.
1083+
let bolt12_invoice_bytes = bolt12_invoice.and_then(|inv| match inv {
1084+
PaidBolt12Invoice::Bolt12Invoice(invoice) => Some(invoice.encode()),
1085+
PaidBolt12Invoice::StaticInvoice(_) => None,
1086+
});
1087+
10651088
let event = Event::PaymentSuccessful {
10661089
payment_id: Some(payment_id),
10671090
payment_hash,
10681091
payment_preimage: Some(payment_preimage),
10691092
fee_paid_msat,
1093+
bolt12_invoice: bolt12_invoice_bytes,
10701094
};
10711095

10721096
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 =
1344+
node_b.bolt12_payment().receive(expected_amount_msat, "proof of payment test", None, Some(1)).unwrap();
1345+
let payment_id = node_a
1346+
.bolt12_payment()
1347+
.send(&offer, Some(1), Some("Test".to_string()), None)
1348+
.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)