Skip to content

Commit d3faf8e

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 38b65b2 commit d3faf8e

2 files changed

Lines changed: 145 additions & 1 deletion

File tree

src/payment/bolt11.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ mod tests {
282282
impl Bolt11Payment {
283283
fn send_internal(
284284
&self, invoice: &LdkBolt11Invoice, amount_msat: Option<u64>,
285-
route_parameters: Option<RouteParametersConfig>, invalid_amount_log: &'static str,
285+
route_parameters: Option<RouteParametersConfig>,
286+
declared_total_mpp_value_msat_override: Option<u64>, invalid_amount_log: &'static str,
286287
) -> Result<PaymentId, Error> {
287288
if !*self.is_running.read().expect("lock") {
288289
return Err(Error::NotRunning);
@@ -314,6 +315,7 @@ impl Bolt11Payment {
314315
let optional_params = OptionalBolt11PaymentParams {
315316
retry_strategy,
316317
route_params_config,
318+
declared_total_mpp_value_msat_override,
317319
..Default::default()
318320
};
319321
match self.channel_manager.pay_for_bolt11_invoice(
@@ -401,6 +403,7 @@ impl Bolt11Payment {
401403
invoice,
402404
None,
403405
route_parameters,
406+
None,
404407
"Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead.",
405408
)
406409
}
@@ -436,6 +439,52 @@ impl Bolt11Payment {
436439
invoice,
437440
Some(amount_msat),
438441
route_parameters,
442+
None,
443+
"Failed to send payment due to amount given being insufficient.",
444+
)
445+
}
446+
447+
/// Send a payment given an invoice and an amount lower than the invoice amount.
448+
///
449+
/// This uses LDK's partial MPP support by declaring the invoice amount as the total MPP value
450+
/// while only sending `amount_msat` from this node. The receiving node must be willing to
451+
/// accept underpaying HTLCs for the payment to complete.
452+
///
453+
/// This will fail if the invoice is a zero-amount invoice, or if the amount given is greater
454+
/// than or equal to the value required by the invoice. Use [`Self::send_using_amount`] instead
455+
/// when paying a zero-amount invoice or paying at least the invoice amount.
456+
///
457+
/// If `route_parameters` are provided they will override the default as well as the
458+
/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
459+
pub fn send_using_amount_underpaying(
460+
&self, invoice: &Bolt11Invoice, amount_msat: u64,
461+
route_parameters: Option<RouteParametersConfig>,
462+
) -> Result<PaymentId, Error> {
463+
if !*self.is_running.read().expect("lock") {
464+
return Err(Error::NotRunning);
465+
}
466+
467+
let invoice = maybe_deref(invoice);
468+
let invoice_amount_msat = invoice.amount_milli_satoshis().ok_or_else(|| {
469+
log_error!(self.logger, "Failed to underpay as the given invoice is \"zero-amount\".");
470+
Error::InvalidInvoice
471+
})?;
472+
473+
if amount_msat >= invoice_amount_msat {
474+
log_error!(
475+
self.logger,
476+
"Failed to underpay as the given amount needs to be less than the invoice amount: required less than {}msat, gave {}msat.",
477+
invoice_amount_msat,
478+
amount_msat
479+
);
480+
return Err(Error::InvalidAmount);
481+
}
482+
483+
self.send_internal(
484+
invoice,
485+
Some(amount_msat),
486+
route_parameters,
487+
Some(invoice_amount_msat),
439488
"Failed to send payment due to amount given being insufficient.",
440489
)
441490
}

tests/integration_tests_rust.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,101 @@ 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_a = node_a.onchain_payment().new_address().unwrap();
315+
let addr_b = node_b.onchain_payment().new_address().unwrap();
316+
let addr_c = node_c.onchain_payment().new_address().unwrap();
317+
let premine_amount_sat = 5_000_000;
318+
premine_and_distribute_funds(
319+
&bitcoind.client,
320+
&electrsd.client,
321+
vec![addr_a, addr_b, addr_c],
322+
Amount::from_sat(premine_amount_sat),
323+
)
324+
.await;
325+
326+
for node in [&node_a, &node_b, &node_c] {
327+
node.sync_wallets().unwrap();
328+
assert_eq!(node.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
329+
}
330+
331+
// The receiver opens both channels and pushes liquidity to both payers so each payer can send
332+
// half of the invoice back.
333+
let channel_amount_sat = 1_000_000;
334+
let push_amount_msat = Some(500_000_000);
335+
for payer in [&node_a, &node_b] {
336+
node_c
337+
.open_channel(
338+
payer.node_id(),
339+
payer.listening_addresses().unwrap().first().unwrap().clone(),
340+
channel_amount_sat,
341+
push_amount_msat,
342+
None,
343+
)
344+
.unwrap();
345+
346+
let funding_txo_c = expect_channel_pending_event!(node_c, payer.node_id());
347+
let funding_txo_payer = expect_channel_pending_event!(payer, node_c.node_id());
348+
assert_eq!(funding_txo_c, funding_txo_payer);
349+
wait_for_tx(&electrsd.client, funding_txo_c.txid).await;
350+
351+
node_c.sync_wallets().unwrap();
352+
}
353+
354+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
355+
356+
for node in [&node_a, &node_b, &node_c] {
357+
node.sync_wallets().unwrap();
358+
}
359+
360+
expect_channel_ready_events!(node_c, node_a.node_id(), node_b.node_id());
361+
expect_channel_ready_event!(node_a, node_c.node_id());
362+
expect_channel_ready_event!(node_b, node_c.node_id());
363+
364+
let amount_msat = 100_000_000;
365+
let half_amount_msat = amount_msat / 2;
366+
let invoice_description =
367+
Bolt11InvoiceDescription::Direct(Description::new(String::from("split")).unwrap());
368+
let invoice =
369+
node_c.bolt11_payment().receive(amount_msat, &invoice_description.into(), 3600).unwrap();
370+
371+
// Each payer sends only half the invoice amount, while declaring the full invoice amount as
372+
// the total MPP value. The receiver should claim only once both HTLCs arrive.
373+
let payment_id_a = node_a
374+
.bolt11_payment()
375+
.send_using_amount_underpaying(&invoice, half_amount_msat, None)
376+
.unwrap();
377+
let payment_id_b = node_b
378+
.bolt11_payment()
379+
.send_using_amount_underpaying(&invoice, half_amount_msat, None)
380+
.unwrap();
381+
382+
let receiver_payment_id = expect_payment_received_event!(node_c, amount_msat);
383+
assert_eq!(receiver_payment_id, Some(PaymentId(invoice.payment_hash().0)));
384+
expect_payment_successful_event!(node_a, Some(payment_id_a), None);
385+
expect_payment_successful_event!(node_b, Some(payment_id_b), None);
386+
387+
// The receiver records the full invoice amount; each payer records only its own half.
388+
let receiver_payments =
389+
node_c.list_payments_with_filter(|p| p.id == receiver_payment_id.unwrap());
390+
assert_eq!(receiver_payments.len(), 1);
391+
assert_eq!(receiver_payments.first().unwrap().amount_msat, Some(amount_msat));
392+
393+
let node_a_payments = node_a.list_payments_with_filter(|p| p.id == payment_id_a);
394+
assert_eq!(node_a_payments.len(), 1);
395+
assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(half_amount_msat));
396+
397+
let node_b_payments = node_b.list_payments_with_filter(|p| p.id == payment_id_b);
398+
assert_eq!(node_b_payments.len(), 1);
399+
assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(half_amount_msat));
400+
}
401+
307402
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
308403
async fn start_stop_reinit() {
309404
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)