Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>? bolt12_invoice);
PaymentFailed(PaymentId? payment_id, PaymentHash? payment_hash, PaymentFailureReason? reason);
PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat, sequence<CustomTlvRecord> custom_records);
PaymentClaimable(PaymentId payment_id, PaymentHash payment_hash, u64 claimable_amount_msat, u32? claim_deadline, sequence<CustomTlvRecord> custom_records);
Expand Down
25 changes: 24 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,6 +76,17 @@ pub enum Event {
payment_preimage: Option<PaymentPreimage>,
/// The total fee which was spent at intermediate hops in this payment.
fee_paid_msat: Option<u64>,
/// 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<Vec<u8>>,
},
/// A sent payment has failed.
PaymentFailed {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -1022,6 +1035,7 @@ where
payment_preimage,
payment_hash,
fee_paid_msat,
bolt12_invoice,
..
} => {
let payment_id = if let Some(id) = payment_id {
Expand Down Expand Up @@ -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 {
Expand Down
78 changes: 78 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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();
Expand Down