From bdceefb82e7b03988177774138677209480585d9 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 4 Feb 2026 10:55:56 +0100 Subject: [PATCH 1/3] Make `payment_id` a required field in `Event`s The switch to tracking payments by ID happened with LDK Node v0.3.0, which is >1.5 years old by now. We can be pretty certain that nobody is upgrading from an older version to the upcoming v0.8. Here we hence make the `payment_id` fields in `Event` required which is a nice API simplification that will also be utilized in the next commit. Co-Authored-By: HAL 9000 --- CHANGELOG.md | 5 ++++ src/event.rs | 25 ++++++++------------ tests/common/mod.rs | 8 +++---- tests/integration_tests_hrn.rs | 2 +- tests/integration_tests_rust.rs | 39 ++++++++++++++++---------------- tests/upgrade_downgrade_tests.rs | 5 ++-- 6 files changed, 40 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f15e61f5..3506daa6ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated. - Users of the VSS storage backend must upgrade their VSS server to at least version `v0.1.0-alpha.0` before upgrading LDK Node. +- The `payment_id` field on the `PaymentSuccessful`, `PaymentFailed`, and + `PaymentReceived` events is now a required (non-optional) `PaymentId`. Events + persisted by LDK Node v0.2.1 or earlier (which stored `payment_id` as + optional) will fail to deserialize on read; users upgrading from those + versions need to drain pending events before the upgrade. # 0.7.0 - Dec. 3, 2025 This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend. diff --git a/src/event.rs b/src/event.rs index 80acd0690e..9e87941691 100644 --- a/src/event.rs +++ b/src/event.rs @@ -103,9 +103,7 @@ pub enum Event { /// A sent payment was successful. PaymentSuccessful { /// A local identifier used to track the payment. - /// - /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. - payment_id: Option, + payment_id: PaymentId, /// The hash of the payment. payment_hash: PaymentHash, /// The preimage to the `payment_hash`. @@ -131,9 +129,7 @@ pub enum Event { /// A sent payment has failed. PaymentFailed { /// A local identifier used to track the payment. - /// - /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. - payment_id: Option, + payment_id: PaymentId, /// The hash of the payment. /// /// This will be `None` if the payment failed before receiving an invoice when paying a @@ -149,9 +145,7 @@ pub enum Event { /// A payment has been received. PaymentReceived { /// A local identifier used to track the payment. - /// - /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. - payment_id: Option, + payment_id: PaymentId, /// The hash of the payment. payment_hash: PaymentHash, /// The value, in thousandths of a satoshi, that has been received. @@ -298,18 +292,18 @@ impl_writeable_tlv_based_enum!(Event, (0, PaymentSuccessful) => { (0, payment_hash, required), (1, fee_paid_msat, option), - (3, payment_id, option), + (3, payment_id, required), (5, payment_preimage, option), (7, bolt12_invoice, option), }, (1, PaymentFailed) => { (0, payment_hash, option), (1, reason, upgradable_option), - (3, payment_id, option), + (3, payment_id, required), }, (2, PaymentReceived) => { (0, payment_hash, required), - (1, payment_id, option), + (1, payment_id, required), (2, amount_msat, required), (3, custom_records, optional_vec), }, @@ -1095,7 +1089,7 @@ where } let event = Event::PaymentReceived { - payment_id: Some(payment_id), + payment_id, payment_hash, amount_msat, custom_records: onion_fields @@ -1160,7 +1154,7 @@ where ); }); let event = Event::PaymentSuccessful { - payment_id: Some(payment_id), + payment_id, payment_hash, payment_preimage: Some(payment_preimage), fee_paid_msat, @@ -1196,8 +1190,7 @@ where }, }; - let event = - Event::PaymentFailed { payment_id: Some(payment_id), payment_hash, reason }; + let event = Event::PaymentFailed { payment_id, payment_hash, reason }; match self.event_queue.add_event(event).await { Ok(_) => return Ok(()), Err(e) => { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index adeb327bf0..c408a70900 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -222,8 +222,8 @@ macro_rules! expect_payment_received_event { ref e @ Event::PaymentReceived { payment_id, amount_msat, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(amount_msat, $amount_msat); - let payment = $node.payment(&payment_id.unwrap()).unwrap(); - if !matches!(payment.kind, ldk_node::payment::PaymentKind::Onchain { .. }) { + let payment = $node.payment(&payment_id).unwrap(); + if !matches!(payment.kind, PaymentKind::Onchain { .. }) { assert_eq!(payment.fee_paid_msat, None); } $node.event_handled().unwrap(); @@ -290,7 +290,7 @@ macro_rules! expect_payment_successful_event { if let Some(fee_msat) = $fee_paid_msat { assert_eq!(fee_paid_msat, fee_msat); } - let payment = $node.payment(&$payment_id.unwrap()).unwrap(); + let payment = $node.payment(&$payment_id).unwrap(); assert_eq!(payment.fee_paid_msat, fee_paid_msat); assert_eq!(payment_id, $payment_id); $node.event_handled().unwrap(); @@ -1235,7 +1235,7 @@ pub(crate) async fn do_channel_full_cycle( .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) .unwrap(); expect_payment_received_event!(node_b, claimable_amount_msat); - expect_payment_successful_event!(node_a, Some(manual_payment_id), None); + expect_payment_successful_event!(node_a, manual_payment_id, None); assert_eq!(node_a.payment(&manual_payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_a.payment(&manual_payment_id).unwrap().direction, PaymentDirection::Outbound); assert_eq!( diff --git a/tests/integration_tests_hrn.rs b/tests/integration_tests_hrn.rs index 9102400398..8f910b8606 100644 --- a/tests/integration_tests_hrn.rs +++ b/tests/integration_tests_hrn.rs @@ -79,5 +79,5 @@ async fn unified_send_to_hrn() { }, }; - expect_payment_successful_event!(node_a, Some(offer_payment_id), None); + expect_payment_successful_event!(node_a, offer_payment_id, None); } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fab73ed0c5..708a9add7e 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -380,13 +380,12 @@ async fn split_underpaid_bolt11_payment() { .unwrap(); let receiver_payment_id = expect_payment_received_event!(node_c, amount_msat); - assert_eq!(receiver_payment_id, Some(PaymentId(invoice.payment_hash().0))); - expect_payment_successful_event!(node_a, Some(payment_id_a), None); - expect_payment_successful_event!(node_b, Some(payment_id_b), None); + assert_eq!(receiver_payment_id, PaymentId(invoice.payment_hash().0)); + expect_payment_successful_event!(node_a, payment_id_a, None); + expect_payment_successful_event!(node_b, payment_id_b, None); // The receiver records the full invoice amount; each payer records only its own half. - let receiver_payments = - node_c.list_payments_with_filter(|p| p.id == receiver_payment_id.unwrap()); + let receiver_payments = node_c.list_payments_with_filter(|p| p.id == receiver_payment_id); assert_eq!(receiver_payments.len(), 1); assert_eq!(receiver_payments.first().unwrap().amount_msat, Some(amount_msat)); @@ -1202,7 +1201,7 @@ async fn splice_channel() { let payment_id = node_b.spontaneous_payment().send(amount_msat, node_a.node_id(), None).unwrap(); - expect_payment_successful_event!(node_b, Some(payment_id), None); + expect_payment_successful_event!(node_b, payment_id, None); expect_payment_received_event!(node_a, amount_msat); // Mine a block to give time for the HTLC to resolve @@ -1298,7 +1297,7 @@ async fn simple_bolt12_send_receive() { match event { ref e @ Event::PaymentSuccessful { payment_id: ref evt_id, ref bolt12_invoice, .. } => { println!("{} got event {:?}", node_a.node_id(), e); - assert_eq!(*evt_id, Some(payment_id)); + assert_eq!(*evt_id, payment_id); assert!( bolt12_invoice.is_some(), "bolt12_invoice should be present for BOLT12 payments" @@ -1372,7 +1371,7 @@ async fn simple_bolt12_send_receive() { ) .unwrap(); - expect_payment_successful_event!(node_a, Some(payment_id), None); + expect_payment_successful_event!(node_a, payment_id, None); let node_a_payments = node_a.list_payments_with_filter(|p| { matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == payment_id }); @@ -1445,7 +1444,7 @@ async fn simple_bolt12_send_receive() { .first() .unwrap() .id; - expect_payment_successful_event!(node_b, Some(node_b_payment_id), None); + expect_payment_successful_event!(node_b, node_b_payment_id, None); let node_b_payments = node_b.list_payments_with_filter(|p| { matches!(p.kind, PaymentKind::Bolt12Refund { .. }) && p.id == node_b_payment_id @@ -1619,7 +1618,7 @@ async fn async_payment() { node_receiver.start().unwrap(); - expect_payment_successful_event!(node_sender, Some(payment_id), None); + expect_payment_successful_event!(node_sender, payment_id, None); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -1845,7 +1844,7 @@ async fn unified_send_receive_bip21_uri() { }, }; - expect_payment_successful_event!(node_a, Some(offer_payment_id), None); + expect_payment_successful_event!(node_a, offer_payment_id, None); // Cut off the BOLT12 part to fallback to BOLT11. let uri_str_without_offer = uri_str.split("&lno=").next().unwrap(); @@ -1865,7 +1864,7 @@ async fn unified_send_receive_bip21_uri() { panic!("Expected Bolt11 payment but got error: {:?}", e); }, }; - expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); + expect_payment_successful_event!(node_a, invoice_payment_id, None); let expect_onchain_amount_sats = 800_000; let onchain_uni_payment = @@ -2000,9 +1999,9 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_successful_event!(payer_node, payment_id, None); let client_payment_id = - expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + expect_payment_received_event!(client_node, expected_received_amount_msat); let client_payment = client_node.payment(&client_payment_id).unwrap(); match client_payment.kind { PaymentKind::Bolt11 { counterparty_skimmed_fee_msat, .. } => { @@ -2029,7 +2028,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { // are working as expected. println!("Paying regular invoice!"); let payment_id = payer_node.bolt11_payment().send(&invoice, None).unwrap(); - expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_successful_event!(payer_node, payment_id, None); expect_event!(service_node, PaymentForwarded); expect_payment_received_event!(client_node, amount_msat); @@ -2075,9 +2074,9 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { .unwrap(); expect_event!(service_node, PaymentForwarded); - expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_successful_event!(payer_node, payment_id, None); let client_payment_id = - expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + expect_payment_received_event!(client_node, expected_received_amount_msat); let client_payment = client_node.payment(&client_payment_id).unwrap(); match client_payment.kind { PaymentKind::Bolt11 { counterparty_skimmed_fee_msat, .. } => { @@ -2182,7 +2181,7 @@ async fn spontaneous_send_with_custom_preimage() { .unwrap(); // check payment status and verify stored preimage - expect_payment_successful_event!(node_a, Some(payment_id), None); + expect_payment_successful_event!(node_a, payment_id, None); let details: PaymentDetails = node_a.list_payments_with_filter(|p| p.id == payment_id).first().unwrap().clone(); assert_eq!(details.status, PaymentStatus::Succeeded); @@ -2368,9 +2367,9 @@ async fn lsps2_client_trusts_lsp() { .claim_for_hash(manual_payment_hash, jit_amount_msat, manual_preimage) .unwrap(); - expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_successful_event!(payer_node, payment_id, None); - let _ = expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + let _ = expect_payment_received_event!(client_node, expected_received_amount_msat); // Check the nodes pick up on the confirmed funding tx now. wait_for_tx(&electrsd.client, funding_txo.txid).await; diff --git a/tests/upgrade_downgrade_tests.rs b/tests/upgrade_downgrade_tests.rs index f07e49427a..e39da6e26d 100644 --- a/tests/upgrade_downgrade_tests.rs +++ b/tests/upgrade_downgrade_tests.rs @@ -302,7 +302,7 @@ async fn expect_current_payment_successful( ) { match next_current_event(node).await { ldk_node::Event::PaymentSuccessful { payment_id, .. } => { - assert_eq!(payment_id.as_ref(), Some(expected_payment_id)); + assert_eq!(&payment_id, expected_payment_id); node.event_handled().unwrap(); }, event => panic!("{} got unexpected event: {:?}", node.node_id(), event), @@ -311,9 +311,8 @@ async fn expect_current_payment_successful( async fn expect_current_payment_received(node: &CurrentNode, expected_amount_msat: u64) { match next_current_event(node).await { - ldk_node::Event::PaymentReceived { amount_msat, payment_id, .. } => { + ldk_node::Event::PaymentReceived { amount_msat, .. } => { assert_eq!(amount_msat, expected_amount_msat); - assert!(payment_id.is_some()); node.event_handled().unwrap(); }, event => panic!("{} got unexpected event: {:?}", node.node_id(), event), From 118395f198fc2fc01d6fda916097dd702150abfa Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 24 Jun 2026 15:15:56 +0200 Subject: [PATCH 2/3] Use payment IDs for BOLT11 payments Create inbound BOLT11 records from claimable and claimed events. Stop pre-creating those records when invoices are generated. Generate outbound BOLT11 IDs from KeysManager entropy. Do not derive them from the payment hash. Duplicate invoice detection is restored in the next commit. Co-Authored-By: HAL 9000 --- src/event.rs | 455 +++++++++++++++++++++----------- src/lib.rs | 2 + src/payment/bolt11.rs | 213 ++++++--------- tests/common/mod.rs | 258 ++++++++++++------ tests/integration_tests_rust.rs | 62 ++--- 5 files changed, 597 insertions(+), 393 deletions(-) diff --git a/src/event.rs b/src/event.rs index 9e87941691..2a122bc579 100644 --- a/src/event.rs +++ b/src/event.rs @@ -194,15 +194,15 @@ pub enum Event { }, /// A payment for a previously-registered payment hash has been received. /// - /// This needs to be manually claimed by supplying the correct preimage to [`claim_for_hash`]. + /// This needs to be manually claimed by supplying the correct preimage to [`claim_for_id`]. /// /// If the provided parameters don't match the expectations or the preimage can't be - /// retrieved in time, should be failed-back via [`fail_for_hash`]. + /// retrieved in time, should be failed-back via [`fail_for_id`]. /// /// Note claiming will necessarily fail after the `claim_deadline` has been reached. /// - /// [`claim_for_hash`]: crate::payment::Bolt11Payment::claim_for_hash - /// [`fail_for_hash`]: crate::payment::Bolt11Payment::fail_for_hash + /// [`claim_for_id`]: crate::payment::Bolt11Payment::claim_for_id + /// [`fail_for_id`]: crate::payment::Bolt11Payment::fail_for_id PaymentClaimable { /// A local identifier used to track the payment. payment_id: PaymentId, @@ -605,6 +605,23 @@ where }) } + fn resolve_inbound_payment_id( + &self, event_payment_id: PaymentId, payment_hash: &PaymentHash, + ) -> (PaymentId, Option) { + if let Some(info) = self.payment_store.get(&event_payment_id) { + return (event_payment_id, Some(info)); + } + + let legacy_id = PaymentId(payment_hash.0); + if legacy_id != event_payment_id { + if let Some(info) = self.payment_store.get(&legacy_id) { + return (legacy_id, Some(info)); + } + } + + (event_payment_id, None) + } + pub async fn handle_event(&self, event: LdkEvent) -> Result<(), ReplayEvent> { match event { LdkEvent::FundingGenerationReady { @@ -708,6 +725,7 @@ where .lsps2_funding_tx_broadcast_safe(user_channel_id, counterparty_node_id); }, LdkEvent::PaymentClaimable { + payment_id, payment_hash, purpose, amount_msat, @@ -716,8 +734,9 @@ where counterparty_skimmed_fee_msat, .. } => { - let payment_id = PaymentId(payment_hash.0); - let payment_info = self.payment_store.get(&payment_id); + let event_payment_id = payment_id.unwrap_or(PaymentId(payment_hash.0)); + let (payment_id, payment_info) = + self.resolve_inbound_payment_id(event_payment_id, &payment_hash); if let Some(info) = payment_info.as_ref() { if info.direction == PaymentDirection::Outbound { log_info!( @@ -838,47 +857,6 @@ where } } - if let Some(info) = payment_info { - // If this is known by the store but ChannelManager doesn't know the preimage, - // the payment has been registered via `_for_hash` variants and needs to be manually claimed via - // user interaction. - match info.kind { - PaymentKind::Bolt11 { preimage, .. } => { - if purpose.preimage().is_none() { - debug_assert!( - preimage.is_none(), - "We would have registered the preimage if we knew" - ); - - let custom_records = onion_fields - .map(|cf| { - cf.custom_tlvs().into_iter().map(|tlv| tlv.into()).collect() - }) - .unwrap_or_default(); - let event = Event::PaymentClaimable { - payment_id, - payment_hash, - claimable_amount_msat: amount_msat, - claim_deadline, - custom_records, - }; - match self.event_queue.add_event(event).await { - Ok(_) => return Ok(()), - Err(e) => { - log_error!( - self.logger, - "Failed to push to event queue: {}", - e - ); - return Err(ReplayEvent()); - }, - }; - } - }, - _ => {}, - } - } - log_info!( self.logger, "Received payment from payment hash {} of {}msat", @@ -886,7 +864,88 @@ where amount_msat, ); let payment_preimage = match purpose { - PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => { + PaymentPurpose::Bolt11InvoicePayment { + payment_preimage, + payment_secret, + .. + } => { + if payment_info.is_none() { + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: payment_preimage, + secret: Some(payment_secret), + counterparty_skimmed_fee_msat: if counterparty_skimmed_fee_msat > 0 + { + Some(counterparty_skimmed_fee_msat) + } else { + None + }, + }; + + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + None, + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); + + match self.payment_store.insert(payment).await { + Ok(false) => (), + Ok(true) => { + log_error!( + self.logger, + "Bolt11InvoicePayment with ID {} was previously known", + payment_id, + ); + debug_assert!(false); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to insert payment with ID {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + }, + } + } + + if payment_preimage.is_none() { + if let Some(info) = payment_info.as_ref() { + let stored_preimage = match &info.kind { + PaymentKind::Bolt11 { preimage, .. } => *preimage, + _ => None, + }; + debug_assert!( + stored_preimage.is_none(), + "We would have registered the preimage if we knew" + ); + } + + let custom_records = onion_fields + .map(|cf| { + cf.custom_tlvs().into_iter().map(|tlv| tlv.into()).collect() + }) + .unwrap_or_default(); + let event = Event::PaymentClaimable { + payment_id, + payment_hash, + claimable_amount_msat: amount_msat, + claim_deadline, + custom_records, + }; + match self.event_queue.add_event(event).await { + Ok(_) => return Ok(()), + Err(e) => { + log_error!(self.logger, "Failed to push to event queue: {}", e); + return Err(ReplayEvent()); + }, + }; + } + payment_preimage }, PaymentPurpose::Bolt12OfferPayment { @@ -898,84 +957,130 @@ where let payer_note = payment_context.invoice_request.payer_note_truncated; let offer_id = payment_context.offer_id; let quantity = payment_context.invoice_request.quantity; - let kind = PaymentKind::Bolt12Offer { - hash: Some(payment_hash), - preimage: payment_preimage, - secret: Some(payment_secret), - offer_id, - payer_note, - quantity, - }; - - let payment = PaymentDetails::new( - payment_id, - kind, - Some(amount_msat), - None, - PaymentDirection::Inbound, - PaymentStatus::Pending, - ); + if payment_info.is_none() { + let kind = PaymentKind::Bolt12Offer { + hash: Some(payment_hash), + preimage: payment_preimage, + secret: Some(payment_secret), + offer_id, + payer_note, + quantity, + }; + + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + None, + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); - match self.payment_store.insert(payment).await { - Ok(false) => (), - Ok(true) => { - log_error!( - self.logger, - "Bolt12OfferPayment with ID {} was previously known", - payment_id, - ); - debug_assert!(false); - }, - Err(e) => { - log_error!( - self.logger, - "Failed to insert payment with ID {}: {}", - payment_id, - e - ); - debug_assert!(false); - }, + match self.payment_store.insert(payment).await { + Ok(false) => (), + Ok(true) => { + log_error!( + self.logger, + "Bolt12OfferPayment with ID {} was previously known", + payment_id, + ); + debug_assert!(false); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to insert payment with ID {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + }, + } } payment_preimage }, - PaymentPurpose::Bolt12RefundPayment { payment_preimage, .. } => { + PaymentPurpose::Bolt12RefundPayment { + payment_preimage, + payment_secret, + .. + } => { + if payment_info.is_none() { + let kind = PaymentKind::Bolt12Refund { + hash: Some(payment_hash), + preimage: payment_preimage, + secret: Some(payment_secret), + payer_note: None, + quantity: None, + }; + + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + None, + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); + + match self.payment_store.insert(payment).await { + Ok(false) => (), + Ok(true) => { + log_error!( + self.logger, + "Bolt12RefundPayment with ID {} was previously known", + payment_id, + ); + debug_assert!(false); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to insert payment with ID {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + }, + } + } payment_preimage }, PaymentPurpose::SpontaneousPayment(preimage) => { - // Since it's spontaneous, we insert it now into our store. - let kind = PaymentKind::Spontaneous { - hash: payment_hash, - preimage: Some(preimage), - }; - - let payment = PaymentDetails::new( - payment_id, - kind, - Some(amount_msat), - None, - PaymentDirection::Inbound, - PaymentStatus::Pending, - ); + if payment_info.is_none() { + let kind = PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(preimage), + }; + + let payment = PaymentDetails::new( + payment_id, + kind, + Some(amount_msat), + None, + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); - match self.payment_store.insert(payment).await { - Ok(false) => (), - Ok(true) => { - log_error!( - self.logger, - "Spontaneous payment with ID {} was previously known", - payment_id, - ); - debug_assert!(false); - }, - Err(e) => { - log_error!( - self.logger, - "Failed to insert payment with ID {}: {}", - payment_id, - e - ); - debug_assert!(false); - }, + match self.payment_store.insert(payment).await { + Ok(false) => (), + Ok(true) => { + log_error!( + self.logger, + "Spontaneous payment with ID {} was previously known", + payment_id, + ); + debug_assert!(false); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to insert payment with ID {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + }, + } } Some(preimage) @@ -1007,6 +1112,7 @@ where } }, LdkEvent::PaymentClaimed { + payment_id, payment_hash, purpose, amount_msat, @@ -1014,9 +1120,10 @@ where htlcs: _, sender_intended_total_msat: _, onion_fields, - payment_id: _, } => { - let payment_id = PaymentId(payment_hash.0); + let event_payment_id = payment_id.unwrap_or(PaymentId(payment_hash.0)); + let (payment_id, _) = + self.resolve_inbound_payment_id(event_payment_id, &payment_hash); log_info!( self.logger, "Claimed payment with ID {} from payment hash {} of {}msat.", @@ -1025,43 +1132,83 @@ where amount_msat, ); - let update = match purpose { + let (update, kind_for_insert) = match purpose { PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, .. - } => PaymentDetailsUpdate { - preimage: Some(payment_preimage), - secret: Some(Some(payment_secret)), - amount_msat: Some(Some(amount_msat)), - status: Some(PaymentStatus::Succeeded), - ..PaymentDetailsUpdate::new(payment_id) + } => { + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: payment_preimage, + secret: Some(payment_secret), + counterparty_skimmed_fee_msat: None, + }; + let update = PaymentDetailsUpdate { + preimage: Some(payment_preimage), + secret: Some(Some(payment_secret)), + amount_msat: Some(Some(amount_msat)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }; + (update, kind) }, PaymentPurpose::Bolt12OfferPayment { - payment_preimage, payment_secret, .. - } => PaymentDetailsUpdate { - preimage: Some(payment_preimage), - secret: Some(Some(payment_secret)), - amount_msat: Some(Some(amount_msat)), - status: Some(PaymentStatus::Succeeded), - ..PaymentDetailsUpdate::new(payment_id) + payment_preimage, + payment_secret, + payment_context, + .. + } => { + let kind = PaymentKind::Bolt12Offer { + hash: Some(payment_hash), + preimage: payment_preimage, + secret: Some(payment_secret), + offer_id: payment_context.offer_id, + payer_note: payment_context.invoice_request.payer_note_truncated, + quantity: payment_context.invoice_request.quantity, + }; + let update = PaymentDetailsUpdate { + preimage: Some(payment_preimage), + secret: Some(Some(payment_secret)), + amount_msat: Some(Some(amount_msat)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }; + (update, kind) }, PaymentPurpose::Bolt12RefundPayment { payment_preimage, payment_secret, .. - } => PaymentDetailsUpdate { - preimage: Some(payment_preimage), - secret: Some(Some(payment_secret)), - amount_msat: Some(Some(amount_msat)), - status: Some(PaymentStatus::Succeeded), - ..PaymentDetailsUpdate::new(payment_id) + } => { + let kind = PaymentKind::Bolt12Refund { + hash: Some(payment_hash), + preimage: payment_preimage, + secret: Some(payment_secret), + payer_note: None, + quantity: None, + }; + let update = PaymentDetailsUpdate { + preimage: Some(payment_preimage), + secret: Some(Some(payment_secret)), + amount_msat: Some(Some(amount_msat)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }; + (update, kind) }, - PaymentPurpose::SpontaneousPayment(preimage) => PaymentDetailsUpdate { - preimage: Some(Some(preimage)), - amount_msat: Some(Some(amount_msat)), - status: Some(PaymentStatus::Succeeded), - ..PaymentDetailsUpdate::new(payment_id) + PaymentPurpose::SpontaneousPayment(preimage) => { + let kind = PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(preimage), + }; + let update = PaymentDetailsUpdate { + preimage: Some(Some(preimage)), + amount_msat: Some(Some(amount_msat)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }; + (update, kind) }, }; @@ -1071,11 +1218,23 @@ where // be the result of a replayed event. ), Ok(DataStoreUpdateResult::NotFound) => { - log_error!( - self.logger, - "Claimed payment with ID {} couldn't be found in store", + let payment = PaymentDetails::new( payment_id, + kind_for_insert, + Some(amount_msat), + None, + PaymentDirection::Inbound, + PaymentStatus::Succeeded, ); + if let Err(e) = self.payment_store.insert(payment).await { + log_error!( + self.logger, + "Failed to insert payment with ID {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + } }, Err(e) => { log_error!( diff --git a/src/lib.rs b/src/lib.rs index 34fa7f54d6..cf742783f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -945,6 +945,7 @@ impl Node { Bolt11Payment::new( Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), + Arc::clone(&self.keys_manager), Arc::clone(&self.connection_manager), Arc::clone(&self.liquidity_source), Arc::clone(&self.payment_store), @@ -963,6 +964,7 @@ impl Node { Arc::new(Bolt11Payment::new( Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), + Arc::clone(&self.keys_manager), Arc::clone(&self.connection_manager), Arc::clone(&self.liquidity_source), Arc::clone(&self.payment_store), diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 4503dfa061..68e1ccca9e 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -19,6 +19,7 @@ use lightning::ln::channelmanager::{ }; use lightning::ln::outbound_payment::{Bolt11PaymentError, Retry, RetryableSendFailure}; use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; +use lightning::sign::EntropySource; use lightning_invoice::{ Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription, }; @@ -37,7 +38,7 @@ use crate::payment::store::{ }; use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, PaymentStore}; +use crate::types::{ChannelManager, KeysManager, PaymentStore}; #[cfg(not(feature = "uniffi"))] type Bolt11Invoice = LdkBolt11Invoice; @@ -69,6 +70,7 @@ impl_writeable_tlv_based!(PaymentMetadata, { pub struct Bolt11Payment { runtime: Arc, channel_manager: Arc, + keys_manager: Arc, connection_manager: Arc>>, liquidity_source: Arc>>, payment_store: Arc, @@ -81,7 +83,7 @@ pub struct Bolt11Payment { impl Bolt11Payment { pub(crate) fn new( runtime: Arc, channel_manager: Arc, - connection_manager: Arc>>, + keys_manager: Arc, connection_manager: Arc>>, liquidity_source: Arc>>, payment_store: Arc, peer_store: Arc>>, config: Arc, is_running: Arc>, logger: Arc, @@ -89,6 +91,7 @@ impl Bolt11Payment { Self { runtime, channel_manager, + keys_manager, connection_manager, liquidity_source, payment_store, @@ -124,42 +127,6 @@ impl Bolt11Payment { } }; - let payment_hash = invoice.payment_hash(); - let payment_secret = invoice.payment_secret(); - let id = PaymentId(payment_hash.0); - let preimage = if manual_claim_payment_hash.is_none() { - // If the user hasn't registered a custom payment hash, we're positive ChannelManager - // will know the preimage at this point. - let mut payment_metadata = invoice.payment_metadata().cloned(); - let res = self - .channel_manager - .get_payment_preimage_decrypt_metadata( - payment_hash, - payment_secret.clone(), - payment_metadata.as_deref_mut(), - ) - .ok(); - debug_assert!(res.is_some(), "We just let ChannelManager create an inbound payment, it can't have forgotten the preimage by now."); - res - } else { - None - }; - let kind = PaymentKind::Bolt11 { - hash: payment_hash, - preimage, - secret: Some(payment_secret.clone()), - counterparty_skimmed_fee_msat: None, - }; - let payment = PaymentDetails::new( - id, - kind, - amount_msat, - None, - PaymentDirection::Inbound, - PaymentStatus::Pending, - ); - self.runtime.block_on(self.payment_store.insert(payment))?; - Ok(invoice) } @@ -196,35 +163,6 @@ impl Bolt11Payment { } })?; - // Register payment in payment store. - let payment_hash = invoice.payment_hash(); - let payment_secret = invoice.payment_secret(); - let id = PaymentId(payment_hash.0); - let mut payment_metadata = invoice.payment_metadata().cloned(); - let preimage = self - .channel_manager - .get_payment_preimage_decrypt_metadata( - payment_hash, - payment_secret.clone(), - payment_metadata.as_deref_mut(), - ) - .ok(); - let kind = PaymentKind::Bolt11 { - hash: payment_hash, - preimage, - secret: Some(payment_secret.clone()), - counterparty_skimmed_fee_msat: None, - }; - let payment = PaymentDetails::new( - id, - kind, - amount_msat, - None, - PaymentDirection::Inbound, - PaymentStatus::Pending, - ); - self.runtime.block_on(self.payment_store.insert(payment))?; - // Persist the chosen LSP peer to make sure we reconnect on restart. let peer_info = PeerInfo { node_id: chosen_lsp.node_id, address: chosen_lsp.address }; self.runtime.block_on(self.peer_store.add_peer(peer_info))?; @@ -275,15 +213,7 @@ impl Bolt11Payment { } let payment_hash = invoice.payment_hash(); - let payment_id = PaymentId(invoice.payment_hash().0); - if let Some(payment) = self.payment_store.get(&payment_id) { - if payment.status == PaymentStatus::Pending - || payment.status == PaymentStatus::Succeeded - { - log_error!(self.logger, "Payment error: an invoice must not be paid twice."); - return Err(Error::DuplicatePayment); - } - } + let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes()); let route_params_config = route_parameters.or(self.config.route_parameters).unwrap_or_default(); @@ -490,13 +420,31 @@ impl Bolt11Payment { /// [`receive_variable_amount_for_hash`]: Self::receive_variable_amount_for_hash /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`PaymentReceived`]: crate::Event::PaymentReceived - pub fn claim_for_hash( - &self, payment_hash: PaymentHash, claimable_amount_msat: u64, preimage: PaymentPreimage, + pub fn claim_for_id( + &self, payment_id: PaymentId, claimable_amount_msat: u64, preimage: PaymentPreimage, ) -> Result<(), Error> { - let payment_id = PaymentId(payment_hash.0); + let details = self.payment_store.get(&payment_id).ok_or_else(|| { + log_error!( + self.logger, + "Failed to manually claim unknown payment with ID: {}", + payment_id + ); + Error::InvalidPaymentId + })?; - let expected_payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); + let payment_hash = match details.kind { + PaymentKind::Bolt11 { hash, .. } => hash, + _ => { + log_error!( + self.logger, + "Failed to manually claim payment with ID {} of unsupported kind", + payment_id + ); + return Err(Error::InvalidPaymentId); + }, + }; + let expected_payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); if expected_payment_hash != payment_hash { log_error!( self.logger, @@ -506,40 +454,30 @@ impl Bolt11Payment { return Err(Error::InvalidPaymentPreimage); } - if let Some(details) = self.payment_store.get(&payment_id) { - // For payments requested via `receive*_via_jit_channel_for_hash()` - // `skimmed_fee_msat` held by LSP must be taken into account. - let skimmed_fee_msat = match details.kind { - PaymentKind::Bolt11 { - counterparty_skimmed_fee_msat: Some(skimmed_fee_msat), - .. - } => skimmed_fee_msat, - _ => 0, - }; - if let Some(invoice_amount_msat) = details.amount_msat { - if claimable_amount_msat < invoice_amount_msat.saturating_sub(skimmed_fee_msat) { - log_error!( - self.logger, - "Failed to manually claim payment {} as the claimable amount is less than expected", - payment_id - ); - return Err(Error::InvalidAmount); - } + // For payments requested via `receive*_via_jit_channel_for_hash()` + // `skimmed_fee_msat` held by LSP must be taken into account. + let skimmed_fee_msat = match details.kind { + PaymentKind::Bolt11 { + counterparty_skimmed_fee_msat: Some(skimmed_fee_msat), .. + } => skimmed_fee_msat, + _ => 0, + }; + if let Some(invoice_amount_msat) = details.amount_msat { + if claimable_amount_msat < invoice_amount_msat.saturating_sub(skimmed_fee_msat) { + log_error!( + self.logger, + "Failed to manually claim payment {} as the claimable amount is less than expected", + payment_id + ); + return Err(Error::InvalidAmount); } - } else { - log_error!( - self.logger, - "Failed to manually claim unknown payment with hash: {}", - payment_hash - ); - return Err(Error::InvalidPaymentHash); } self.channel_manager.claim_funds(preimage); Ok(()) } - /// Allows to manually fail payments with the given hash that have previously + /// Allows to manually fail payments with the given id that have previously /// been registered via [`receive_for_hash`] or [`receive_variable_amount_for_hash`]. /// /// This should be called in reponse to a [`PaymentClaimable`] event if the payment needs to be @@ -552,8 +490,27 @@ impl Bolt11Payment { /// [`receive_for_hash`]: Self::receive_for_hash /// [`receive_variable_amount_for_hash`]: Self::receive_variable_amount_for_hash /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - pub fn fail_for_hash(&self, payment_hash: PaymentHash) -> Result<(), Error> { - let payment_id = PaymentId(payment_hash.0); + pub fn fail_for_id(&self, payment_id: PaymentId) -> Result<(), Error> { + let details = self.payment_store.get(&payment_id).ok_or_else(|| { + log_error!( + self.logger, + "Failed to manually fail unknown payment with ID {}", + payment_id, + ); + Error::InvalidPaymentId + })?; + + let payment_hash = match details.kind { + PaymentKind::Bolt11 { hash, .. } => hash, + _ => { + log_error!( + self.logger, + "Failed to manually fail payment with ID {} of unsupported kind", + payment_id + ); + return Err(Error::InvalidPaymentId); + }, + }; let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), @@ -565,10 +522,10 @@ impl Bolt11Payment { Ok(DataStoreUpdateResult::NotFound) => { log_error!( self.logger, - "Failed to manually fail unknown payment with hash {}", - payment_hash, + "Failed to manually fail unknown payment with ID {}", + payment_id, ); - return Err(Error::InvalidPaymentHash); + return Err(Error::InvalidPaymentId); }, Err(e) => { log_error!( @@ -604,13 +561,13 @@ impl Bolt11Payment { /// the inbound payment arrives. /// /// **Note:** users *MUST* handle this event and claim the payment manually via - /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// [`claim_for_id`] as soon as they have obtained access to the preimage of the given /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via - /// [`fail_for_hash`]. + /// [`fail_for_id`]. /// /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - /// [`claim_for_hash`]: Self::claim_for_hash - /// [`fail_for_hash`]: Self::fail_for_hash + /// [`claim_for_id`]: Self::claim_for_id + /// [`fail_for_id`]: Self::fail_for_id pub fn receive_for_hash( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, @@ -640,13 +597,13 @@ impl Bolt11Payment { /// the inbound payment arrives. /// /// **Note:** users *MUST* handle this event and claim the payment manually via - /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// [`claim_for_id`] as soon as they have obtained access to the preimage of the given /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via - /// [`fail_for_hash`]. + /// [`fail_for_id`]. /// /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - /// [`claim_for_hash`]: Self::claim_for_hash - /// [`fail_for_hash`]: Self::fail_for_hash + /// [`claim_for_id`]: Self::claim_for_id + /// [`fail_for_id`]: Self::fail_for_id pub fn receive_variable_amount_for_hash( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { @@ -695,14 +652,14 @@ impl Bolt11Payment { /// is performed *before* emitting the event. /// /// **Note:** users *MUST* handle this event and claim the payment manually via - /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// [`claim_for_id`] as soon as they have obtained access to the preimage of the given /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via - /// [`fail_for_hash`]. + /// [`fail_for_id`]. /// /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - /// [`claim_for_hash`]: Self::claim_for_hash - /// [`fail_for_hash`]: Self::fail_for_hash + /// [`claim_for_id`]: Self::claim_for_id + /// [`fail_for_id`]: Self::fail_for_id /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11::counterparty_skimmed_fee_msat pub fn receive_via_jit_channel_for_hash( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, @@ -762,14 +719,14 @@ impl Bolt11Payment { /// is performed *before* emitting the event. /// /// **Note:** users *MUST* handle this event and claim the payment manually via - /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// [`claim_for_id`] as soon as they have obtained access to the preimage of the given /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via - /// [`fail_for_hash`]. + /// [`fail_for_id`]. /// /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - /// [`claim_for_hash`]: Self::claim_for_hash - /// [`fail_for_hash`]: Self::fail_for_hash + /// [`claim_for_id`]: Self::claim_for_id + /// [`fail_for_id`]: Self::fail_for_id /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11::counterparty_skimmed_fee_msat pub fn receive_variable_amount_via_jit_channel_for_hash( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index c408a70900..d8339a88ca 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -223,7 +223,7 @@ macro_rules! expect_payment_received_event { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(amount_msat, $amount_msat); let payment = $node.payment(&payment_id).unwrap(); - if !matches!(payment.kind, PaymentKind::Onchain { .. }) { + if !matches!(payment.kind, ldk_node::payment::PaymentKind::Onchain { .. }) { assert_eq!(payment.fee_paid_msat, None); } $node.event_handled().unwrap(); @@ -239,6 +239,36 @@ macro_rules! expect_payment_received_event { pub(crate) use expect_payment_received_event; macro_rules! expect_payment_claimable_event { + ($node:expr, $payment_hash:expr, $claimable_amount_msat:expr) => {{ + let event = tokio::time::timeout( + std::time::Duration::from_secs(crate::common::INTEROP_TIMEOUT_SECS), + $node.next_event_async(), + ) + .await + .unwrap_or_else(|_| { + panic!( + "{} timed out waiting for PaymentClaimable event after 60s", + std::stringify!($node) + ) + }); + match event { + ref e @ Event::PaymentClaimable { + payment_id, + payment_hash, + claimable_amount_msat, + .. + } => { + println!("{} got event {:?}", std::stringify!($node), e); + assert_eq!(payment_hash, $payment_hash); + assert_eq!(claimable_amount_msat, $claimable_amount_msat); + $node.event_handled().unwrap(); + (payment_id, claimable_amount_msat) + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); + }, + } + }}; ($node:expr, $payment_id:expr, $payment_hash:expr, $claimable_amount_msat:expr) => {{ let event = tokio::time::timeout( std::time::Duration::from_secs(crate::common::INTEROP_TIMEOUT_SECS), @@ -1072,10 +1102,10 @@ pub(crate) async fn do_channel_full_cycle( .unwrap(); println!("\nA send"); - let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); + let outbound_payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); assert_eq!(node_a.bolt11_payment().send(&invoice, None), Err(NodeError::DuplicatePayment)); - assert!(!node_a.list_payments_with_filter(|p| p.id == payment_id).is_empty()); + assert!(!node_a.list_payments_with_filter(|p| p.id == outbound_payment_id).is_empty()); let outbound_payments_a = node_a.list_payments_with_filter(|p| { p.direction == PaymentDirection::Outbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) @@ -1095,7 +1125,7 @@ pub(crate) async fn do_channel_full_cycle( let inbound_payments_b = node_b.list_payments_with_filter(|p| { p.direction == PaymentDirection::Inbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) }); - assert_eq!(inbound_payments_b.len(), 1); + assert_eq!(inbound_payments_b.len(), 0); // Verify bolt12_invoice is None for BOLT11 payments match node_a.next_event_async().await { @@ -1108,24 +1138,42 @@ pub(crate) async fn do_channel_full_cycle( panic!("{} got unexpected event!: {:?}", std::stringify!(node_a), e); }, } - expect_event!(node_b, PaymentReceived); - assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); - assert!(matches!(node_a.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); - assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); - assert!(matches!(node_b.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); + let inbound_payment_id = expect_payment_received_event!(node_b, invoice_amount_1_msat); + assert_eq!(node_a.payment(&outbound_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&outbound_payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!( + node_a.payment(&outbound_payment_id).unwrap().amount_msat, + Some(invoice_amount_1_msat) + ); + assert!(matches!( + node_a.payment(&outbound_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); + assert_eq!(node_b.payment(&inbound_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&inbound_payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!( + node_b.payment(&inbound_payment_id).unwrap().amount_msat, + Some(invoice_amount_1_msat) + ); + assert!(matches!( + node_b.payment(&inbound_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); // Assert we fail duplicate outbound payments and check the status hasn't changed. assert_eq!(Err(NodeError::DuplicatePayment), node_a.bolt11_payment().send(&invoice, None)); - assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); - assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(node_a.payment(&outbound_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&outbound_payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!( + node_a.payment(&outbound_payment_id).unwrap().amount_msat, + Some(invoice_amount_1_msat) + ); + assert_eq!(node_b.payment(&inbound_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&inbound_payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!( + node_b.payment(&inbound_payment_id).unwrap().amount_msat, + Some(invoice_amount_1_msat) + ); // Test under-/overpayment let invoice_amount_2_msat = 2500_000; @@ -1148,28 +1196,40 @@ pub(crate) async fn do_channel_full_cycle( let overpaid_amount_msat = invoice_amount_2_msat + 100; println!("\nA overpaid send"); - let payment_id = + let outbound_payment_id = node_a.bolt11_payment().send_using_amount(&invoice, overpaid_amount_msat, None).unwrap(); expect_event!(node_a, PaymentSuccessful); - let received_amount = match node_b.next_event_async().await { - ref e @ Event::PaymentReceived { amount_msat, .. } => { + let (inbound_payment_id, received_amount) = match node_b.next_event_async().await { + ref e @ Event::PaymentReceived { payment_id, amount_msat, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); node_b.event_handled().unwrap(); - amount_msat + (payment_id, amount_msat) }, ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); }, }; assert_eq!(received_amount, overpaid_amount_msat); - assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(overpaid_amount_msat)); - assert!(matches!(node_a.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); - assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(overpaid_amount_msat)); - assert!(matches!(node_b.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); + assert_eq!(node_a.payment(&outbound_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&outbound_payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!( + node_a.payment(&outbound_payment_id).unwrap().amount_msat, + Some(overpaid_amount_msat) + ); + assert!(matches!( + node_a.payment(&outbound_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); + assert_eq!(node_b.payment(&inbound_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&inbound_payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!( + node_b.payment(&inbound_payment_id).unwrap().amount_msat, + Some(overpaid_amount_msat) + ); + assert!(matches!( + node_b.payment(&inbound_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); // Test "zero-amount" invoice payment println!("\nB receive_variable_amount_payment"); @@ -1183,31 +1243,43 @@ pub(crate) async fn do_channel_full_cycle( node_a.bolt11_payment().send(&variable_amount_invoice, None) ); println!("\nA send_using_amount"); - let payment_id = node_a + let outbound_payment_id = node_a .bolt11_payment() .send_using_amount(&variable_amount_invoice, determined_amount_msat, None) .unwrap(); expect_event!(node_a, PaymentSuccessful); - let received_amount = match node_b.next_event_async().await { - ref e @ Event::PaymentReceived { amount_msat, .. } => { + let (inbound_payment_id, received_amount) = match node_b.next_event_async().await { + ref e @ Event::PaymentReceived { payment_id, amount_msat, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); node_b.event_handled().unwrap(); - amount_msat + (payment_id, amount_msat) }, ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); }, }; assert_eq!(received_amount, determined_amount_msat); - assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); - assert!(matches!(node_a.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); - assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); - assert!(matches!(node_b.payment(&payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); + assert_eq!(node_a.payment(&outbound_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&outbound_payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!( + node_a.payment(&outbound_payment_id).unwrap().amount_msat, + Some(determined_amount_msat) + ); + assert!(matches!( + node_a.payment(&outbound_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); + assert_eq!(node_b.payment(&inbound_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&inbound_payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!( + node_b.payment(&inbound_payment_id).unwrap().amount_msat, + Some(determined_amount_msat) + ); + assert!(matches!( + node_b.payment(&inbound_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); // Test claiming manually registered payments. let invoice_amount_3_msat = 5_532_000; @@ -1222,27 +1294,33 @@ pub(crate) async fn do_channel_full_cycle( manual_payment_hash, ) .unwrap(); - let manual_payment_id = node_a.bolt11_payment().send(&manual_invoice, None).unwrap(); + let outbound_manual_payment_id = node_a.bolt11_payment().send(&manual_invoice, None).unwrap(); - let claimable_amount_msat = expect_payment_claimable_event!( - node_b, - manual_payment_id, - manual_payment_hash, - invoice_amount_3_msat - ); + let (manual_payment_id, claimable_amount_msat) = + expect_payment_claimable_event!(node_b, manual_payment_hash, invoice_amount_3_msat); node_b .bolt11_payment() - .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) + .claim_for_id(manual_payment_id, claimable_amount_msat, manual_preimage) .unwrap(); - expect_payment_received_event!(node_b, claimable_amount_msat); - expect_payment_successful_event!(node_a, manual_payment_id, None); - assert_eq!(node_a.payment(&manual_payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&manual_payment_id).unwrap().direction, PaymentDirection::Outbound); + let received_payment_id = expect_payment_received_event!(node_b, claimable_amount_msat); + assert_eq!(received_payment_id, manual_payment_id); + expect_payment_successful_event!(node_a, outbound_manual_payment_id, None); + assert_eq!( + node_a.payment(&outbound_manual_payment_id).unwrap().status, + PaymentStatus::Succeeded + ); + assert_eq!( + node_a.payment(&outbound_manual_payment_id).unwrap().direction, + PaymentDirection::Outbound + ); assert_eq!( - node_a.payment(&manual_payment_id).unwrap().amount_msat, + node_a.payment(&outbound_manual_payment_id).unwrap().amount_msat, Some(invoice_amount_3_msat) ); - assert!(matches!(node_a.payment(&manual_payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); + assert!(matches!( + node_a.payment(&outbound_manual_payment_id).unwrap().kind, + PaymentKind::Bolt11 { .. } + )); assert_eq!(node_b.payment(&manual_payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_b.payment(&manual_payment_id).unwrap().direction, PaymentDirection::Inbound); assert_eq!( @@ -1265,27 +1343,27 @@ pub(crate) async fn do_channel_full_cycle( manual_fail_payment_hash, ) .unwrap(); - let manual_fail_payment_id = node_a.bolt11_payment().send(&manual_fail_invoice, None).unwrap(); + let outbound_manual_fail_payment_id = + node_a.bolt11_payment().send(&manual_fail_invoice, None).unwrap(); - expect_payment_claimable_event!( - node_b, - manual_fail_payment_id, - manual_fail_payment_hash, - invoice_amount_4_msat - ); - node_b.bolt11_payment().fail_for_hash(manual_fail_payment_hash).unwrap(); + let (manual_fail_payment_id, _) = + expect_payment_claimable_event!(node_b, manual_fail_payment_hash, invoice_amount_4_msat); + node_b.bolt11_payment().fail_for_id(manual_fail_payment_id).unwrap(); expect_event!(node_a, PaymentFailed); - assert_eq!(node_a.payment(&manual_fail_payment_id).unwrap().status, PaymentStatus::Failed); assert_eq!( - node_a.payment(&manual_fail_payment_id).unwrap().direction, + node_a.payment(&outbound_manual_fail_payment_id).unwrap().status, + PaymentStatus::Failed + ); + assert_eq!( + node_a.payment(&outbound_manual_fail_payment_id).unwrap().direction, PaymentDirection::Outbound ); assert_eq!( - node_a.payment(&manual_fail_payment_id).unwrap().amount_msat, + node_a.payment(&outbound_manual_fail_payment_id).unwrap().amount_msat, Some(invoice_amount_4_msat) ); assert!(matches!( - node_a.payment(&manual_fail_payment_id).unwrap().kind, + node_a.payment(&outbound_manual_fail_payment_id).unwrap().kind, PaymentKind::Bolt11 { .. } )); assert_eq!(node_b.payment(&manual_fail_payment_id).unwrap().status, PaymentStatus::Failed); @@ -1312,16 +1390,19 @@ pub(crate) async fn do_channel_full_cycle( .unwrap(); expect_event!(node_a, PaymentSuccessful); let next_event = node_b.next_event_async().await; - let (received_keysend_amount, received_custom_records) = match next_event { - ref e @ Event::PaymentReceived { amount_msat, ref custom_records, .. } => { - println!("{} got event {:?}", std::stringify!(node_b), e); - node_b.event_handled().unwrap(); - (amount_msat, custom_records) - }, - ref e => { - panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); - }, - }; + let (received_keysend_payment_id, received_keysend_amount, received_custom_records) = + match next_event { + ref e @ Event::PaymentReceived { + payment_id, amount_msat, ref custom_records, .. + } => { + println!("{} got event {:?}", std::stringify!(node_b), e); + node_b.event_handled().unwrap(); + (payment_id, amount_msat, custom_records) + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); + }, + }; assert_eq!(received_keysend_amount, keysend_amount_msat); assert_eq!(node_a.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); assert_eq!(node_a.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Outbound); @@ -1331,11 +1412,20 @@ pub(crate) async fn do_channel_full_cycle( PaymentKind::Spontaneous { .. } )); assert_eq!(received_custom_records, &custom_tlvs); - assert_eq!(node_b.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat)); + assert_eq!( + node_b.payment(&received_keysend_payment_id).unwrap().status, + PaymentStatus::Succeeded + ); + assert_eq!( + node_b.payment(&received_keysend_payment_id).unwrap().direction, + PaymentDirection::Inbound + ); + assert_eq!( + node_b.payment(&received_keysend_payment_id).unwrap().amount_msat, + Some(keysend_amount_msat) + ); assert!(matches!( - node_b.payment(&keysend_payment_id).unwrap().kind, + node_b.payment(&received_keysend_payment_id).unwrap().kind, PaymentKind::Spontaneous { .. } )); assert_eq!( @@ -1344,7 +1434,7 @@ pub(crate) async fn do_channel_full_cycle( ); assert_eq!( node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })).len(), - 6 + 5 ); assert_eq!( node_a diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 708a9add7e..af15dfe9cc 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -290,7 +290,7 @@ async fn multi_hop_sending() { .bolt11_payment() .receive(2_500_000, &invoice_description.clone().into(), 9217) .unwrap(); - nodes[0].bolt11_payment().send(&invoice, Some(route_params)).unwrap(); + let outbound_payment_id = nodes[0].bolt11_payment().send(&invoice, Some(route_params)).unwrap(); expect_event!(nodes[1], PaymentForwarded); @@ -299,9 +299,9 @@ async fn multi_hop_sending() { let node_3_fwd_event = matches!(nodes[3].next_event(), Some(Event::PaymentForwarded { .. })); assert!(node_2_fwd_event || node_3_fwd_event); - let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); + expect_payment_received_event!(&nodes[4], 2_500_000); let fee_paid_msat = Some(2000); - expect_payment_successful_event!(nodes[0], payment_id, Some(fee_paid_msat)); + expect_payment_successful_event!(nodes[0], outbound_payment_id, Some(fee_paid_msat)); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -380,7 +380,6 @@ async fn split_underpaid_bolt11_payment() { .unwrap(); let receiver_payment_id = expect_payment_received_event!(node_c, amount_msat); - assert_eq!(receiver_payment_id, PaymentId(invoice.payment_hash().0)); expect_payment_successful_event!(node_a, payment_id_a, None); expect_payment_successful_event!(node_b, payment_id_b, None); @@ -1376,7 +1375,7 @@ async fn simple_bolt12_send_receive() { matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == payment_id }); assert_eq!(node_a_payments.len(), 1); - let payment_hash = match node_a_payments.first().unwrap().kind { + match node_a_payments.first().unwrap().kind { PaymentKind::Bolt12Offer { hash, preimage, @@ -1392,7 +1391,6 @@ async fn simple_bolt12_send_receive() { assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 // API currently doesn't allow to do that. - hash.unwrap() }, _ => { panic!("Unexpected payment kind"); @@ -1400,8 +1398,7 @@ async fn simple_bolt12_send_receive() { }; assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - expect_payment_received_event!(node_b, expected_amount_msat); - let node_b_payment_id = PaymentId(payment_hash.0); + let node_b_payment_id = expect_payment_received_event!(node_b, expected_amount_msat); let node_b_payments = node_b.list_payments_with_filter(|p| { matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == node_b_payment_id }); @@ -1433,8 +1430,8 @@ async fn simple_bolt12_send_receive() { None, ) .unwrap(); - let invoice = node_a.bolt12_payment().request_refund_payment(&refund).unwrap(); - expect_payment_received_event!(node_a, overpaid_amount); + let _invoice = node_a.bolt12_payment().request_refund_payment(&refund).unwrap(); + let node_a_payment_id = expect_payment_received_event!(node_a, overpaid_amount); let node_b_payment_id = node_b .list_payments_with_filter(|p| { @@ -1471,7 +1468,6 @@ async fn simple_bolt12_send_receive() { } assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount)); - let node_a_payment_id = PaymentId(invoice.payment_hash().0); let node_a_payments = node_a.list_payments_with_filter(|p| { matches!(p.kind, PaymentKind::Bolt12Refund { .. }) && p.id == node_a_payment_id }); @@ -1990,7 +1986,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. println!("Paying JIT invoice!"); - let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + let payer_payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); expect_channel_pending_event!(service_node, client_node.node_id()); expect_channel_ready_event!(service_node, client_node.node_id()); expect_event!(service_node, PaymentForwarded); @@ -1999,7 +1995,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - expect_payment_successful_event!(payer_node, payment_id, None); + expect_payment_successful_event!(payer_node, payer_payment_id, None); let client_payment_id = expect_payment_received_event!(client_node, expected_received_amount_msat); let client_payment = client_node.payment(&client_payment_id).unwrap(); @@ -2033,7 +2029,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { expect_payment_received_event!(client_node, amount_msat); //////////////////////////////////////////////////////////////////////////// - // receive_via_jit_channel_for_hash and claim_for_hash + // receive_via_jit_channel_for_hash and claim_for_id //////////////////////////////////////////////////////////////////////////// println!("Generating JIT invoice!"); // Increase the amount to make sure it does not fit into the existing channels. @@ -2053,7 +2049,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. println!("Paying JIT invoice!"); - let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + let payer_payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); expect_channel_pending_event!(service_node, client_node.node_id()); expect_channel_ready_event!(service_node, client_node.node_id()); expect_channel_pending_event!(client_node, service_node.node_id()); @@ -2061,22 +2057,22 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - let claimable_amount_msat = expect_payment_claimable_event!( + let (client_payment_id, claimable_amount_msat) = expect_payment_claimable_event!( client_node, - payment_id, manual_payment_hash, expected_received_amount_msat ); println!("Claiming payment!"); client_node .bolt11_payment() - .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) + .claim_for_id(client_payment_id, claimable_amount_msat, manual_preimage) .unwrap(); expect_event!(service_node, PaymentForwarded); - expect_payment_successful_event!(payer_node, payment_id, None); - let client_payment_id = + expect_payment_successful_event!(payer_node, payer_payment_id, None); + let received_payment_id = expect_payment_received_event!(client_node, expected_received_amount_msat); + assert_eq!(received_payment_id, client_payment_id); let client_payment = client_node.payment(&client_payment_id).unwrap(); match client_payment.kind { PaymentKind::Bolt11 { counterparty_skimmed_fee_msat, .. } => { @@ -2086,7 +2082,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { } //////////////////////////////////////////////////////////////////////////// - // receive_via_jit_channel_for_hash and fail_for_hash + // receive_via_jit_channel_for_hash and fail_for_id //////////////////////////////////////////////////////////////////////////// println!("Generating JIT invoice!"); // Increase the amount to make sure it does not fit into the existing channels. @@ -2106,7 +2102,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. println!("Paying JIT invoice!"); - let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + let _payer_payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); expect_channel_pending_event!(service_node, client_node.node_id()); expect_channel_ready_event!(service_node, client_node.node_id()); expect_channel_pending_event!(client_node, service_node.node_id()); @@ -2114,17 +2110,16 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - expect_payment_claimable_event!( + let (client_payment_id, _) = expect_payment_claimable_event!( client_node, - payment_id, manual_payment_hash, expected_received_amount_msat ); println!("Failing payment!"); - client_node.bolt11_payment().fail_for_hash(manual_payment_hash).unwrap(); + client_node.bolt11_payment().fail_for_id(client_payment_id).unwrap(); expect_event!(payer_node, PaymentFailed); - assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); + assert_eq!(client_node.payment(&client_payment_id).unwrap().status, PaymentStatus::Failed); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -2316,8 +2311,8 @@ async fn lsps2_client_trusts_lsp() { // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. println!("Paying JIT invoice!"); - let payment_id = payer_node.bolt11_payment().send(&res, None).unwrap(); - println!("Payment ID: {:?}", payment_id); + let payer_payment_id = payer_node.bolt11_payment().send(&res, None).unwrap(); + println!("Payment ID: {:?}", payer_payment_id); let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id()); expect_channel_ready_event!(service_node, client_node.node_id()); expect_channel_pending_event!(client_node, service_node.node_id()); @@ -2355,21 +2350,22 @@ async fn lsps2_client_trusts_lsp() { let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - let _ = expect_payment_claimable_event!( + let (client_payment_id, _) = expect_payment_claimable_event!( client_node, - payment_id, manual_payment_hash, expected_received_amount_msat ); client_node .bolt11_payment() - .claim_for_hash(manual_payment_hash, jit_amount_msat, manual_preimage) + .claim_for_id(client_payment_id, jit_amount_msat, manual_preimage) .unwrap(); - expect_payment_successful_event!(payer_node, payment_id, None); + expect_payment_successful_event!(payer_node, payer_payment_id, None); - let _ = expect_payment_received_event!(client_node, expected_received_amount_msat); + let received_payment_id = + expect_payment_received_event!(client_node, expected_received_amount_msat); + assert_eq!(received_payment_id, client_payment_id); // Check the nodes pick up on the confirmed funding tx now. wait_for_tx(&electrsd.client, funding_txo.txid).await; From 544e10067938256d250f5b5060916156d2f82829 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 24 Jun 2026 17:12:38 +0200 Subject: [PATCH 3/3] Track manual BOLT11 invoices Manual BOLT11 invoices need a pending entry before HTLC arrival. That lets duplicate registrations be rejected while keeping randomized payment IDs. Store the pending entries with their invoice expiry and resolve BOLT11 claimable events by scanning those entries. Co-Authored-By: HAL 9000 --- src/builder.rs | 1 + src/event.rs | 94 ++++++++++++++++++++-- src/lib.rs | 8 +- src/payment/bolt11.rs | 114 ++++++++++++++++++++++++++- src/payment/pending_payment_store.rs | 30 ++++++- tests/common/mod.rs | 11 +++ tests/integration_tests_rust.rs | 2 + 7 files changed, 246 insertions(+), 14 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 3df594b7cf..1df89f5c62 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2149,6 +2149,7 @@ fn build_with_store_internal( scorer, peer_store, payment_store, + pending_payment_store, lnurl_auth, is_running, node_metrics, diff --git a/src/event.rs b/src/event.rs index 2a122bc579..8e9b36c42e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -10,6 +10,7 @@ use core::task::{Poll, Waker}; use std::collections::VecDeque; use std::ops::Deref; use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::PublicKey; @@ -53,7 +54,8 @@ use crate::payment::store::{ use crate::payment::PaymentMetadata; use crate::runtime::Runtime; use crate::types::{ - CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet, + CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, PendingPaymentStore, + Sweeper, Wallet, }; use crate::{ hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, @@ -529,6 +531,7 @@ where network_graph: Arc, liquidity_source: Arc>>, payment_store: Arc, + pending_payment_store: Arc, peer_store: Arc>, keys_manager: Arc, runtime: Arc, @@ -549,10 +552,10 @@ where channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, liquidity_source: Arc>>, payment_store: Arc, - peer_store: Arc>, keys_manager: Arc, - static_invoice_store: Option, onion_messenger: Arc, - om_mailbox: Option>, runtime: Arc, logger: L, - config: Arc, + pending_payment_store: Arc, peer_store: Arc>, + keys_manager: Arc, static_invoice_store: Option, + onion_messenger: Arc, om_mailbox: Option>, + runtime: Arc, logger: L, config: Arc, ) -> Self { Self { event_queue, @@ -564,6 +567,7 @@ where network_graph, liquidity_source, payment_store, + pending_payment_store, peer_store, keys_manager, logger, @@ -605,6 +609,59 @@ where }) } + fn current_time_secs() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs() + } + + async fn prune_expired_pending_payments(&self) -> Result<(), ReplayEvent> { + let now = Self::current_time_secs(); + let expired_payment_ids = self + .pending_payment_store + .list_filter(|payment| payment.has_expired(now)) + .into_iter() + .map(|payment| payment.details.id) + .collect::>(); + + for payment_id in expired_payment_ids { + if let Err(e) = self.pending_payment_store.remove(&payment_id).await { + log_error!( + self.logger, + "Failed to remove expired pending payment with ID {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + } + } + + Ok(()) + } + + fn find_inbound_payment_by_hash(&self, payment_hash: &PaymentHash) -> Option { + self.payment_store + .list_filter(|payment| { + payment.direction == PaymentDirection::Inbound + && matches!(&payment.kind, PaymentKind::Bolt11 { hash, .. } if hash == payment_hash) + }) + .first() + .cloned() + } + + async fn find_pending_inbound_payment_id( + &self, payment_hash: &PaymentHash, + ) -> Result, ReplayEvent> { + self.prune_expired_pending_payments().await?; + Ok(self + .pending_payment_store + .list_filter(|payment| { + payment.details.direction == PaymentDirection::Inbound + && payment.details.status == PaymentStatus::Pending + && matches!(&payment.details.kind, PaymentKind::Bolt11 { hash, .. } if hash == payment_hash) + }) + .first() + .map(|payment| payment.details.id)) + } + fn resolve_inbound_payment_id( &self, event_payment_id: PaymentId, payment_hash: &PaymentHash, ) -> (PaymentId, Option) { @@ -619,6 +676,10 @@ where } } + if let Some(info) = self.find_inbound_payment_by_hash(payment_hash) { + return (info.id, Some(info)); + } + (event_payment_id, None) } @@ -735,8 +796,16 @@ where .. } => { let event_payment_id = payment_id.unwrap_or(PaymentId(payment_hash.0)); - let (payment_id, payment_info) = + let (mut payment_id, payment_info) = self.resolve_inbound_payment_id(event_payment_id, &payment_hash); + let pending_payment_id = if payment_info.is_none() { + self.find_pending_inbound_payment_id(&payment_hash).await? + } else { + None + }; + if let Some(pending_payment_id) = pending_payment_id { + payment_id = pending_payment_id; + } if let Some(info) = payment_info.as_ref() { if info.direction == PaymentDirection::Outbound { log_info!( @@ -911,6 +980,19 @@ where return Err(ReplayEvent()); }, } + + if pending_payment_id.is_some() { + if let Err(e) = self.pending_payment_store.remove(&payment_id).await + { + log_error!( + self.logger, + "Failed to remove pending payment with ID {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + } + } } if payment_preimage.is_none() { diff --git a/src/lib.rs b/src/lib.rs index cf742783f0..e43db451ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,8 +176,8 @@ use runtime::Runtime; pub use tokio; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, - Wallet, + HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, PendingPaymentStore, + Router, Scorer, Sweeper, Wallet, }; pub use types::{ ChannelCounterparty, ChannelDetails, CustomTlvRecord, PeerDetails, ReserveType, UserChannelId, @@ -244,6 +244,7 @@ pub struct Node { scorer: Arc>, peer_store: Arc>>, payment_store: Arc, + pending_payment_store: Arc, lnurl_auth: Arc, is_running: Arc>, node_metrics: Arc, @@ -605,6 +606,7 @@ impl Node { Arc::clone(&self.network_graph), Arc::clone(&self.liquidity_source), Arc::clone(&self.payment_store), + Arc::clone(&self.pending_payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.keys_manager), static_invoice_store, @@ -949,6 +951,7 @@ impl Node { Arc::clone(&self.connection_manager), Arc::clone(&self.liquidity_source), Arc::clone(&self.payment_store), + Arc::clone(&self.pending_payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.config), Arc::clone(&self.is_running), @@ -968,6 +971,7 @@ impl Node { Arc::clone(&self.connection_manager), Arc::clone(&self.liquidity_source), Arc::clone(&self.payment_store), + Arc::clone(&self.pending_payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.config), Arc::clone(&self.is_running), diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 68e1ccca9e..876ea38728 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -10,6 +10,7 @@ //! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; @@ -23,7 +24,7 @@ use lightning::sign::EntropySource; use lightning_invoice::{ Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription, }; -use lightning_types::payment::{PaymentHash, PaymentPreimage}; +use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::connection::ConnectionManager; @@ -36,9 +37,10 @@ use crate::payment::store::{ LSPS2Parameters, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; +use crate::payment::PendingPaymentDetails; use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, KeysManager, PaymentStore}; +use crate::types::{ChannelManager, KeysManager, PaymentStore, PendingPaymentStore}; #[cfg(not(feature = "uniffi"))] type Bolt11Invoice = LdkBolt11Invoice; @@ -74,6 +76,7 @@ pub struct Bolt11Payment { connection_manager: Arc>>, liquidity_source: Arc>>, payment_store: Arc, + pending_payment_store: Arc, peer_store: Arc>>, config: Arc, is_running: Arc>, @@ -85,8 +88,8 @@ impl Bolt11Payment { runtime: Arc, channel_manager: Arc, keys_manager: Arc, connection_manager: Arc>>, liquidity_source: Arc>>, payment_store: Arc, - peer_store: Arc>>, config: Arc, - is_running: Arc>, logger: Arc, + pending_payment_store: Arc, peer_store: Arc>>, + config: Arc, is_running: Arc>, logger: Arc, ) -> Self { Self { runtime, @@ -95,6 +98,7 @@ impl Bolt11Payment { connection_manager, liquidity_source, payment_store, + pending_payment_store, peer_store, config, is_running, @@ -102,10 +106,86 @@ impl Bolt11Payment { } } + fn current_time_secs() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_secs() + } + + fn prune_expired_pending_payments(&self) -> Result<(), Error> { + let now = Self::current_time_secs(); + let expired_payment_ids = self + .pending_payment_store + .list_filter(|payment| payment.has_expired(now)) + .into_iter() + .map(|payment| payment.details.id) + .collect::>(); + + for payment_id in expired_payment_ids { + self.runtime.block_on(self.pending_payment_store.remove(&payment_id))?; + } + + Ok(()) + } + + fn has_pending_or_succeeded_inbound_payment(&self, payment_hash: &PaymentHash) -> bool { + !self + .payment_store + .list_filter(|payment| { + payment.direction == PaymentDirection::Inbound + && matches!(&payment.kind, PaymentKind::Bolt11 { hash, .. } if hash == payment_hash) + && matches!(payment.status, PaymentStatus::Pending | PaymentStatus::Succeeded) + }) + .is_empty() + || !self + .pending_payment_store + .list_filter(|payment| { + payment.details.direction == PaymentDirection::Inbound + && matches!(&payment.details.kind, PaymentKind::Bolt11 { hash, .. } if hash == payment_hash) + && matches!( + payment.details.status, + PaymentStatus::Pending | PaymentStatus::Succeeded + ) + }) + .is_empty() + } + + fn register_manual_claim_invoice( + &self, payment_hash: PaymentHash, amount_msat: Option, payment_secret: PaymentSecret, + expiry_secs: u32, + ) -> Result<(), Error> { + let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes()); + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(payment_secret), + counterparty_skimmed_fee_msat: None, + }; + let payment = PaymentDetails::new( + payment_id, + kind, + amount_msat, + None, + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); + let expires_at = Some(Self::current_time_secs().saturating_add(expiry_secs as u64)); + let pending_payment = + PendingPaymentDetails::new_with_expiry(payment, Vec::new(), expires_at); + self.runtime.block_on(self.pending_payment_store.insert_or_update(pending_payment))?; + Ok(()) + } + pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, ) -> Result { + if let Some(payment_hash) = manual_claim_payment_hash { + self.prune_expired_pending_payments()?; + if self.has_pending_or_succeeded_inbound_payment(&payment_hash) { + log_error!(self.logger, "Payment error: an invoice must not be paid twice."); + return Err(Error::DuplicatePayment); + } + } + let invoice = { let invoice_params = Bolt11InvoiceParameters { amount_msats: amount_msat, @@ -127,6 +207,15 @@ impl Bolt11Payment { } }; + if let Some(payment_hash) = manual_claim_payment_hash { + self.register_manual_claim_invoice( + payment_hash, + amount_msat, + *invoice.payment_secret(), + expiry_secs, + )?; + } + Ok(invoice) } @@ -135,6 +224,14 @@ impl Bolt11Payment { expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result { + if let Some(payment_hash) = payment_hash { + self.prune_expired_pending_payments()?; + if self.has_pending_or_succeeded_inbound_payment(&payment_hash) { + log_error!(self.logger, "Payment error: an invoice must not be paid twice."); + return Err(Error::DuplicatePayment); + } + } + let connection_manager = Arc::clone(&self.connection_manager); let (invoice, chosen_lsp) = self.runtime.block_on(async move { if let Some(amount_msat) = amount_msat { @@ -167,6 +264,15 @@ impl Bolt11Payment { let peer_info = PeerInfo { node_id: chosen_lsp.node_id, address: chosen_lsp.address }; self.runtime.block_on(self.peer_store.add_peer(peer_info))?; + if let Some(payment_hash) = payment_hash { + self.register_manual_claim_invoice( + payment_hash, + amount_msat, + *invoice.payment_secret(), + expiry_secs, + )?; + } + Ok(invoice) } } diff --git a/src/payment/pending_payment_store.rs b/src/payment/pending_payment_store.rs index eb72f89ec9..358c078514 100644 --- a/src/payment/pending_payment_store.rs +++ b/src/payment/pending_payment_store.rs @@ -20,22 +20,35 @@ pub struct PendingPaymentDetails { pub details: PaymentDetails, /// Transaction IDs that have replaced or conflict with this payment. pub conflicting_txids: Vec, + /// The timestamp after which this pending payment can be pruned. + pub expires_at: Option, } impl PendingPaymentDetails { pub(crate) fn new(details: PaymentDetails, conflicting_txids: Vec) -> Self { - Self { details, conflicting_txids } + Self::new_with_expiry(details, conflicting_txids, None) + } + + pub(crate) fn new_with_expiry( + details: PaymentDetails, conflicting_txids: Vec, expires_at: Option, + ) -> Self { + Self { details, conflicting_txids, expires_at } } /// Convert to finalized payment for the main payment store pub fn into_payment_details(self) -> PaymentDetails { self.details } + + pub(crate) fn has_expired(&self, now: u64) -> bool { + self.expires_at.map_or(false, |expires_at| expires_at <= now) + } } impl_writeable_tlv_based!(PendingPaymentDetails, { (0, details, required), (2, conflicting_txids, optional_vec), + (4, expires_at, option), }); #[derive(Clone, Debug, PartialEq, Eq)] @@ -43,6 +56,7 @@ pub(crate) struct PendingPaymentDetailsUpdate { pub id: PaymentId, pub payment_update: Option, pub conflicting_txids: Option>, + pub expires_at: Option>, } impl StorableObject for PendingPaymentDetails { @@ -68,6 +82,13 @@ impl StorableObject for PendingPaymentDetails { } } + if let Some(new_expires_at) = update.expires_at { + if self.expires_at != new_expires_at { + self.expires_at = new_expires_at; + updated = true; + } + } + updated } @@ -89,6 +110,11 @@ impl From<&PendingPaymentDetails> for PendingPaymentDetailsUpdate { } else { Some(value.conflicting_txids.clone()) }; - Self { id: value.id(), payment_update: Some(value.details.to_update()), conflicting_txids } + Self { + id: value.id(), + payment_update: Some(value.details.to_update()), + conflicting_txids, + expires_at: Some(value.expires_at), + } } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d8339a88ca..686c50c070 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1294,10 +1294,20 @@ pub(crate) async fn do_channel_full_cycle( manual_payment_hash, ) .unwrap(); + assert!(matches!( + node_b.bolt11_payment().receive_for_hash( + invoice_amount_3_msat, + &invoice_description.clone().into(), + 9217, + manual_payment_hash, + ), + Err(NodeError::DuplicatePayment) + )); let outbound_manual_payment_id = node_a.bolt11_payment().send(&manual_invoice, None).unwrap(); let (manual_payment_id, claimable_amount_msat) = expect_payment_claimable_event!(node_b, manual_payment_hash, invoice_amount_3_msat); + assert_ne!(manual_payment_id.0, manual_payment_hash.0); node_b .bolt11_payment() .claim_for_id(manual_payment_id, claimable_amount_msat, manual_preimage) @@ -1348,6 +1358,7 @@ pub(crate) async fn do_channel_full_cycle( let (manual_fail_payment_id, _) = expect_payment_claimable_event!(node_b, manual_fail_payment_hash, invoice_amount_4_msat); + assert_ne!(manual_fail_payment_id.0, manual_fail_payment_hash.0); node_b.bolt11_payment().fail_for_id(manual_fail_payment_id).unwrap(); expect_event!(node_a, PaymentFailed); assert_eq!( diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index af15dfe9cc..9490adc266 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -2062,6 +2062,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { manual_payment_hash, expected_received_amount_msat ); + assert_ne!(client_payment_id.0, manual_payment_hash.0); println!("Claiming payment!"); client_node .bolt11_payment() @@ -2115,6 +2116,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { manual_payment_hash, expected_received_amount_msat ); + assert_ne!(client_payment_id.0, manual_payment_hash.0); println!("Failing payment!"); client_node.bolt11_payment().fail_for_id(client_payment_id).unwrap();