diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index e89158b59..b84f04658 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -400,7 +400,7 @@ enum VssHeaderProviderError { [Enum] interface Event { - PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat); + PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, PaymentPreimage? payment_preimage, u64? fee_paid_msat, sequence? bolt12_invoice); PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason); PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence custom_records); PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline, sequence custom_records); diff --git a/src/event.rs b/src/event.rs index 75270bf53..b4a7b87d1 100644 --- a/src/event.rs +++ b/src/event.rs @@ -16,7 +16,8 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint}; use lightning::events::bump_transaction::BumpTransactionEvent; use lightning::events::{ - ClosureReason, Event as LdkEvent, PaymentFailureReason, PaymentPurpose, ReplayEvent, + ClosureReason, Event as LdkEvent, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose, + ReplayEvent, }; use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; @@ -75,6 +76,17 @@ pub enum Event { payment_preimage: Option, /// The total fee which was spent at intermediate hops in this payment. fee_paid_msat: Option, + /// The BOLT12 invoice that was paid, serialized as bytes. + /// + /// This is useful for proof of payment. A third party can verify that the payment was made + /// by checking that the `payment_hash` in the invoice matches `sha256(payment_preimage)`. + /// + /// Will be `None` for non-BOLT12 payments, or for async payments (`StaticInvoice`) + /// where proof of payment is not possible. + /// + /// To parse the invoice in native Rust, use `Bolt12Invoice::try_from(bytes)`. + /// In FFI bindings, hex-encode the bytes and use `Bolt12Invoice.from_str(hex_string)`. + bolt12_invoice: Option>, }, /// A sent payment has failed. PaymentFailed { @@ -264,6 +276,7 @@ impl_writeable_tlv_based_enum!(Event, (1, fee_paid_msat, option), (3, payment_id, option), (5, payment_preimage, option), + (7, bolt12_invoice, option), }, (1, PaymentFailed) => { (0, payment_hash, option), @@ -1022,6 +1035,7 @@ where payment_preimage, payment_hash, fee_paid_msat, + bolt12_invoice, .. } => { let payment_id = if let Some(id) = payment_id { @@ -1062,11 +1076,20 @@ where hex_utils::to_string(&payment_preimage.0) ); }); + + // Serialize the BOLT12 invoice to bytes for proof of payment. + // Only Bolt12Invoice supports proof of payment; StaticInvoice does not. + let bolt12_invoice_bytes = bolt12_invoice.and_then(|inv| match inv { + PaidBolt12Invoice::Bolt12Invoice(invoice) => Some(invoice.encode()), + PaidBolt12Invoice::StaticInvoice(_) => None, + }); + let event = Event::PaymentSuccessful { payment_id: Some(payment_id), payment_hash, payment_preimage: Some(payment_preimage), fee_paid_msat, + bolt12_invoice: bolt12_invoice_bytes, }; match self.event_queue.add_event(event).await { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 9b02cd61f..424e38925 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -33,6 +33,7 @@ use ldk_node::payment::{ }; use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; +use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; use lightning_invoice::{Bolt11InvoiceDescription, Description}; @@ -1303,6 +1304,83 @@ async fn simple_bolt12_send_receive() { assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn bolt12_proof_of_payment() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_amount_sat), + ) + .await; + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Sleep until we broadcasted a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + + // Sleep one more sec to make sure the node announcement propagates. + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let expected_amount_msat = 100_000_000; + let offer = node_b + .bolt12_payment() + .receive(expected_amount_msat, "proof of payment test", None, Some(1)) + .unwrap(); + let payment_id = + node_a.bolt12_payment().send(&offer, Some(1), Some("Test".to_string()), None).unwrap(); + + // Wait for payment and verify proof of payment + match node_a.next_event_async().await { + Event::PaymentSuccessful { + payment_id: event_payment_id, + payment_hash, + payment_preimage, + fee_paid_msat: _, + bolt12_invoice, + } => { + assert_eq!(event_payment_id, Some(payment_id)); + + // Verify proof of payment: sha256(preimage) == payment_hash + let preimage = payment_preimage.expect("preimage should be present"); + let computed_hash = Sha256Hash::hash(&preimage.0); + assert_eq!(PaymentHash(computed_hash.to_byte_array()), payment_hash); + + // Verify the BOLT12 invoice is present and contains the correct payment hash + let invoice_bytes = + bolt12_invoice.expect("bolt12_invoice should be present for BOLT12 payments"); + let invoice = LdkBolt12Invoice::try_from(invoice_bytes) + .expect("should be able to parse invoice from bytes"); + assert_eq!(invoice.payment_hash(), payment_hash); + assert_eq!(invoice.amount_msats(), expected_amount_msat); + + node_a.event_handled().unwrap(); + }, + ref e => { + panic!("Unexpected event: {:?}", e); + }, + } + + expect_payment_received_event!(node_b, expected_amount_msat); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn async_payment() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();