From cb8848eb0beed8e680f1a53c476a80f655cba8a8 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 4 Feb 2026 10:55:56 +0100 Subject: [PATCH 1/7] 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 ++++ benches/payments.rs | 8 ++---- src/event.rs | 25 +++++++----------- tests/common/mod.rs | 6 ++--- tests/integration_tests_hrn.rs | 2 +- tests/integration_tests_migration.rs | 4 +-- tests/integration_tests_rust.rs | 39 ++++++++++++++-------------- tests/upgrade_downgrade_tests.rs | 5 ++-- 8 files changed, 43 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b231e8d1c4..be583459fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ a persisted `ChannelClosed` event. - 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. ## Feature and API updates - The Bitcoin Core RPC and REST chain-source builder methods now accept an optional diff --git a/benches/payments.rs b/benches/payments.rs index 926dc5dade..91de0bfa77 100644 --- a/benches/payments.rs +++ b/benches/payments.rs @@ -75,12 +75,8 @@ async fn send_payments(node_a: Arc, node_b: Arc) -> std::time::Durat while success_count < total_payments { match node_a.next_event_async().await { Event::PaymentSuccessful { payment_id, payment_hash, .. } => { - if let Some(id) = payment_id { - success_count += 1; - println!("{}: Payment with id {:?} completed", payment_hash.0.as_hex(), id); - } else { - println!("Payment completed (no payment_id)"); - } + success_count += 1; + println!("{}: Payment with id {:?} completed", payment_hash.0.as_hex(), payment_id); }, Event::PaymentFailed { payment_id, payment_hash, .. } => { println!("{}: Payment {:?} failed", payment_hash.unwrap().0.as_hex(), payment_id); diff --git a/src/event.rs b/src/event.rs index b8ca735198..573f441fae 100644 --- a/src/event.rs +++ b/src/event.rs @@ -105,9 +105,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`. @@ -133,9 +131,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 @@ -151,9 +147,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), }, @@ -1163,7 +1157,7 @@ where } let event = Event::PaymentReceived { - payment_id: Some(payment_id), + payment_id, payment_hash, amount_msat, custom_records: onion_fields @@ -1228,7 +1222,7 @@ where ); }); let event = Event::PaymentSuccessful { - payment_id: Some(payment_id), + payment_id, payment_hash, payment_preimage: Some(payment_preimage), fee_paid_msat, @@ -1264,8 +1258,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 50e2b993c8..9e21021874 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -224,7 +224,7 @@ 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(); + let payment = $node.payment(&payment_id).unwrap(); if !matches!(payment.kind, ldk_node::payment::PaymentKind::Onchain { .. }) { assert_eq!(payment.fee_paid_msat, None); } @@ -292,7 +292,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(); @@ -1403,7 +1403,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 6e758105a2..d61604798c 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_migration.rs b/tests/integration_tests_migration.rs index 7e5767dca6..53f60d19f7 100644 --- a/tests/integration_tests_migration.rs +++ b/tests/integration_tests_migration.rs @@ -203,7 +203,7 @@ async fn migrate_node_across_all_backends() { Bolt11InvoiceDescription::Direct(Description::new("ln send".to_string()).unwrap()); let invoice = node_b.bolt11_payment().receive(10_000, &description.into(), 3600).unwrap(); let ln_send_id = node.bolt11_payment().send(&invoice, None).unwrap(); - expect_payment_successful_event!(node, Some(ln_send_id), None); + expect_payment_successful_event!(node, ln_send_id, None); expect_payment_received_event!(node_b, 10_000); // Lightning receive: node B -> node. @@ -211,7 +211,7 @@ async fn migrate_node_across_all_backends() { Bolt11InvoiceDescription::Direct(Description::new("ln receive".to_string()).unwrap()); let invoice = node.bolt11_payment().receive(5_000, &description.into(), 3600).unwrap(); let ln_receive_id = node_b.bolt11_payment().send(&invoice, None).unwrap(); - expect_payment_successful_event!(node_b, Some(ln_receive_id), None); + expect_payment_successful_event!(node_b, ln_receive_id, None); expect_payment_received_event!(node, 5_000); // On-chain send: node -> a foreign address. diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index e401c82189..02ecbfd7c0 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -585,13 +585,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)); @@ -1793,7 +1792,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 @@ -2315,7 +2314,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" @@ -2389,7 +2388,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 }); @@ -2462,7 +2461,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 @@ -2636,7 +2635,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)] @@ -2862,7 +2861,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(); @@ -2882,7 +2881,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 = @@ -3017,9 +3016,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, .. } => { @@ -3046,7 +3045,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); @@ -3092,9 +3091,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, .. } => { @@ -3199,7 +3198,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); @@ -3385,9 +3384,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 de5bef96e8..1b9a92a1a3 100644 --- a/tests/upgrade_downgrade_tests.rs +++ b/tests/upgrade_downgrade_tests.rs @@ -304,7 +304,7 @@ // ) { // 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), @@ -313,9 +313,8 @@ // // 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 31eb068580f068045056bf24a9b7fb2ed6723080 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 28 Jul 2026 13:46:31 +0200 Subject: [PATCH 2/7] Track unknown BOLT11 manual claims Gate manual claiming for unknown custom-hash BOLT11 payments behind a config flag. Nodes that do not opt in fail these payments back without storing or queueing user events. Co-Authored-By: HAL 9000 --- CHANGELOG.md | 5 + src/config.rs | 13 +++ src/event.rs | 132 ++++++++++++++++------ src/payment/bolt11.rs | 166 ++++++++++++++++------------ tests/common/mod.rs | 1 + tests/integration_tests_postgres.rs | 3 +- tests/integration_tests_vss.rs | 3 +- 7 files changed, 214 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be583459fc..5fafb0fe12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ 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. +- Applications using the BOLT 11 manual-claiming receive APIs + (`receive_*_for_hash`) now need to set + `Config::manually_claim_unknown_bolt11_payments` to `true`. Otherwise + matching inbound HTLCs will be failed back instead of emitting + `Event::PaymentClaimable`. ## Feature and API updates - The Bitcoin Core RPC and REST chain-source builder methods now accept an optional diff --git a/src/config.rs b/src/config.rs index 772c4bd806..16a8caa265 100644 --- a/src/config.rs +++ b/src/config.rs @@ -155,6 +155,7 @@ pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_ /// | `route_parameters` | None | /// | `tor_config` | None | /// | `hrn_config` | HumanReadableNamesConfig::default() | +/// | `manually_claim_unknown_bolt11_payments` | false | /// /// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their /// respective default values. @@ -219,6 +220,17 @@ pub struct Config { /// /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki pub hrn_config: HumanReadableNamesConfig, + /// Whether to emit [`Event::PaymentClaimable`] for unknown BOLT 11 payments that were created + /// with a user-provided payment hash and therefore need to be manually claimed. + /// + /// If disabled, such payments are failed back without being added to the payment store. + /// + /// **Warning:** Enabling this may let any holder of a valid invoice generated by this node tie + /// up inbound HTLC slots and grow the persisted event queue until the payment is claimed, + /// failed, or times out. + /// + /// [`Event::PaymentClaimable`]: crate::Event::PaymentClaimable + pub manually_claim_unknown_bolt11_payments: bool, } impl Default for Config { @@ -235,6 +247,7 @@ impl Default for Config { route_parameters: None, node_alias: None, hrn_config: HumanReadableNamesConfig::default(), + manually_claim_unknown_bolt11_payments: false, } } } diff --git a/src/event.rs b/src/event.rs index 573f441fae..4334506aee 100644 --- a/src/event.rs +++ b/src/event.rs @@ -194,15 +194,21 @@ pub enum Event { /// The caveat described above the `total_fee_earned_msat` field applies here as well. outbound_amount_forwarded_msat: Option, }, - /// A payment for a previously-registered payment hash has been received. + /// A BOLT 11 payment with a user-provided payment hash has been received. /// /// This needs to be manually claimed by supplying the correct preimage to [`claim_for_hash`]. /// + /// This event is only emitted for unknown payments if + /// [`Config::manually_claim_unknown_bolt11_payments`] is enabled. Enabling it may let any + /// holder of a valid invoice generated by this node tie up inbound HTLC slots and grow the + /// persisted event queue until the payment is claimed, failed, or times out. + /// /// 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`]. /// /// Note claiming will necessarily fail after the `claim_deadline` has been reached. /// + /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments /// [`claim_for_hash`]: crate::payment::Bolt11Payment::claim_for_hash /// [`fail_for_hash`]: crate::payment::Bolt11Payment::fail_for_hash PaymentClaimable { @@ -906,45 +912,99 @@ where } } - if let Some(info) = payment_info { + let should_emit_payment_claimable = if let Some(info) = payment_info.as_ref() { // 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" - ); + // the payment 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" + ); + true + }, + _ => false, + } + } else if let PaymentPurpose::Bolt11InvoicePayment { + payment_preimage: None, + payment_secret, + .. + } = &purpose + { + if !self.config.manually_claim_unknown_bolt11_payments { + log_info!( + self.logger, + "Refusing unknown BOLT11 payment with hash {} as manual claiming is disabled", + hex_utils::to_string(&payment_hash.0), + ); + self.channel_manager.fail_htlc_backwards(&payment_hash); + return Ok(()); + } - 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()); - }, - }; - } + let invoice_amount_msat = + amount_msat.saturating_add(counterparty_skimmed_fee_msat); + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + 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(invoice_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()); }, - _ => {}, } + true + } else { + false + }; + if should_emit_payment_claimable { + 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!( diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 4503dfa061..733ee417ee 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -124,26 +124,25 @@ impl Bolt11Payment { } }; + if manual_claim_payment_hash.is_some() { + return Ok(invoice); + } + 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 - }; + // 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 preimage = self + .channel_manager + .get_payment_preimage_decrypt_metadata( + payment_hash, + payment_secret.clone(), + payment_metadata.as_deref_mut(), + ) + .ok(); + debug_assert!(preimage.is_some(), "We just let ChannelManager create an inbound payment, it can't have forgotten the preimage by now."); let kind = PaymentKind::Bolt11 { hash: payment_hash, preimage, @@ -168,6 +167,7 @@ impl Bolt11Payment { expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result { + let should_register_payment = payment_hash.is_none(); 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 { @@ -196,34 +196,36 @@ 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))?; + if should_register_payment { + // 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 }; @@ -600,14 +602,19 @@ impl Bolt11Payment { /// Returns a payable invoice that can be used to request a payment of the amount /// given for the given payment hash. /// - /// We will register the given payment hash and emit a [`PaymentClaimable`] event once - /// the inbound payment arrives. + /// If [`Config::manually_claim_unknown_bolt11_payments`] is enabled, a + /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the + /// inbound payment will be failed back when it arrives. + /// + /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a + /// payment hash is unsafe and can lead to loss of funds. /// - /// **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 - /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via - /// [`fail_for_hash`]. + /// **Note:** if manual claiming is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* + /// immediately fail the payment via [`fail_for_hash`]. /// + /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash @@ -636,14 +643,19 @@ impl Bolt11Payment { /// Returns a payable invoice that can be used to request a payment for the given payment hash /// and the amount to be determined by the user, also known as a "zero-amount" invoice. /// - /// We will register the given payment hash and emit a [`PaymentClaimable`] event once - /// the inbound payment arrives. + /// If [`Config::manually_claim_unknown_bolt11_payments`] is enabled, a + /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the + /// inbound payment will be failed back when it 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 - /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via - /// [`fail_for_hash`]. + /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a + /// payment hash is unsafe and can lead to loss of funds. /// + /// **Note:** if manual claiming is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* + /// immediately fail the payment via [`fail_for_hash`]. + /// + /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash @@ -690,16 +702,22 @@ impl Bolt11Payment { /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the /// channel to us. We'll use its cheapest offer otherwise. /// - /// We will register the given payment hash and emit a [`PaymentClaimable`] event once - /// the inbound payment arrives. The check that [`counterparty_skimmed_fee_msat`] is within the limits - /// is performed *before* emitting the event. + /// If [`Config::manually_claim_unknown_bolt11_payments`] is enabled, a + /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the + /// inbound payment will be failed back when it arrives. The check that + /// [`counterparty_skimmed_fee_msat`] is within the limits is performed *before* emitting the + /// event. + /// + /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a + /// payment hash is unsafe and can lead to loss of funds. /// - /// **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 - /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via - /// [`fail_for_hash`]. + /// **Note:** if manual claiming is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* + /// immediately fail the payment via [`fail_for_hash`]. /// /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash @@ -757,16 +775,22 @@ impl Bolt11Payment { /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. /// We'll use its cheapest offer otherwise. /// - /// We will register the given payment hash and emit a [`PaymentClaimable`] event once - /// the inbound payment arrives. The check that [`counterparty_skimmed_fee_msat`] is within the limits - /// is performed *before* emitting the event. + /// If [`Config::manually_claim_unknown_bolt11_payments`] is enabled, a + /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the + /// inbound payment will be failed back when it arrives. The check that + /// [`counterparty_skimmed_fee_msat`] is within the limits is performed *before* emitting the + /// event. + /// + /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a + /// payment hash is unsafe and can lead to loss of funds. /// - /// **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 - /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via - /// [`fail_for_hash`]. + /// **Note:** if manual claiming is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* + /// immediately fail the payment via [`fail_for_hash`]. /// /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9e21021874..a1646fd642 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -623,6 +623,7 @@ pub(crate) fn setup_two_nodes_with_store( println!("\n== Node B =="); let mut config_b = random_config(); config_b.store_type = store_type; + config_b.node_config.manually_claim_unknown_bolt11_payments = true; if cfg!(hrn_tests) { config_b.node_config.hrn_config = HumanReadableNamesConfig { diff --git a/tests/integration_tests_postgres.rs b/tests/integration_tests_postgres.rs index 889d681ba4..d71e36ca6d 100644 --- a/tests/integration_tests_postgres.rs +++ b/tests/integration_tests_postgres.rs @@ -37,7 +37,8 @@ async fn channel_full_cycle_with_postgres_store() { node_a.start().unwrap(); println!("\n== Node B =="); - let config_b = common::random_config(); + let mut config_b = common::random_config(); + config_b.node_config.manually_claim_unknown_bolt11_payments = true; let mut builder_b = Builder::from_config(config_b.node_config); builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index f0838585f7..3dd69905e7 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -35,7 +35,8 @@ async fn channel_full_cycle_with_vss_store() { node_a.start().unwrap(); println!("\n== Node B =="); - let config_b = common::random_config(); + let mut config_b = common::random_config(); + config_b.node_config.manually_claim_unknown_bolt11_payments = true; let mut builder_b = Builder::from_config(config_b.node_config); builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b From e6f152676dc66726da58bbb2d399c6ab16f2189e Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 29 Jul 2026 11:15:55 +0200 Subject: [PATCH 3/7] f Rename unknown BOLT11 config flag Use a name that reflects both possible user actions for unknown BOLT11 payments: either claim them with a preimage or fail them back. Co-Authored-By: HAL 9000 --- CHANGELOG.md | 2 +- src/config.rs | 6 +++--- src/event.rs | 8 ++++---- src/payment/bolt11.rs | 24 ++++++++++++------------ tests/common/mod.rs | 2 +- tests/integration_tests_postgres.rs | 2 +- tests/integration_tests_vss.rs | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fafb0fe12..55ef779e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ versions need to drain pending events before the upgrade. - Applications using the BOLT 11 manual-claiming receive APIs (`receive_*_for_hash`) now need to set - `Config::manually_claim_unknown_bolt11_payments` to `true`. Otherwise + `Config::manually_handle_unknown_bolt11_payments` to `true`. Otherwise matching inbound HTLCs will be failed back instead of emitting `Event::PaymentClaimable`. diff --git a/src/config.rs b/src/config.rs index 16a8caa265..438be7a872 100644 --- a/src/config.rs +++ b/src/config.rs @@ -155,7 +155,7 @@ pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_ /// | `route_parameters` | None | /// | `tor_config` | None | /// | `hrn_config` | HumanReadableNamesConfig::default() | -/// | `manually_claim_unknown_bolt11_payments` | false | +/// | `manually_handle_unknown_bolt11_payments` | false | /// /// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their /// respective default values. @@ -230,7 +230,7 @@ pub struct Config { /// failed, or times out. /// /// [`Event::PaymentClaimable`]: crate::Event::PaymentClaimable - pub manually_claim_unknown_bolt11_payments: bool, + pub manually_handle_unknown_bolt11_payments: bool, } impl Default for Config { @@ -247,7 +247,7 @@ impl Default for Config { route_parameters: None, node_alias: None, hrn_config: HumanReadableNamesConfig::default(), - manually_claim_unknown_bolt11_payments: false, + manually_handle_unknown_bolt11_payments: false, } } } diff --git a/src/event.rs b/src/event.rs index 4334506aee..03e4426aa2 100644 --- a/src/event.rs +++ b/src/event.rs @@ -199,7 +199,7 @@ pub enum Event { /// This needs to be manually claimed by supplying the correct preimage to [`claim_for_hash`]. /// /// This event is only emitted for unknown payments if - /// [`Config::manually_claim_unknown_bolt11_payments`] is enabled. Enabling it may let any + /// [`Config::manually_handle_unknown_bolt11_payments`] is enabled. Enabling it may let any /// holder of a valid invoice generated by this node tie up inbound HTLC slots and grow the /// persisted event queue until the payment is claimed, failed, or times out. /// @@ -208,7 +208,7 @@ pub enum Event { /// /// Note claiming will necessarily fail after the `claim_deadline` has been reached. /// - /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`claim_for_hash`]: crate::payment::Bolt11Payment::claim_for_hash /// [`fail_for_hash`]: crate::payment::Bolt11Payment::fail_for_hash PaymentClaimable { @@ -931,10 +931,10 @@ where .. } = &purpose { - if !self.config.manually_claim_unknown_bolt11_payments { + if !self.config.manually_handle_unknown_bolt11_payments { log_info!( self.logger, - "Refusing unknown BOLT11 payment with hash {} as manual claiming is disabled", + "Refusing unknown BOLT11 payment with hash {} as manual handling is disabled", hex_utils::to_string(&payment_hash.0), ); self.channel_manager.fail_htlc_backwards(&payment_hash); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 733ee417ee..9ae5ebb051 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -602,19 +602,19 @@ impl Bolt11Payment { /// Returns a payable invoice that can be used to request a payment of the amount /// given for the given payment hash. /// - /// If [`Config::manually_claim_unknown_bolt11_payments`] is enabled, a + /// If [`Config::manually_handle_unknown_bolt11_payments`] is enabled, a /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the /// inbound payment will be failed back when it arrives. /// /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a /// payment hash is unsafe and can lead to loss of funds. /// - /// **Note:** if manual claiming is enabled, users *MUST* handle this event and claim the + /// **Note:** if manual handling is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* /// immediately fail the payment via [`fail_for_hash`]. /// - /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash @@ -643,19 +643,19 @@ impl Bolt11Payment { /// Returns a payable invoice that can be used to request a payment for the given payment hash /// and the amount to be determined by the user, also known as a "zero-amount" invoice. /// - /// If [`Config::manually_claim_unknown_bolt11_payments`] is enabled, a + /// If [`Config::manually_handle_unknown_bolt11_payments`] is enabled, a /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the /// inbound payment will be failed back when it arrives. /// /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a /// payment hash is unsafe and can lead to loss of funds. /// - /// **Note:** if manual claiming is enabled, users *MUST* handle this event and claim the + /// **Note:** if manual handling is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* /// immediately fail the payment via [`fail_for_hash`]. /// - /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash @@ -702,7 +702,7 @@ impl Bolt11Payment { /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the /// channel to us. We'll use its cheapest offer otherwise. /// - /// If [`Config::manually_claim_unknown_bolt11_payments`] is enabled, a + /// If [`Config::manually_handle_unknown_bolt11_payments`] is enabled, a /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the /// inbound payment will be failed back when it arrives. The check that /// [`counterparty_skimmed_fee_msat`] is within the limits is performed *before* emitting the @@ -711,13 +711,13 @@ impl Bolt11Payment { /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a /// payment hash is unsafe and can lead to loss of funds. /// - /// **Note:** if manual claiming is enabled, users *MUST* handle this event and claim the + /// **Note:** if manual handling is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* /// immediately fail the payment via [`fail_for_hash`]. /// /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash @@ -775,7 +775,7 @@ impl Bolt11Payment { /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. /// We'll use its cheapest offer otherwise. /// - /// If [`Config::manually_claim_unknown_bolt11_payments`] is enabled, a + /// If [`Config::manually_handle_unknown_bolt11_payments`] is enabled, a /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the /// inbound payment will be failed back when it arrives. The check that /// [`counterparty_skimmed_fee_msat`] is within the limits is performed *before* emitting the @@ -784,13 +784,13 @@ impl Bolt11Payment { /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a /// payment hash is unsafe and can lead to loss of funds. /// - /// **Note:** if manual claiming is enabled, users *MUST* handle this event and claim the + /// **Note:** if manual handling is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* /// immediately fail the payment via [`fail_for_hash`]. /// /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - /// [`Config::manually_claim_unknown_bolt11_payments`]: crate::config::Config::manually_claim_unknown_bolt11_payments + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a1646fd642..475377e62c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -623,7 +623,7 @@ pub(crate) fn setup_two_nodes_with_store( println!("\n== Node B =="); let mut config_b = random_config(); config_b.store_type = store_type; - config_b.node_config.manually_claim_unknown_bolt11_payments = true; + config_b.node_config.manually_handle_unknown_bolt11_payments = true; if cfg!(hrn_tests) { config_b.node_config.hrn_config = HumanReadableNamesConfig { diff --git a/tests/integration_tests_postgres.rs b/tests/integration_tests_postgres.rs index d71e36ca6d..c9283039d5 100644 --- a/tests/integration_tests_postgres.rs +++ b/tests/integration_tests_postgres.rs @@ -38,7 +38,7 @@ async fn channel_full_cycle_with_postgres_store() { println!("\n== Node B =="); let mut config_b = common::random_config(); - config_b.node_config.manually_claim_unknown_bolt11_payments = true; + config_b.node_config.manually_handle_unknown_bolt11_payments = true; let mut builder_b = Builder::from_config(config_b.node_config); builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 3dd69905e7..d9f3c6cd5c 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -36,7 +36,7 @@ async fn channel_full_cycle_with_vss_store() { println!("\n== Node B =="); let mut config_b = common::random_config(); - config_b.node_config.manually_claim_unknown_bolt11_payments = true; + config_b.node_config.manually_handle_unknown_bolt11_payments = true; let mut builder_b = Builder::from_config(config_b.node_config); builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b From a9d3dcfba3ed2989ad48ae7a17722cc4a885f1bc Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 28 Jul 2026 14:06:23 +0200 Subject: [PATCH 4/7] Use payment IDs for BOLT11 payments Create inbound BOLT11 records from claimable and claimed events so inbound payments can be tracked by the IDs emitted by LDK. Generate outbound BOLT11 IDs from KeysManager entropy instead of deriving them from the payment hash. Co-Authored-By: HAL 9000 --- bindings/python/src/ldk_node/test_ldk_node.py | 2 +- src/event.rs | 571 ++++++++++++------ src/lib.rs | 2 + src/payment/bolt11.rs | 341 +++++------ tests/common/mod.rs | 259 ++++---- tests/integration_tests_rust.rs | 74 +-- 6 files changed, 690 insertions(+), 559 deletions(-) diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 304caf9c04..063c95f700 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -235,7 +235,7 @@ def test_spontaneous_payment(self): self.assertEqual(received_event.custom_records, custom_tlvs) sender_payment = node_1.payment(keysend_payment_id) - receiver_payment = node_2.payment(keysend_payment_id) + receiver_payment = node_2.payment(received_event.payment_id) self.assertIsNotNone(sender_payment) self.assertIsNotNone(receiver_payment) diff --git a/src/event.rs b/src/event.rs index 03e4426aa2..8d32ef1c1a 100644 --- a/src/event.rs +++ b/src/event.rs @@ -194,23 +194,23 @@ pub enum Event { /// The caveat described above the `total_fee_earned_msat` field applies here as well. outbound_amount_forwarded_msat: Option, }, - /// A BOLT 11 payment with a user-provided payment hash has been received. + /// A payment for a BOLT 11 invoice with a user-provided payment hash has been received. /// - /// This needs to be manually claimed by supplying the correct preimage to [`claim_for_hash`]. - /// - /// This event is only emitted for unknown payments if - /// [`Config::manually_handle_unknown_bolt11_payments`] is enabled. Enabling it may let any - /// holder of a valid invoice generated by this node tie up inbound HTLC slots and grow the - /// persisted event queue until the payment is claimed, failed, or times out. + /// 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. /// + /// This event is only emitted for unknown BOLT 11 payments if + /// [`Config::manually_handle_unknown_bolt11_payments`] is enabled. Enabling this may let any + /// holder of a valid invoice generated by this node tie up inbound HTLC slots and grow the + /// persisted event queue until the payment is claimed, failed, or times out. + /// + /// [`claim_for_id`]: crate::payment::Bolt11Payment::claim_for_id + /// [`fail_for_id`]: crate::payment::Bolt11Payment::fail_for_id /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments - /// [`claim_for_hash`]: crate::payment::Bolt11Payment::claim_for_hash - /// [`fail_for_hash`]: crate::payment::Bolt11Payment::fail_for_hash PaymentClaimable { /// A local identifier used to track the payment. payment_id: PaymentId, @@ -675,6 +675,25 @@ where }) } + fn resolve_inbound_payment_id( + &self, event_payment_id: Option, payment_hash: &PaymentHash, + ) -> (PaymentId, Option) { + let legacy_id = PaymentId(payment_hash.0); + let payment_id = event_payment_id.unwrap_or(legacy_id); + + if let Some(info) = self.payment_store.get(&payment_id) { + return (payment_id, Some(info)); + } + + if legacy_id != payment_id { + if let Some(info) = self.payment_store.get(&legacy_id) { + return (legacy_id, Some(info)); + } + } + + (payment_id, None) + } + pub async fn handle_event(&self, event: LdkEvent) -> Result<(), ReplayEvent> { match event { LdkEvent::FundingGenerationReady { @@ -782,6 +801,7 @@ where .lsps2_funding_tx_broadcast_safe(user_channel_id, counterparty_node_id); }, LdkEvent::PaymentClaimable { + payment_id, payment_hash, purpose, amount_msat, @@ -790,8 +810,8 @@ where counterparty_skimmed_fee_msat, .. } => { - let payment_id = PaymentId(payment_hash.0); - let payment_info = self.payment_store.get(&payment_id); + let (payment_id, mut payment_info) = + self.resolve_inbound_payment_id(payment_id, &payment_hash); if let Some(info) = payment_info.as_ref() { if info.direction == PaymentDirection::Outbound { log_info!( @@ -912,144 +932,51 @@ where } } - let should_emit_payment_claimable = if let Some(info) = payment_info.as_ref() { - // If this is known by the store but ChannelManager doesn't know the preimage, - // the payment 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" - ); - true - }, - _ => false, - } - } else if let PaymentPurpose::Bolt11InvoicePayment { - payment_preimage: None, - payment_secret, - .. - } = &purpose - { - if !self.config.manually_handle_unknown_bolt11_payments { - log_info!( + if payment_info.is_none() { + if let PaymentPurpose::Bolt11InvoicePayment { + payment_preimage: None, + payment_secret, + .. + } = &purpose + { + if !self.config.manually_handle_unknown_bolt11_payments { + log_info!( self.logger, - "Refusing unknown BOLT11 payment with hash {} as manual handling is disabled", + "Refusing unknown BOLT11 payment with hash {} of {}msat", hex_utils::to_string(&payment_hash.0), + amount_msat, ); - self.channel_manager.fail_htlc_backwards(&payment_hash); - return Ok(()); - } - - let invoice_amount_msat = - amount_msat.saturating_add(counterparty_skimmed_fee_msat); - let kind = PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - 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(invoice_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()); - }, - } - true - } else { - false - }; - if should_emit_payment_claimable { - 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()); - }, - }; - } + self.channel_manager.fail_htlc_backwards(&payment_hash); + return Ok(()); + } - log_info!( - self.logger, - "Received payment from payment hash {} of {}msat", - hex_utils::to_string(&payment_hash.0), - amount_msat, - ); - let payment_preimage = match purpose { - PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => { - payment_preimage - }, - PaymentPurpose::Bolt12OfferPayment { - payment_preimage, - payment_secret, - payment_context, - .. - } => { - 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 invoice_amount_msat = + amount_msat.saturating_add(counterparty_skimmed_fee_msat); + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + 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), + Some(invoice_amount_msat), None, PaymentDirection::Inbound, PaymentStatus::Pending, ); - match self.payment_store.insert(payment).await { + match self.payment_store.insert(payment.clone()).await { Ok(false) => (), Ok(true) => { log_error!( self.logger, - "Bolt12OfferPayment with ID {} was previously known", + "Bolt11InvoicePayment with ID {} was previously known", payment_id, ); debug_assert!(false); @@ -1061,49 +988,247 @@ where payment_id, e ); - debug_assert!(false); + return Err(ReplayEvent()); }, } + payment_info = Some(payment); + } + } + + let should_insert_payment = payment_info.is_none(); + if let Some(info) = payment_info { + // If this is known by the store but ChannelManager doesn't know the preimage, + // the payment 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", + hex_utils::to_string(&payment_hash.0), + amount_msat, + ); + let payment_preimage = match purpose { + PaymentPurpose::Bolt11InvoicePayment { + payment_preimage, + payment_secret, + .. + } => { + if should_insert_payment { + let invoice_amount_msat = + amount_msat.saturating_add(counterparty_skimmed_fee_msat); + 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(invoice_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()); + }, + } + } + payment_preimage }, - PaymentPurpose::Bolt12RefundPayment { payment_preimage, .. } => { + PaymentPurpose::Bolt12OfferPayment { + payment_preimage, + payment_secret, + payment_context, + .. + } => { + let payer_note = payment_context.invoice_request.payer_note_truncated; + let offer_id = payment_context.offer_id; + let quantity = payment_context.invoice_request.quantity; + if should_insert_payment { + 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 + ); + return Err(ReplayEvent()); + }, + } + } + payment_preimage + }, + PaymentPurpose::Bolt12RefundPayment { + payment_preimage, + payment_secret, + .. + } => { + if should_insert_payment { + 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), - }; + if should_insert_payment { + 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, - ); + 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) @@ -1135,6 +1260,7 @@ where } }, LdkEvent::PaymentClaimed { + payment_id, payment_hash, purpose, amount_msat, @@ -1142,9 +1268,8 @@ where htlcs: _, sender_intended_total_msat: _, onion_fields, - payment_id: _, } => { - let payment_id = PaymentId(payment_hash.0); + let (payment_id, _) = self.resolve_inbound_payment_id(payment_id, &payment_hash); log_info!( self.logger, "Claimed payment with ID {} from payment hash {} of {}msat.", @@ -1153,43 +1278,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) }, }; @@ -1199,11 +1364,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 b22c1538cc..e8c69dec09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1016,6 +1016,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), @@ -1034,6 +1035,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 9ae5ebb051..763705c268 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, @@ -103,63 +106,24 @@ impl Bolt11Payment { &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, ) -> Result { - let invoice = { - let invoice_params = Bolt11InvoiceParameters { - amount_msats: amount_msat, - description: invoice_description.clone(), - invoice_expiry_delta_secs: Some(expiry_secs), - payment_hash: manual_claim_payment_hash, - ..Default::default() - }; - - match self.channel_manager.create_bolt11_invoice(invoice_params) { - Ok(inv) => { - log_info!(self.logger, "Invoice created: {}", inv); - inv - }, - Err(e) => { - log_error!(self.logger, "Failed to create invoice: {}", e); - return Err(Error::InvoiceCreationFailed); - }, - } + let invoice_params = Bolt11InvoiceParameters { + amount_msats: amount_msat, + description: invoice_description.clone(), + invoice_expiry_delta_secs: Some(expiry_secs), + payment_hash: manual_claim_payment_hash, + ..Default::default() }; - if manual_claim_payment_hash.is_some() { - return Ok(invoice); + match self.channel_manager.create_bolt11_invoice(invoice_params) { + Ok(inv) => { + log_info!(self.logger, "Invoice created: {}", inv); + Ok(inv) + }, + Err(e) => { + log_error!(self.logger, "Failed to create invoice: {}", e); + Err(Error::InvoiceCreationFailed) + }, } - - let payment_hash = invoice.payment_hash(); - let payment_secret = invoice.payment_secret(); - let id = PaymentId(payment_hash.0); - // 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 preimage = self - .channel_manager - .get_payment_preimage_decrypt_metadata( - payment_hash, - payment_secret.clone(), - payment_metadata.as_deref_mut(), - ) - .ok(); - debug_assert!(preimage.is_some(), "We just let ChannelManager create an inbound payment, it can't have forgotten the preimage by now."); - 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) } fn receive_via_jit_channel_inner( @@ -167,9 +131,8 @@ impl Bolt11Payment { expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result { - let should_register_payment = payment_hash.is_none(); let connection_manager = Arc::clone(&self.connection_manager); - let (invoice, chosen_lsp) = self.runtime.block_on(async move { + let res = self.runtime.block_on(async move { if let Some(amount_msat) = amount_msat { self.liquidity_source .lsps2_client() @@ -194,38 +157,8 @@ impl Bolt11Payment { ) .await } - })?; - - if should_register_payment { - // 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))?; - } + }); + let (invoice, chosen_lsp) = res?; // 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 }; @@ -277,15 +210,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(); @@ -476,10 +401,10 @@ impl Bolt11Payment { ) } - /// Allows to attempt manually claiming payments with the given preimage that have previously - /// been registered via [`receive_for_hash`] or [`receive_variable_amount_for_hash`]. + /// Allows to attempt manually claiming a payment with the given preimage after a + /// [`PaymentClaimable`] event has been emitted. /// - /// This should be called in reponse to a [`PaymentClaimable`] event as soon as the preimage is + /// This should be called in response to a [`PaymentClaimable`] event as soon as the preimage is /// available. /// /// Will check that the payment is known, and that the given preimage and claimable amount @@ -488,17 +413,33 @@ impl Bolt11Payment { /// /// When claiming the payment has succeeded, a [`PaymentReceived`] event will be emitted. /// - /// [`receive_for_hash`]: Self::receive_for_hash - /// [`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, @@ -508,54 +449,60 @@ 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 - /// been registered via [`receive_for_hash`] or [`receive_variable_amount_for_hash`]. + /// Allows to manually fail a payment after a [`PaymentClaimable`] event has been emitted. /// - /// This should be called in reponse to a [`PaymentClaimable`] event if the payment needs to be + /// This should be called in response to a [`PaymentClaimable`] event if the payment needs to be /// failed back, e.g., if the correct preimage can't be retrieved in time before the claim /// deadline has been reached. /// /// Will check that the payment is known before failing the payment, and will return an error /// otherwise. /// - /// [`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), @@ -567,10 +514,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!( @@ -602,22 +549,23 @@ impl Bolt11Payment { /// Returns a payable invoice that can be used to request a payment of the amount /// given for the given payment hash. /// - /// If [`Config::manually_handle_unknown_bolt11_payments`] is enabled, a - /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the - /// inbound payment will be failed back when it arrives. + /// The inbound payment will only be accepted if + /// [`Config::manually_handle_unknown_bolt11_payments`] is enabled. We will then emit a + /// [`PaymentClaimable`] event once the inbound payment arrives. /// - /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a - /// payment hash is unsafe and can lead to loss of funds. + /// **Warning:** it is the user's responsibility to never reuse the same payment hash. + /// Reusing a payment hash is unsafe and can lead to loss of funds. We do not detect or reject + /// duplicates for this API. /// - /// **Note:** if manual handling is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* - /// immediately fail the payment via [`fail_for_hash`]. + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`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_id`]. /// - /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - /// [`claim_for_hash`]: Self::claim_for_hash - /// [`fail_for_hash`]: Self::fail_for_hash + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments + /// [`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, @@ -643,22 +591,23 @@ impl Bolt11Payment { /// Returns a payable invoice that can be used to request a payment for the given payment hash /// and the amount to be determined by the user, also known as a "zero-amount" invoice. /// - /// If [`Config::manually_handle_unknown_bolt11_payments`] is enabled, a - /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the - /// inbound payment will be failed back when it arrives. + /// The inbound payment will only be accepted if + /// [`Config::manually_handle_unknown_bolt11_payments`] is enabled. We will then emit a + /// [`PaymentClaimable`] event once the inbound payment arrives. /// - /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a - /// payment hash is unsafe and can lead to loss of funds. + /// **Warning:** it is the user's responsibility to never reuse the same payment hash. + /// Reusing a payment hash is unsafe and can lead to loss of funds. We do not detect or reject + /// duplicates for this API. /// - /// **Note:** if manual handling is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* - /// immediately fail the payment via [`fail_for_hash`]. + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`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_id`]. /// - /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - /// [`claim_for_hash`]: Self::claim_for_hash - /// [`fail_for_hash`]: Self::fail_for_hash + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments + /// [`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 { @@ -702,25 +651,25 @@ impl Bolt11Payment { /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the /// channel to us. We'll use its cheapest offer otherwise. /// - /// If [`Config::manually_handle_unknown_bolt11_payments`] is enabled, a - /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the - /// inbound payment will be failed back when it arrives. The check that - /// [`counterparty_skimmed_fee_msat`] is within the limits is performed *before* emitting the - /// event. + /// The inbound payment will only be accepted if + /// [`Config::manually_handle_unknown_bolt11_payments`] is enabled. We will then emit a + /// [`PaymentClaimable`] event once the inbound payment arrives. The check that + /// [`counterparty_skimmed_fee_msat`] is within the limits is performed *before* emitting the event. /// - /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a - /// payment hash is unsafe and can lead to loss of funds. + /// **Warning:** it is the user's responsibility to never reuse the same payment hash. + /// Reusing a payment hash is unsafe and can lead to loss of funds. We do not detect or reject + /// duplicates for this API. /// - /// **Note:** if manual handling is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* - /// immediately fail the payment via [`fail_for_hash`]. + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`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_id`]. /// /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - /// [`claim_for_hash`]: Self::claim_for_hash - /// [`fail_for_hash`]: Self::fail_for_hash + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments + /// [`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, @@ -775,25 +724,25 @@ impl Bolt11Payment { /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. /// We'll use its cheapest offer otherwise. /// - /// If [`Config::manually_handle_unknown_bolt11_payments`] is enabled, a - /// [`PaymentClaimable`] event will be emitted once the inbound payment arrives. Otherwise, the - /// inbound payment will be failed back when it arrives. The check that - /// [`counterparty_skimmed_fee_msat`] is within the limits is performed *before* emitting the - /// event. + /// The inbound payment will only be accepted if + /// [`Config::manually_handle_unknown_bolt11_payments`] is enabled. We will then emit a + /// [`PaymentClaimable`] event once the inbound payment arrives. The check that + /// [`counterparty_skimmed_fee_msat`] is within the limits is performed *before* emitting the event. /// - /// **Warning:** it is the user's responsibility to never reuse the same payment hash. Reusing a - /// payment hash is unsafe and can lead to loss of funds. + /// **Warning:** it is the user's responsibility to never reuse the same payment hash. + /// Reusing a payment hash is unsafe and can lead to loss of funds. We do not detect or reject + /// duplicates for this API. /// - /// **Note:** if manual handling is enabled, 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 payment hash. If they're unable to obtain the preimage, they *MUST* - /// immediately fail the payment via [`fail_for_hash`]. + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`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_id`]. /// /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments /// [`PaymentClaimable`]: crate::Event::PaymentClaimable - /// [`claim_for_hash`]: Self::claim_for_hash - /// [`fail_for_hash`]: Self::fail_for_hash + /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments + /// [`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 475377e62c..2e76484c8f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -241,6 +241,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), @@ -1241,10 +1271,9 @@ pub(crate) async fn do_channel_full_cycle( .unwrap(); println!("\nA send"); - let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); - assert_eq!(node_a.bolt11_payment().send(&invoice, None), Err(NodeError::DuplicatePayment)); + let outbound_payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); - 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 { .. }) @@ -1264,7 +1293,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 { @@ -1277,24 +1306,17 @@ 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 { .. })); - - // 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)); + let inbound_payment_id = expect_payment_received_event!(node_b, invoice_amount_1_msat); + let outbound_payment = node_a.payment(&outbound_payment_id).unwrap(); + assert_eq!(outbound_payment.status, PaymentStatus::Succeeded); + assert_eq!(outbound_payment.direction, PaymentDirection::Outbound); + assert_eq!(outbound_payment.amount_msat, Some(invoice_amount_1_msat)); + assert!(matches!(&outbound_payment.kind, PaymentKind::Bolt11 { .. })); + let inbound_payment = node_b.payment(&inbound_payment_id).unwrap(); + assert_eq!(inbound_payment.status, PaymentStatus::Succeeded); + assert_eq!(inbound_payment.direction, PaymentDirection::Inbound); + assert_eq!(inbound_payment.amount_msat, Some(invoice_amount_1_msat)); + assert!(matches!(&inbound_payment.kind, PaymentKind::Bolt11 { .. })); // Test under-/overpayment let invoice_amount_2_msat = 2500_000; @@ -1317,28 +1339,30 @@ 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 { .. })); + let outbound_payment = node_a.payment(&outbound_payment_id).unwrap(); + assert_eq!(outbound_payment.status, PaymentStatus::Succeeded); + assert_eq!(outbound_payment.direction, PaymentDirection::Outbound); + assert_eq!(outbound_payment.amount_msat, Some(overpaid_amount_msat)); + assert!(matches!(&outbound_payment.kind, PaymentKind::Bolt11 { .. })); + let inbound_payment = node_b.payment(&inbound_payment_id).unwrap(); + assert_eq!(inbound_payment.status, PaymentStatus::Succeeded); + assert_eq!(inbound_payment.direction, PaymentDirection::Inbound); + assert_eq!(inbound_payment.amount_msat, Some(overpaid_amount_msat)); + assert!(matches!(&inbound_payment.kind, PaymentKind::Bolt11 { .. })); // Test "zero-amount" invoice payment println!("\nB receive_variable_amount_payment"); @@ -1352,31 +1376,33 @@ 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 { .. })); + let outbound_payment = node_a.payment(&outbound_payment_id).unwrap(); + assert_eq!(outbound_payment.status, PaymentStatus::Succeeded); + assert_eq!(outbound_payment.direction, PaymentDirection::Outbound); + assert_eq!(outbound_payment.amount_msat, Some(determined_amount_msat)); + assert!(matches!(&outbound_payment.kind, PaymentKind::Bolt11 { .. })); + let inbound_payment = node_b.payment(&inbound_payment_id).unwrap(); + assert_eq!(inbound_payment.status, PaymentStatus::Succeeded); + assert_eq!(inbound_payment.direction, PaymentDirection::Inbound); + assert_eq!(inbound_payment.amount_msat, Some(determined_amount_msat)); + assert!(matches!(&inbound_payment.kind, PaymentKind::Bolt11 { .. })); // Test claiming manually registered payments. let invoice_amount_3_msat = 5_532_000; @@ -1391,34 +1417,28 @@ 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); + assert_ne!(manual_payment_id.0, manual_payment_hash.0); 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); - assert_eq!( - node_a.payment(&manual_payment_id).unwrap().amount_msat, - Some(invoice_amount_3_msat) - ); - assert!(matches!(node_a.payment(&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!( - node_b.payment(&manual_payment_id).unwrap().amount_msat, - Some(invoice_amount_3_msat) - ); - assert!(matches!(node_b.payment(&manual_payment_id).unwrap().kind, PaymentKind::Bolt11 { .. })); + 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); + let outbound_manual_payment = node_a.payment(&outbound_manual_payment_id).unwrap(); + assert_eq!(outbound_manual_payment.status, PaymentStatus::Succeeded); + assert_eq!(outbound_manual_payment.direction, PaymentDirection::Outbound); + assert_eq!(outbound_manual_payment.amount_msat, Some(invoice_amount_3_msat)); + assert!(matches!(&outbound_manual_payment.kind, PaymentKind::Bolt11 { .. })); + let manual_payment = node_b.payment(&manual_payment_id).unwrap(); + assert_eq!(manual_payment.status, PaymentStatus::Succeeded); + assert_eq!(manual_payment.direction, PaymentDirection::Inbound); + assert_eq!(manual_payment.amount_msat, Some(invoice_amount_3_msat)); + assert!(matches!(&manual_payment.kind, PaymentKind::Bolt11 { .. })); // Test failing manually registered payments. let invoice_amount_4_msat = 5_532_000; @@ -1434,42 +1454,24 @@ 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); + 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!(node_a.payment(&manual_fail_payment_id).unwrap().status, PaymentStatus::Failed); - assert_eq!( - node_a.payment(&manual_fail_payment_id).unwrap().direction, - PaymentDirection::Outbound - ); - assert_eq!( - node_a.payment(&manual_fail_payment_id).unwrap().amount_msat, - Some(invoice_amount_4_msat) - ); - assert!(matches!( - node_a.payment(&manual_fail_payment_id).unwrap().kind, - PaymentKind::Bolt11 { .. } - )); - assert_eq!(node_b.payment(&manual_fail_payment_id).unwrap().status, PaymentStatus::Failed); - assert_eq!( - node_b.payment(&manual_fail_payment_id).unwrap().direction, - PaymentDirection::Inbound - ); - assert_eq!( - node_b.payment(&manual_fail_payment_id).unwrap().amount_msat, - Some(invoice_amount_4_msat) - ); - assert!(matches!( - node_b.payment(&manual_fail_payment_id).unwrap().kind, - PaymentKind::Bolt11 { .. } - )); + let outbound_manual_fail_payment = node_a.payment(&outbound_manual_fail_payment_id).unwrap(); + assert_eq!(outbound_manual_fail_payment.status, PaymentStatus::Failed); + assert_eq!(outbound_manual_fail_payment.direction, PaymentDirection::Outbound); + assert_eq!(outbound_manual_fail_payment.amount_msat, Some(invoice_amount_4_msat)); + assert!(matches!(&outbound_manual_fail_payment.kind, PaymentKind::Bolt11 { .. })); + let manual_fail_payment = node_b.payment(&manual_fail_payment_id).unwrap(); + assert_eq!(manual_fail_payment.status, PaymentStatus::Failed); + assert_eq!(manual_fail_payment.direction, PaymentDirection::Inbound); + assert_eq!(manual_fail_payment.amount_msat, Some(invoice_amount_4_msat)); + assert!(matches!(&manual_fail_payment.kind, PaymentKind::Bolt11 { .. })); // Test spontaneous/keysend payments println!("\nA send_spontaneous_payment"); @@ -1481,39 +1483,38 @@ 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); - assert_eq!(node_a.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat)); - assert!(matches!( - node_a.payment(&keysend_payment_id).unwrap().kind, - PaymentKind::Spontaneous { .. } - )); + let keysend_payment = node_a.payment(&keysend_payment_id).unwrap(); + assert_eq!(keysend_payment.status, PaymentStatus::Succeeded); + assert_eq!(keysend_payment.direction, PaymentDirection::Outbound); + assert_eq!(keysend_payment.amount_msat, Some(keysend_amount_msat)); + assert!(matches!(&keysend_payment.kind, 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!(matches!( - node_b.payment(&keysend_payment_id).unwrap().kind, - PaymentKind::Spontaneous { .. } - )); + let received_keysend_payment = node_b.payment(&received_keysend_payment_id).unwrap(); + assert_eq!(received_keysend_payment.status, PaymentStatus::Succeeded); + assert_eq!(received_keysend_payment.direction, PaymentDirection::Inbound); + assert_eq!(received_keysend_payment.amount_msat, Some(keysend_amount_msat)); + assert!(matches!(&received_keysend_payment.kind, PaymentKind::Spontaneous { .. })); assert_eq!( node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt11 { .. })).len(), 5 ); 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 02ecbfd7c0..1f32c68b24 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -495,7 +495,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); @@ -504,9 +504,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)] @@ -585,7 +585,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); @@ -2393,7 +2392,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, @@ -2409,7 +2408,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"); @@ -2417,8 +2415,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 }); @@ -2450,8 +2447,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| { @@ -2488,7 +2485,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 }); @@ -2955,7 +2951,8 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let service_node_id = service_node.node_id(); let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - let client_config = random_config(); + let mut client_config = random_config(); + client_config.node_config.manually_handle_unknown_bolt11_payments = true; setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.add_liquidity_source(service_node_id, service_addr, None, true); @@ -3007,7 +3004,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); @@ -3016,7 +3013,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(); @@ -3050,7 +3047,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. @@ -3070,7 +3067,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()); @@ -3078,22 +3075,24 @@ 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 ); + assert_ne!(client_payment_id.0, manual_payment_hash.0); + assert_eq!(client_node.payment(&client_payment_id).unwrap().amount_msat, Some(jit_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, .. } => { @@ -3103,7 +3102,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. @@ -3123,7 +3122,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()); @@ -3131,17 +3130,17 @@ 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 ); + assert_ne!(client_payment_id.0, manual_payment_hash.0); 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)] @@ -3273,7 +3272,8 @@ async fn lsps2_client_trusts_lsp() { let service_node_id = service_node.node_id(); let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - let client_config = random_config(); + let mut client_config = random_config(); + client_config.node_config.manually_handle_unknown_bolt11_payments = true; setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.add_liquidity_source(service_node_id, service_addr.clone(), None, true); @@ -3333,8 +3333,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()); @@ -3372,21 +3372,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; @@ -3449,7 +3450,8 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { let service_node_id = service_node.node_id(); let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - let client_config = random_config(); + let mut client_config = random_config(); + client_config.node_config.manually_handle_unknown_bolt11_payments = true; setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.add_liquidity_source(service_node_id, service_addr.clone(), None, true); From 7d4b84e755cc67ed9439a703b26d9088233338e0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 29 Jul 2026 10:46:38 +0200 Subject: [PATCH 5/7] f Restore BOLT11 duplicate payments Keep outbound BOLT11 payments keyed by payment hash so repeated attempts to pay the same invoice can still return DuplicatePayment. Co-Authored-By: HAL 9000 --- src/lib.rs | 2 -- src/payment/bolt11.rs | 17 +++++++++++------ tests/common/mod.rs | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e8c69dec09..b22c1538cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1016,7 +1016,6 @@ 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), @@ -1035,7 +1034,6 @@ 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 763705c268..e33dd3950f 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -19,7 +19,6 @@ 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, }; @@ -38,7 +37,7 @@ use crate::payment::store::{ }; use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, KeysManager, PaymentStore}; +use crate::types::{ChannelManager, PaymentStore}; #[cfg(not(feature = "uniffi"))] type Bolt11Invoice = LdkBolt11Invoice; @@ -70,7 +69,6 @@ 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, @@ -83,7 +81,7 @@ pub struct Bolt11Payment { impl Bolt11Payment { pub(crate) fn new( runtime: Arc, channel_manager: Arc, - keys_manager: Arc, connection_manager: Arc>>, + connection_manager: Arc>>, liquidity_source: Arc>>, payment_store: Arc, peer_store: Arc>>, config: Arc, is_running: Arc>, logger: Arc, @@ -91,7 +89,6 @@ impl Bolt11Payment { Self { runtime, channel_manager, - keys_manager, connection_manager, liquidity_source, payment_store, @@ -210,7 +207,15 @@ impl Bolt11Payment { } let payment_hash = invoice.payment_hash(); - let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes()); + 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 route_params_config = route_parameters.or(self.config.route_parameters).unwrap_or_default(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2e76484c8f..e393bca8c4 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1272,6 +1272,7 @@ pub(crate) async fn do_channel_full_cycle( println!("\nA send"); let outbound_payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); + assert_eq!(Err(NodeError::DuplicatePayment), node_a.bolt11_payment().send(&invoice, None)); assert!(!node_a.list_payments_with_filter(|p| p.id == outbound_payment_id).is_empty()); From 6cc849fc5b46924be7110d1c67f8d6e97340dbf8 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 29 Jul 2026 11:59:28 +0200 Subject: [PATCH 6/7] f Document legacy for-hash events Clarify that v0.7-or-earlier for-hash invoices can still emit PaymentClaimable after upgrade regardless of the new config flag. Co-Authored-By: HAL 9000 --- src/event.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/event.rs b/src/event.rs index 8d32ef1c1a..606e6be849 100644 --- a/src/event.rs +++ b/src/event.rs @@ -208,6 +208,9 @@ pub enum Event { /// holder of a valid invoice generated by this node tie up inbound HTLC slots and grow the /// persisted event queue until the payment is claimed, failed, or times out. /// + /// Any `_for_hash` invoices created with LDK Node v0.7 or prior can still emit this event + /// after upgrade and must be claimed or failed back regardless of the config setting. + /// /// [`claim_for_id`]: crate::payment::Bolt11Payment::claim_for_id /// [`fail_for_id`]: crate::payment::Bolt11Payment::fail_for_id /// [`Config::manually_handle_unknown_bolt11_payments`]: crate::config::Config::manually_handle_unknown_bolt11_payments From 97b27d4ea1e488f824f4cbd991cddc303a01f97a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 29 Jul 2026 11:07:11 +0200 Subject: [PATCH 7/7] Test v0.7 manual claim upgrades Cover manually claiming and failing BOLT11 for-hash invoices that were created before upgrading, so the new ID-based APIs keep handling legacy payment-hash IDs. Co-Authored-By: HAL 9000 --- Cargo.toml | 1 + tests/upgrade_downgrade_tests.rs | 275 +++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 7fc945833e..ee7bb7ddf7 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ regex = "1.5.6" criterion = { version = "0.7.0", features = ["async_tokio"] } ldk-node-062 = { package = "ldk-node", version = "=0.6.2" } ldk-node-070 = { package = "ldk-node", version = "=0.7.0" } +lightning-types-0-3 = { package = "lightning-types", version = "0.3.2" } [target.'cfg(not(no_download))'.dev-dependencies] electrsd = { version = "0.36.1", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_29_0"] } diff --git a/tests/upgrade_downgrade_tests.rs b/tests/upgrade_downgrade_tests.rs index 1b9a92a1a3..c275f36c0c 100644 --- a/tests/upgrade_downgrade_tests.rs +++ b/tests/upgrade_downgrade_tests.rs @@ -13,6 +13,281 @@ // // TODO(@benthecarman) Bring back after 0.8 is cut. +#![cfg(not(feature = "uniffi"))] + +mod common; + +use std::str::FromStr; +use std::time::Duration; + +use crate::common::{ + expect_channel_pending_event, expect_channel_ready_event, expect_event, + expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, + generate_blocks_and_wait, generate_listening_addresses, premine_and_distribute_funds, + random_config, random_storage_path, setup_bitcoind_and_electrsd, setup_node, wait_for_tx, + TestChainSource, +}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; +use bitcoin::Amount; +use ldk_node::config::Config; +use ldk_node::entropy::NodeEntropy; +use ldk_node::lightning::ln::channelmanager::PaymentId; +use ldk_node::lightning::ln::msgs::SocketAddress as CurrentSocketAddress; +use ldk_node::lightning_invoice::Bolt11Invoice as CurrentBolt11Invoice; +use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; +use ldk_node::{Builder, Event, Node}; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; +use lightning_types_0_3::payment::PaymentHash as OldPaymentHash; + +const RECEIVER_SEED_BYTES: [u8; 64] = [43; 64]; +const CHANNEL_AMOUNT_SAT: u64 = 500_000; +const CLAIM_AMOUNT_MSAT: u64 = 100_000; +const FAIL_AMOUNT_MSAT: u64 = 200_000; + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn v0_7_for_hash_payments_can_be_manually_resolved_after_upgrade() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + let chain_source = TestChainSource::Esplora(&electrsd); + + let payer_config = random_config(); + let payer = setup_node(&chain_source, payer_config); + + let receiver_storage_path = random_storage_path().to_str().unwrap().to_owned(); + let receiver_addresses = generate_listening_addresses(); + let old_receiver_addresses = to_v0_7_socket_addresses(&receiver_addresses); + let old_receiver = + build_v0_7_receiver(receiver_storage_path.clone(), old_receiver_addresses, &esplora_url); + + let payer_addr = payer.onchain_payment().new_address().unwrap(); + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![payer_addr], + Amount::from_sat(1_000_000), + ) + .await; + payer.sync_wallets().unwrap(); + old_receiver.sync_wallets().unwrap(); + + let old_receiver_addr = to_current_socket_address( + old_receiver.listening_addresses().unwrap().first().unwrap().clone(), + ); + payer + .open_channel( + old_receiver.node_id(), + old_receiver_addr.clone(), + CHANNEL_AMOUNT_SAT, + None, + None, + ) + .unwrap(); + + let funding_txo_a = expect_channel_pending_event!(payer, old_receiver.node_id()); + let funding_txo_b = expect_v0_7_channel_pending(&old_receiver, payer.node_id()).await; + assert_eq!(funding_txo_a, funding_txo_b); + wait_for_tx(&electrsd.client, funding_txo_a.txid).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + payer.sync_wallets().unwrap(); + old_receiver.sync_wallets().unwrap(); + expect_channel_ready_event!(payer, old_receiver.node_id()); + expect_v0_7_channel_ready(&old_receiver, payer.node_id()).await; + wait_for_current_channel_usable(&payer, old_receiver.node_id(), CLAIM_AMOUNT_MSAT).await; + + let invoice_description = ldk_node_070::lightning_invoice::Bolt11InvoiceDescription::Direct( + ldk_node_070::lightning_invoice::Description::new("upgrade manual".to_owned()).unwrap(), + ); + let claim_preimage = [42u8; 32]; + let claim_hash = payment_hash_from_preimage(claim_preimage); + let claim_invoice = old_receiver + .bolt11_payment() + .receive_for_hash( + CLAIM_AMOUNT_MSAT, + &invoice_description, + 3600, + OldPaymentHash(claim_hash.0), + ) + .unwrap() + .to_string(); + + let fail_preimage = [43u8; 32]; + let fail_hash = payment_hash_from_preimage(fail_preimage); + let fail_invoice = old_receiver + .bolt11_payment() + .receive_for_hash(FAIL_AMOUNT_MSAT, &invoice_description, 3600, OldPaymentHash(fail_hash.0)) + .unwrap() + .to_string(); + + old_receiver.stop().unwrap(); + + let receiver = build_current_receiver(receiver_storage_path, receiver_addresses, &esplora_url); + assert_eq!(receiver.node_id(), old_receiver.node_id()); + assert_legacy_manual_payment(&receiver, claim_hash, CLAIM_AMOUNT_MSAT); + assert_legacy_manual_payment(&receiver, fail_hash, FAIL_AMOUNT_MSAT); + + payer.connect(receiver.node_id(), old_receiver_addr, true).unwrap(); + wait_for_current_channel_usable(&payer, receiver.node_id(), CLAIM_AMOUNT_MSAT).await; + + let claim_invoice = CurrentBolt11Invoice::from_str(&claim_invoice).unwrap(); + let payer_claim_id = payer.bolt11_payment().send(&claim_invoice, None).unwrap(); + let (claim_payment_id, claimable_amount_msat) = + expect_payment_claimable_event!(receiver, claim_hash, CLAIM_AMOUNT_MSAT); + assert_eq!(claim_payment_id, PaymentId(claim_hash.0)); + receiver + .bolt11_payment() + .claim_for_id(claim_payment_id, claimable_amount_msat, PaymentPreimage(claim_preimage)) + .unwrap(); + let received_payment_id = expect_payment_received_event!(receiver, CLAIM_AMOUNT_MSAT); + assert_eq!(received_payment_id, claim_payment_id); + expect_payment_successful_event!(payer, payer_claim_id, None); + assert_eq!(receiver.payment(&claim_payment_id).unwrap().status, PaymentStatus::Succeeded); + + let fail_invoice = CurrentBolt11Invoice::from_str(&fail_invoice).unwrap(); + let payer_fail_id = payer.bolt11_payment().send(&fail_invoice, None).unwrap(); + let (fail_payment_id, _) = + expect_payment_claimable_event!(receiver, fail_hash, FAIL_AMOUNT_MSAT); + assert_eq!(fail_payment_id, PaymentId(fail_hash.0)); + receiver.bolt11_payment().fail_for_id(fail_payment_id).unwrap(); + expect_event!(payer, PaymentFailed); + assert_eq!(payer.payment(&payer_fail_id).unwrap().status, PaymentStatus::Failed); + assert_eq!(receiver.payment(&fail_payment_id).unwrap().status, PaymentStatus::Failed); + + payer.stop().unwrap(); + receiver.stop().unwrap(); +} + +fn build_v0_7_receiver( + storage_path: String, + listening_addresses: Vec, esplora_url: &str, +) -> ldk_node_070::Node { + let mut config = ldk_node_070::config::Config::default(); + config.network = bitcoin::Network::Regtest; + config.storage_dir_path = storage_path; + config.anchor_channels_config = None; + + let mut builder = ldk_node_070::Builder::from_config(config); + builder.set_entropy_seed_bytes(RECEIVER_SEED_BYTES); + builder.set_listening_addresses(listening_addresses).unwrap(); + builder.set_node_alias("upgrade-receiver".to_string()).unwrap(); + builder.set_chain_source_esplora(esplora_url.to_owned(), None); + + let node = builder.build().unwrap(); + node.start().unwrap(); + node +} + +fn build_current_receiver( + storage_path: String, listening_addresses: Vec, esplora_url: &str, +) -> Node { + let mut config = Config::default(); + config.network = bitcoin::Network::Regtest; + config.storage_dir_path = storage_path; + config.listening_addresses = Some(listening_addresses); + config.node_alias = common::random_node_alias(); + + let mut builder = Builder::from_config(config); + builder.set_chain_source_esplora(esplora_url.to_owned(), None); + let node = builder.build(NodeEntropy::from_seed_bytes(RECEIVER_SEED_BYTES)).unwrap(); + node.start().unwrap(); + node +} + +fn payment_hash_from_preimage(preimage: [u8; 32]) -> PaymentHash { + PaymentHash(Sha256::hash(&preimage).to_byte_array()) +} + +fn assert_legacy_manual_payment(node: &Node, payment_hash: PaymentHash, amount_msat: u64) { + let payment_id = PaymentId(payment_hash.0); + let payment = node.payment(&payment_id).unwrap(); + assert_eq!(payment.id, payment_id); + assert_eq!(payment.amount_msat, Some(amount_msat)); + assert_eq!(payment.direction, PaymentDirection::Inbound); + assert_eq!(payment.status, PaymentStatus::Pending); + assert!(matches!(payment.kind, PaymentKind::Bolt11 { preimage: None, .. })); +} + +fn to_current_socket_address( + address: ldk_node_070::lightning::ln::msgs::SocketAddress, +) -> CurrentSocketAddress { + match address { + ldk_node_070::lightning::ln::msgs::SocketAddress::TcpIpV4 { addr, port } => { + CurrentSocketAddress::TcpIpV4 { addr, port } + }, + _ => panic!("unexpected non-IPv4 test address: {:?}", address), + } +} + +fn to_v0_7_socket_addresses( + addresses: &[CurrentSocketAddress], +) -> Vec { + addresses + .iter() + .map(|address| match address { + CurrentSocketAddress::TcpIpV4 { addr, port } => { + ldk_node_070::lightning::ln::msgs::SocketAddress::TcpIpV4 { + addr: *addr, + port: *port, + } + }, + _ => panic!("unexpected non-IPv4 test address: {:?}", address), + }) + .collect() +} + +async fn expect_v0_7_channel_pending( + node: &ldk_node_070::Node, expected_counterparty: PublicKey, +) -> bitcoin::OutPoint { + match next_v0_7_event(node).await { + ldk_node_070::Event::ChannelPending { counterparty_node_id, funding_txo, .. } => { + assert_eq!(counterparty_node_id, expected_counterparty); + node.event_handled().unwrap(); + funding_txo + }, + event => panic!("{} got unexpected event: {:?}", node.node_id(), event), + } +} + +async fn expect_v0_7_channel_ready(node: &ldk_node_070::Node, expected_counterparty: PublicKey) { + match next_v0_7_event(node).await { + ldk_node_070::Event::ChannelReady { counterparty_node_id, .. } => { + assert_eq!(counterparty_node_id, Some(expected_counterparty)); + node.event_handled().unwrap(); + }, + event => panic!("{} got unexpected event: {:?}", node.node_id(), event), + } +} + +async fn next_v0_7_event(node: &ldk_node_070::Node) -> ldk_node_070::Event { + tokio::time::timeout(Duration::from_secs(common::INTEROP_TIMEOUT_SECS), node.next_event_async()) + .await + .unwrap_or_else(|_| panic!("{} timed out waiting for event", node.node_id())) +} + +async fn wait_for_current_channel_usable( + node: &Node, counterparty_node_id: PublicKey, min_outbound_amount_msat: u64, +) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(180); + while tokio::time::Instant::now() < deadline { + let is_usable = node.list_channels().iter().any(|c| { + c.counterparty.node_id == counterparty_node_id + && c.is_usable + && c.next_outbound_htlc_limit_msat >= min_outbound_amount_msat + }); + if is_usable { + return; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + panic!( + "channel from {} to {} not ready to send {} msat within 180s", + node.node_id(), + counterparty_node_id, + min_outbound_amount_msat + ); +} + // To keep monitoring whether the serialized node/channel/payment state remains // understandable by v0.7.0, these tests intentionally write current state through // the legacy v1 filesystem-store implementation via `build_with_store`, then