diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 068269997..5007f3c64 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -279,20 +279,21 @@ mod tests { } } -#[cfg_attr(feature = "uniffi", uniffi::export)] impl Bolt11Payment { - /// Send a payment given an invoice. - /// - /// If `route_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. - pub fn send( - &self, invoice: &Bolt11Invoice, route_parameters: Option, - ) -> Result { + fn ensure_running(&self) -> Result<(), Error> { if !*self.is_running.read().expect("lock") { return Err(Error::NotRunning); } + Ok(()) + } + + fn send_internal( + &self, invoice: &LdkBolt11Invoice, amount_msat: Option, + route_parameters: Option, + declared_total_mpp_value_msat_override: Option, invalid_amount_log: &'static str, + ) -> Result { + self.ensure_running()?; - let invoice = maybe_deref(invoice); let payment_hash = invoice.payment_hash(); let payment_id = PaymentId(invoice.payment_hash().0); if let Some(payment) = self.payment_store.get(&payment_id) { @@ -308,23 +309,30 @@ impl Bolt11Payment { route_parameters.or(self.config.route_parameters).unwrap_or_default(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); let payment_secret = Some(*invoice.payment_secret()); + let payment_amount_msat = amount_msat.or_else(|| invoice.amount_milli_satoshis()); let optional_params = OptionalBolt11PaymentParams { retry_strategy, route_params_config, + declared_total_mpp_value_msat_override, ..Default::default() }; match self.channel_manager.pay_for_bolt11_invoice( invoice, payment_id, - None, + amount_msat, optional_params, ) { Ok(()) => { let payee_pubkey = invoice.recover_payee_pub_key(); - let amt_msat = - invoice.amount_milli_satoshis().expect("invoice amount should be set"); - log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); + let payment_amount_msat = + payment_amount_msat.expect("payment amount should be set"); + log_info!( + self.logger, + "Initiated sending {} msat to {}", + payment_amount_msat, + payee_pubkey + ); let kind = PaymentKind::Bolt11 { hash: payment_hash, @@ -335,7 +343,7 @@ impl Bolt11Payment { let payment = PaymentDetails::new( payment_id, kind, - invoice.amount_milli_satoshis(), + Some(payment_amount_msat), None, PaymentDirection::Outbound, PaymentStatus::Pending, @@ -346,9 +354,7 @@ impl Bolt11Payment { Ok(payment_id) }, Err(Bolt11PaymentError::InvalidAmount) => { - log_error!(self.logger, - "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead." - ); + log_error!(self.logger, "{}", invalid_amount_log); return Err(Error::InvalidInvoice); }, Err(Bolt11PaymentError::SendingFailed(e)) => { @@ -365,7 +371,7 @@ impl Bolt11Payment { let payment = PaymentDetails::new( payment_id, kind, - invoice.amount_milli_satoshis(), + payment_amount_msat, None, PaymentDirection::Outbound, PaymentStatus::Failed, @@ -378,6 +384,28 @@ impl Bolt11Payment { }, } } +} + +#[cfg_attr(feature = "uniffi", uniffi::export)] +impl Bolt11Payment { + /// Send a payment given an invoice. + /// + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. + pub fn send( + &self, invoice: &Bolt11Invoice, route_parameters: Option, + ) -> Result { + self.ensure_running()?; + + let invoice = maybe_deref(invoice); + self.send_internal( + invoice, + None, + route_parameters, + None, + "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead.", + ) + } /// Send a payment given an invoice and an amount in millisatoshis. /// @@ -392,9 +420,7 @@ impl Bolt11Payment { &self, invoice: &Bolt11Invoice, amount_msat: u64, route_parameters: Option, ) -> Result { - if !*self.is_running.read().expect("lock") { - return Err(Error::NotRunning); - } + self.ensure_running()?; let invoice = maybe_deref(invoice); if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { @@ -406,94 +432,56 @@ 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 route_params_config = - route_parameters.or(self.config.route_parameters).unwrap_or_default(); - let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let payment_secret = Some(*invoice.payment_secret()); - - let optional_params = OptionalBolt11PaymentParams { - retry_strategy, - route_params_config, - ..Default::default() - }; - match self.channel_manager.pay_for_bolt11_invoice( + self.send_internal( invoice, - payment_id, Some(amount_msat), - optional_params, - ) { - Ok(()) => { - let payee_pubkey = invoice.recover_payee_pub_key(); - log_info!( - self.logger, - "Initiated sending {} msat to {}", - amount_msat, - payee_pubkey - ); - - let kind = PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: payment_secret, - counterparty_skimmed_fee_msat: None, - }; + route_parameters, + None, + "Failed to send payment due to amount given being insufficient.", + ) + } - let payment = PaymentDetails::new( - payment_id, - kind, - Some(amount_msat), - None, - PaymentDirection::Outbound, - PaymentStatus::Pending, - ); - self.runtime.block_on(self.payment_store.insert(payment))?; + /// Send a payment given an invoice and an amount lower than the invoice amount. + /// + /// This uses LDK's partial MPP support by declaring the invoice amount as the total MPP value + /// while only sending `amount_msat` from this node. The receiving node must be willing to + /// accept underpaying HTLCs for the payment to complete. + /// + /// This will fail if the invoice is a zero-amount invoice, or if the amount given is greater + /// than or equal to the value required by the invoice. Use [`Self::send_using_amount`] instead + /// when paying a zero-amount invoice or paying at least the invoice amount. + /// + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. + pub fn send_using_amount_underpaying( + &self, invoice: &Bolt11Invoice, amount_msat: u64, + route_parameters: Option, + ) -> Result { + self.ensure_running()?; - Ok(payment_id) - }, - Err(Bolt11PaymentError::InvalidAmount) => { - log_error!( - self.logger, - "Failed to send payment due to amount given being insufficient." - ); - return Err(Error::InvalidInvoice); - }, - Err(Bolt11PaymentError::SendingFailed(e)) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - match e { - RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), - _ => { - let kind = PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: payment_secret, - counterparty_skimmed_fee_msat: None, - }; - let payment = PaymentDetails::new( - payment_id, - kind, - Some(amount_msat), - None, - PaymentDirection::Outbound, - PaymentStatus::Failed, - ); + let invoice = maybe_deref(invoice); + let invoice_amount_msat = invoice.amount_milli_satoshis().ok_or_else(|| { + log_error!(self.logger, "Failed to underpay as the given invoice is \"zero-amount\"."); + Error::InvalidInvoice + })?; - self.runtime.block_on(self.payment_store.insert(payment))?; - Err(Error::PaymentSendingFailed) - }, - } - }, + if amount_msat >= invoice_amount_msat { + log_error!( + self.logger, + "Failed to underpay as the given amount needs to be less than the invoice amount: required less than {}msat, gave {}msat.", + invoice_amount_msat, + amount_msat + ); + return Err(Error::InvalidAmount); } + + self.send_internal( + invoice, + Some(amount_msat), + route_parameters, + Some(invoice_amount_msat), + "Failed to send payment due to amount given being insufficient.", + ) } /// Allows to attempt manually claiming payments with the given preimage that have previously diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 309d5bf4d..f232661ff 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -28,7 +28,7 @@ use common::{ }; use electrsd::corepc_node::Node as BitcoinD; use electrsd::ElectrsD; -use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; +use ldk_node::config::{AsyncPaymentsRole, ChannelConfig, EsploraSyncConfig}; use ldk_node::entropy::NodeEntropy; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ @@ -304,6 +304,100 @@ async fn multi_hop_sending() { expect_payment_successful_event!(nodes[0], payment_id, Some(fee_paid_msat)); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn split_underpaid_bolt11_payment() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let node_c = setup_node(&chain_source, random_config(true)); + + let addr_c = node_c.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_c], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_c.sync_wallets().unwrap(); + assert_eq!(node_c.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + let mut receiver_channel_config = ChannelConfig::default(); + receiver_channel_config.accept_underpaying_htlcs = true; + + // The receiver opens both channels so its per-channel config accepts underpaying HTLCs. + // It pushes liquidity to both payers so each payer can send half of the invoice back. + let channel_amount_sat = 1_000_000; + let push_amount_msat = Some(500_000_000); + for payer in [&node_a, &node_b] { + node_c + .open_channel( + payer.node_id(), + payer.listening_addresses().unwrap().first().unwrap().clone(), + channel_amount_sat, + push_amount_msat, + Some(receiver_channel_config), + ) + .unwrap(); + + let funding_txo_c = expect_channel_pending_event!(node_c, payer.node_id()); + let funding_txo_payer = expect_channel_pending_event!(payer, node_c.node_id()); + assert_eq!(funding_txo_c, funding_txo_payer); + wait_for_tx(&electrsd.client, funding_txo_c.txid).await; + + node_c.sync_wallets().unwrap(); + } + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + for node in [&node_a, &node_b, &node_c] { + node.sync_wallets().unwrap(); + } + + expect_channel_ready_event!(node_c, node_a.node_id()); + expect_channel_ready_event!(node_a, node_c.node_id()); + expect_channel_ready_event!(node_c, node_b.node_id()); + expect_channel_ready_event!(node_b, node_c.node_id()); + + let amount_msat = 100_000_000; + let half_amount_msat = amount_msat / 2; + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("split")).unwrap()); + let invoice = + node_c.bolt11_payment().receive(amount_msat, &invoice_description.into(), 3600).unwrap(); + + // Each payer sends only half the invoice amount, while declaring the full invoice amount as + // the total MPP value. The receiver should claim only once both HTLCs arrive. + let payment_id_a = node_a + .bolt11_payment() + .send_using_amount_underpaying(&invoice, half_amount_msat, None) + .unwrap(); + let payment_id_b = node_b + .bolt11_payment() + .send_using_amount_underpaying(&invoice, half_amount_msat, None) + .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); + + // 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()); + assert_eq!(receiver_payments.len(), 1); + assert_eq!(receiver_payments.first().unwrap().amount_msat, Some(amount_msat)); + + let node_a_payments = node_a.list_payments_with_filter(|p| p.id == payment_id_a); + assert_eq!(node_a_payments.len(), 1); + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(half_amount_msat)); + + let node_b_payments = node_b.list_payments_with_filter(|p| p.id == payment_id_b); + assert_eq!(node_b_payments.len(), 1); + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(half_amount_msat)); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn start_stop_reinit() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();