Skip to content

Commit 4aac5e8

Browse files
authored
Merge pull request #939 from benthecarman/underpay-htlc
Expose BOLT11 underpayment sends
2 parents c7e001b + d3faf8e commit 4aac5e8

2 files changed

Lines changed: 187 additions & 99 deletions

File tree

src/payment/bolt11.rs

Lines changed: 92 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -264,20 +264,16 @@ mod tests {
264264
}
265265
}
266266

267-
#[cfg_attr(feature = "uniffi", uniffi::export)]
268267
impl Bolt11Payment {
269-
/// Send a payment given an invoice.
270-
///
271-
/// If `route_parameters` are provided they will override the default as well as the
272-
/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
273-
pub fn send(
274-
&self, invoice: &Bolt11Invoice, route_parameters: Option<RouteParametersConfig>,
268+
fn send_internal(
269+
&self, invoice: &LdkBolt11Invoice, amount_msat: Option<u64>,
270+
route_parameters: Option<RouteParametersConfig>,
271+
declared_total_mpp_value_msat_override: Option<u64>, invalid_amount_log: &'static str,
275272
) -> Result<PaymentId, Error> {
276273
if !*self.is_running.read().expect("lock") {
277274
return Err(Error::NotRunning);
278275
}
279276

280-
let invoice = maybe_deref(invoice);
281277
let payment_hash = invoice.payment_hash();
282278
let payment_id = PaymentId(invoice.payment_hash().0);
283279
if let Some(payment) = self.payment_store.get(&payment_id) {
@@ -293,23 +289,34 @@ impl Bolt11Payment {
293289
route_parameters.or(self.config.route_parameters).unwrap_or_default();
294290
let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
295291
let payment_secret = Some(*invoice.payment_secret());
292+
let payment_amount_msat = match amount_msat.or_else(|| invoice.amount_milli_satoshis()) {
293+
Some(amount_msat) => amount_msat,
294+
None => {
295+
log_error!(self.logger, "{}", invalid_amount_log);
296+
return Err(Error::InvalidInvoice);
297+
},
298+
};
296299

297300
let optional_params = OptionalBolt11PaymentParams {
298301
retry_strategy,
299302
route_params_config,
303+
declared_total_mpp_value_msat_override,
300304
..Default::default()
301305
};
302306
match self.channel_manager.pay_for_bolt11_invoice(
303307
invoice,
304308
payment_id,
305-
None,
309+
amount_msat,
306310
optional_params,
307311
) {
308312
Ok(()) => {
309313
let payee_pubkey = invoice.recover_payee_pub_key();
310-
let amt_msat =
311-
invoice.amount_milli_satoshis().expect("invoice amount should be set");
312-
log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey);
314+
log_info!(
315+
self.logger,
316+
"Initiated sending {} msat to {}",
317+
payment_amount_msat,
318+
payee_pubkey
319+
);
313320

314321
let kind = PaymentKind::Bolt11 {
315322
hash: payment_hash,
@@ -320,7 +327,7 @@ impl Bolt11Payment {
320327
let payment = PaymentDetails::new(
321328
payment_id,
322329
kind,
323-
invoice.amount_milli_satoshis(),
330+
Some(payment_amount_msat),
324331
None,
325332
PaymentDirection::Outbound,
326333
PaymentStatus::Pending,
@@ -331,9 +338,7 @@ impl Bolt11Payment {
331338
Ok(payment_id)
332339
},
333340
Err(Bolt11PaymentError::InvalidAmount) => {
334-
log_error!(self.logger,
335-
"Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead."
336-
);
341+
log_error!(self.logger, "{}", invalid_amount_log);
337342
return Err(Error::InvalidInvoice);
338343
},
339344
Err(Bolt11PaymentError::SendingFailed(e)) => {
@@ -350,7 +355,7 @@ impl Bolt11Payment {
350355
let payment = PaymentDetails::new(
351356
payment_id,
352357
kind,
353-
invoice.amount_milli_satoshis(),
358+
Some(payment_amount_msat),
354359
None,
355360
PaymentDirection::Outbound,
356361
PaymentStatus::Failed,
@@ -363,6 +368,30 @@ impl Bolt11Payment {
363368
},
364369
}
365370
}
371+
}
372+
373+
#[cfg_attr(feature = "uniffi", uniffi::export)]
374+
impl Bolt11Payment {
375+
/// Send a payment given an invoice.
376+
///
377+
/// If `route_parameters` are provided they will override the default as well as the
378+
/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
379+
pub fn send(
380+
&self, invoice: &Bolt11Invoice, route_parameters: Option<RouteParametersConfig>,
381+
) -> Result<PaymentId, Error> {
382+
if !*self.is_running.read().expect("lock") {
383+
return Err(Error::NotRunning);
384+
}
385+
386+
let invoice = maybe_deref(invoice);
387+
self.send_internal(
388+
invoice,
389+
None,
390+
route_parameters,
391+
None,
392+
"Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead.",
393+
)
394+
}
366395

367396
/// Send a payment given an invoice and an amount in millisatoshis.
368397
///
@@ -391,94 +420,58 @@ impl Bolt11Payment {
391420
}
392421
}
393422

394-
let payment_hash = invoice.payment_hash();
395-
let payment_id = PaymentId(invoice.payment_hash().0);
396-
if let Some(payment) = self.payment_store.get(&payment_id) {
397-
if payment.status == PaymentStatus::Pending
398-
|| payment.status == PaymentStatus::Succeeded
399-
{
400-
log_error!(self.logger, "Payment error: an invoice must not be paid twice.");
401-
return Err(Error::DuplicatePayment);
402-
}
403-
}
404-
405-
let route_params_config =
406-
route_parameters.or(self.config.route_parameters).unwrap_or_default();
407-
let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
408-
let payment_secret = Some(*invoice.payment_secret());
409-
410-
let optional_params = OptionalBolt11PaymentParams {
411-
retry_strategy,
412-
route_params_config,
413-
..Default::default()
414-
};
415-
match self.channel_manager.pay_for_bolt11_invoice(
423+
self.send_internal(
416424
invoice,
417-
payment_id,
418425
Some(amount_msat),
419-
optional_params,
420-
) {
421-
Ok(()) => {
422-
let payee_pubkey = invoice.recover_payee_pub_key();
423-
log_info!(
424-
self.logger,
425-
"Initiated sending {} msat to {}",
426-
amount_msat,
427-
payee_pubkey
428-
);
429-
430-
let kind = PaymentKind::Bolt11 {
431-
hash: payment_hash,
432-
preimage: None,
433-
secret: payment_secret,
434-
counterparty_skimmed_fee_msat: None,
435-
};
426+
route_parameters,
427+
None,
428+
"Failed to send payment due to amount given being insufficient.",
429+
)
430+
}
436431

437-
let payment = PaymentDetails::new(
438-
payment_id,
439-
kind,
440-
Some(amount_msat),
441-
None,
442-
PaymentDirection::Outbound,
443-
PaymentStatus::Pending,
444-
);
445-
self.runtime.block_on(self.payment_store.insert(payment))?;
432+
/// Send a payment given an invoice and an amount lower than the invoice amount.
433+
///
434+
/// This uses LDK's partial MPP support by declaring the invoice amount as the total MPP value
435+
/// while only sending `amount_msat` from this node. The receiving node must be willing to
436+
/// accept underpaying HTLCs for the payment to complete.
437+
///
438+
/// This will fail if the invoice is a zero-amount invoice, or if the amount given is greater
439+
/// than or equal to the value required by the invoice. Use [`Self::send_using_amount`] instead
440+
/// when paying a zero-amount invoice or paying at least the invoice amount.
441+
///
442+
/// If `route_parameters` are provided they will override the default as well as the
443+
/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
444+
pub fn send_using_amount_underpaying(
445+
&self, invoice: &Bolt11Invoice, amount_msat: u64,
446+
route_parameters: Option<RouteParametersConfig>,
447+
) -> Result<PaymentId, Error> {
448+
if !*self.is_running.read().expect("lock") {
449+
return Err(Error::NotRunning);
450+
}
446451

447-
Ok(payment_id)
448-
},
449-
Err(Bolt11PaymentError::InvalidAmount) => {
450-
log_error!(
451-
self.logger,
452-
"Failed to send payment due to amount given being insufficient."
453-
);
454-
return Err(Error::InvalidInvoice);
455-
},
456-
Err(Bolt11PaymentError::SendingFailed(e)) => {
457-
log_error!(self.logger, "Failed to send payment: {:?}", e);
458-
match e {
459-
RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment),
460-
_ => {
461-
let kind = PaymentKind::Bolt11 {
462-
hash: payment_hash,
463-
preimage: None,
464-
secret: payment_secret,
465-
counterparty_skimmed_fee_msat: None,
466-
};
467-
let payment = PaymentDetails::new(
468-
payment_id,
469-
kind,
470-
Some(amount_msat),
471-
None,
472-
PaymentDirection::Outbound,
473-
PaymentStatus::Failed,
474-
);
452+
let invoice = maybe_deref(invoice);
453+
let invoice_amount_msat = invoice.amount_milli_satoshis().ok_or_else(|| {
454+
log_error!(self.logger, "Failed to underpay as the given invoice is \"zero-amount\".");
455+
Error::InvalidInvoice
456+
})?;
475457

476-
self.runtime.block_on(self.payment_store.insert(payment))?;
477-
Err(Error::PaymentSendingFailed)
478-
},
479-
}
480-
},
458+
if amount_msat >= invoice_amount_msat {
459+
log_error!(
460+
self.logger,
461+
"Failed to underpay as the given amount needs to be less than the invoice amount: required less than {}msat, gave {}msat.",
462+
invoice_amount_msat,
463+
amount_msat
464+
);
465+
return Err(Error::InvalidAmount);
481466
}
467+
468+
self.send_internal(
469+
invoice,
470+
Some(amount_msat),
471+
route_parameters,
472+
Some(invoice_amount_msat),
473+
"Failed to send payment due to amount given being insufficient.",
474+
)
482475
}
483476

484477
/// Allows to attempt manually claiming payments with the given preimage that have previously

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)