Skip to content

Commit f8cdad5

Browse files
committed
Expose BOLT11 underpayment sends
Add a BOLT11 payment API for sending less than the invoice amount while using the invoice amount as the declared total MPP value. Cover the path with an integration test where two nodes each pay half of one invoice and the receiver claims the full amount. AI-Tool-Disclosure: Created with OpenAI Codex.
1 parent 1cf7e61 commit f8cdad5

2 files changed

Lines changed: 143 additions & 2 deletions

File tree

src/payment/bolt11.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,8 @@ impl Bolt11Payment {
289289

290290
fn send_internal(
291291
&self, invoice: &LdkBolt11Invoice, amount_msat: Option<u64>,
292-
route_parameters: Option<RouteParametersConfig>, invalid_amount_log: &'static str,
292+
route_parameters: Option<RouteParametersConfig>,
293+
declared_total_mpp_value_msat_override: Option<u64>, invalid_amount_log: &'static str,
293294
) -> Result<PaymentId, Error> {
294295
self.ensure_running()?;
295296

@@ -313,6 +314,7 @@ impl Bolt11Payment {
313314
let optional_params = OptionalBolt11PaymentParams {
314315
retry_strategy,
315316
route_params_config,
317+
declared_total_mpp_value_msat_override,
316318
..Default::default()
317319
};
318320
match self.channel_manager.pay_for_bolt11_invoice(
@@ -400,6 +402,7 @@ impl Bolt11Payment {
400402
invoice,
401403
None,
402404
route_parameters,
405+
None,
403406
"Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead.",
404407
)
405408
}
@@ -433,6 +436,50 @@ impl Bolt11Payment {
433436
invoice,
434437
Some(amount_msat),
435438
route_parameters,
439+
None,
440+
"Failed to send payment due to amount given being insufficient.",
441+
)
442+
}
443+
444+
/// Send a payment given an invoice and an amount lower than the invoice amount.
445+
///
446+
/// This uses LDK's partial MPP support by declaring the invoice amount as the total MPP value
447+
/// while only sending `amount_msat` from this node. The receiving node must be willing to
448+
/// accept underpaying HTLCs for the payment to complete.
449+
///
450+
/// This will fail if the invoice is a zero-amount invoice, or if the amount given is greater
451+
/// than or equal to the value required by the invoice. Use [`Self::send_using_amount`] instead
452+
/// when paying a zero-amount invoice or paying at least the invoice amount.
453+
///
454+
/// If `route_parameters` are provided they will override the default as well as the
455+
/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
456+
pub fn send_using_amount_underpaying(
457+
&self, invoice: &Bolt11Invoice, amount_msat: u64,
458+
route_parameters: Option<RouteParametersConfig>,
459+
) -> Result<PaymentId, Error> {
460+
self.ensure_running()?;
461+
462+
let invoice = maybe_deref(invoice);
463+
let invoice_amount_msat = invoice.amount_milli_satoshis().ok_or_else(|| {
464+
log_error!(self.logger, "Failed to underpay as the given invoice is \"zero-amount\".");
465+
Error::InvalidInvoice
466+
})?;
467+
468+
if amount_msat >= invoice_amount_msat {
469+
log_error!(
470+
self.logger,
471+
"Failed to underpay as the given amount needs to be less than the invoice amount: required less than {}msat, gave {}msat.",
472+
invoice_amount_msat,
473+
amount_msat
474+
);
475+
return Err(Error::InvalidAmount);
476+
}
477+
478+
self.send_internal(
479+
invoice,
480+
Some(amount_msat),
481+
route_parameters,
482+
Some(invoice_amount_msat),
436483
"Failed to send payment due to amount given being insufficient.",
437484
)
438485
}

tests/integration_tests_rust.rs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use common::{
2828
};
2929
use electrsd::corepc_node::Node as BitcoinD;
3030
use electrsd::ElectrsD;
31-
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
31+
use ldk_node::config::{AsyncPaymentsRole, ChannelConfig, EsploraSyncConfig};
3232
use ldk_node::entropy::NodeEntropy;
3333
use ldk_node::liquidity::LSPS2ServiceConfig;
3434
use ldk_node::payment::{
@@ -304,6 +304,100 @@ async fn multi_hop_sending() {
304304
expect_payment_successful_event!(nodes[0], payment_id, Some(fee_paid_msat));
305305
}
306306

307+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
308+
async fn split_underpaid_bolt11_payment() {
309+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
310+
let chain_source = random_chain_source(&bitcoind, &electrsd);
311+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
312+
let node_c = setup_node(&chain_source, random_config(true));
313+
314+
let addr_c = node_c.onchain_payment().new_address().unwrap();
315+
let premine_amount_sat = 5_000_000;
316+
premine_and_distribute_funds(
317+
&bitcoind.client,
318+
&electrsd.client,
319+
vec![addr_c],
320+
Amount::from_sat(premine_amount_sat),
321+
)
322+
.await;
323+
node_c.sync_wallets().unwrap();
324+
assert_eq!(node_c.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
325+
326+
let mut receiver_channel_config = ChannelConfig::default();
327+
receiver_channel_config.accept_underpaying_htlcs = true;
328+
329+
// The receiver opens both channels so its per-channel config accepts underpaying HTLCs.
330+
// It pushes liquidity to both payers so each payer can send half of the invoice back.
331+
let channel_amount_sat = 1_000_000;
332+
let push_amount_msat = Some(500_000_000);
333+
for payer in [&node_a, &node_b] {
334+
node_c
335+
.open_channel(
336+
payer.node_id(),
337+
payer.listening_addresses().unwrap().first().unwrap().clone(),
338+
channel_amount_sat,
339+
push_amount_msat,
340+
Some(receiver_channel_config),
341+
)
342+
.unwrap();
343+
344+
let funding_txo_c = expect_channel_pending_event!(node_c, payer.node_id());
345+
let funding_txo_payer = expect_channel_pending_event!(payer, node_c.node_id());
346+
assert_eq!(funding_txo_c, funding_txo_payer);
347+
wait_for_tx(&electrsd.client, funding_txo_c.txid).await;
348+
349+
node_c.sync_wallets().unwrap();
350+
}
351+
352+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
353+
354+
for node in [&node_a, &node_b, &node_c] {
355+
node.sync_wallets().unwrap();
356+
}
357+
358+
expect_channel_ready_event!(node_c, node_a.node_id());
359+
expect_channel_ready_event!(node_a, node_c.node_id());
360+
expect_channel_ready_event!(node_c, node_b.node_id());
361+
expect_channel_ready_event!(node_b, node_c.node_id());
362+
363+
let amount_msat = 100_000_000;
364+
let half_amount_msat = amount_msat / 2;
365+
let invoice_description =
366+
Bolt11InvoiceDescription::Direct(Description::new(String::from("split")).unwrap());
367+
let invoice =
368+
node_c.bolt11_payment().receive(amount_msat, &invoice_description.into(), 3600).unwrap();
369+
370+
// Each payer sends only half the invoice amount, while declaring the full invoice amount as
371+
// the total MPP value. The receiver should claim only once both HTLCs arrive.
372+
let payment_id_a = node_a
373+
.bolt11_payment()
374+
.send_using_amount_underpaying(&invoice, half_amount_msat, None)
375+
.unwrap();
376+
let payment_id_b = node_b
377+
.bolt11_payment()
378+
.send_using_amount_underpaying(&invoice, half_amount_msat, None)
379+
.unwrap();
380+
381+
let receiver_payment_id = expect_payment_received_event!(node_c, amount_msat);
382+
assert_eq!(receiver_payment_id, Some(PaymentId(invoice.payment_hash().0)));
383+
expect_payment_successful_event!(node_a, Some(payment_id_a), None);
384+
expect_payment_successful_event!(node_b, Some(payment_id_b), None);
385+
386+
// The receiver records the full invoice amount; each payer records only its own half.
387+
let receiver_payments =
388+
node_c.list_payments_with_filter(|p| p.id == receiver_payment_id.unwrap());
389+
assert_eq!(receiver_payments.len(), 1);
390+
assert_eq!(receiver_payments.first().unwrap().amount_msat, Some(amount_msat));
391+
392+
let node_a_payments = node_a.list_payments_with_filter(|p| p.id == payment_id_a);
393+
assert_eq!(node_a_payments.len(), 1);
394+
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(half_amount_msat));
395+
396+
let node_b_payments = node_b.list_payments_with_filter(|p| p.id == payment_id_b);
397+
assert_eq!(node_b_payments.len(), 1);
398+
assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(half_amount_msat));
399+
}
400+
307401
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
308402
async fn start_stop_reinit() {
309403
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)