Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 91 additions & 103 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouteParametersConfig>,
) -> Result<PaymentId, Error> {
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<u64>,
route_parameters: Option<RouteParametersConfig>,
declared_total_mpp_value_msat_override: Option<u64>, invalid_amount_log: &'static str,
) -> Result<PaymentId, Error> {
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) {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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)) => {
Expand All @@ -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,
Expand All @@ -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<RouteParametersConfig>,
) -> Result<PaymentId, Error> {
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.
///
Expand All @@ -392,9 +420,7 @@ impl Bolt11Payment {
&self, invoice: &Bolt11Invoice, amount_msat: u64,
route_parameters: Option<RouteParametersConfig>,
) -> Result<PaymentId, Error> {
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() {
Expand All @@ -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<RouteParametersConfig>,
) -> Result<PaymentId, Error> {
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
Expand Down
96 changes: 95 additions & 1 deletion tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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();
Expand Down