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
13 changes: 13 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,19 @@ where
};
}

// If the invoice has been canceled, reject the HTLC. We only do this
// when the preimage is known to preserve retry behavior for `_for_hash`
// manual-claim payments, where `fail_for_hash` may have been a
// temporary rejection (e.g., preimage not yet available).
if info.status == PaymentStatus::Failed && purpose.preimage().is_some() {
log_info!(
self.logger,
"Refused inbound payment with ID {payment_id}: invoice has been canceled."
);
self.channel_manager.fail_htlc_backwards(&payment_hash);
return Ok(());
}

if info.status == PaymentStatus::Succeeded
|| matches!(info.kind, PaymentKind::Spontaneous { .. })
{
Expand Down
34 changes: 34 additions & 0 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,40 @@ impl Bolt11Payment {
Ok(())
}

/// Allows to cancel a previously created invoice identified by the given payment hash.
///
/// This will mark the corresponding payment as failed and cause any incoming HTLCs for this
/// invoice to be automatically failed back.
///
/// Will check that the payment is known and has not already been claimed, and will return an
/// error otherwise.
pub fn cancel_invoice(&self, payment_hash: PaymentHash) -> Result<(), Error> {
let payment_id = PaymentId(payment_hash.0);

if let Some(info) = self.payment_store.get(&payment_id) {
if info.direction != PaymentDirection::Inbound {
log_error!(
self.logger,
"Failed to cancel invoice for non-inbound payment with hash {payment_hash}"
);
return Err(Error::InvalidPaymentHash);
}

if info.status == PaymentStatus::Succeeded {
log_error!(
self.logger,
"Failed to cancel invoice with hash {payment_hash}: payment has already been claimed",
);
return Err(Error::InvalidPaymentHash);
}
} else {
log_error!(self.logger, "Failed to cancel unknown invoice with hash {payment_hash}");
return Err(Error::InvalidPaymentHash);
}

self.fail_for_hash(payment_hash)
}

/// Returns a payable invoice that can be used to request and receive a payment of the amount
/// given.
///
Expand Down
75 changes: 75 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,81 @@ async fn spontaneous_send_with_custom_preimage() {
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn cancel_invoice() {
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 addr_a = node_a.onchain_payment().new_address().unwrap();
let addr_b = node_b.onchain_payment().new_address().unwrap();

let premine_amount_sat = 2_125_000;
premine_and_distribute_funds(
&bitcoind.client,
&electrsd.client,
vec![addr_a, addr_b],
Amount::from_sat(premine_amount_sat),
)
.await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

let funding_amount_sat = 2_080_000;
let push_msat = (funding_amount_sat / 2) * 1000;
open_channel_push_amt(&node_a, &node_b, funding_amount_sat, Some(push_msat), true, &electrsd)
.await;

generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

expect_channel_ready_event!(node_a, node_b.node_id());
expect_channel_ready_event!(node_b, node_a.node_id());

// Sleep a bit for gossip to propagate.
tokio::time::sleep(std::time::Duration::from_secs(1)).await;

let invoice_description =
Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap());
let amount_msat = 2_500_000;

// Create an invoice on node_b and immediately cancel it.
let invoice = node_b
.bolt11_payment()
.receive(amount_msat, &invoice_description.clone().into(), 9217)
.unwrap();

let payment_hash = PaymentHash(invoice.payment_hash().0);
let payment_id = PaymentId(payment_hash.0);
node_b.bolt11_payment().cancel_invoice(payment_hash).unwrap();

// Verify the payment status is now Failed.
assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Failed);

// Attempting to pay the canceled invoice should result in a failure on the sender side.
node_a.bolt11_payment().send(&invoice, None).unwrap();
expect_event!(node_a, PaymentFailed);

// Verify cancelling an already claimed payment errors.
let invoice_2 = node_b
.bolt11_payment()
.receive(amount_msat, &invoice_description.clone().into(), 9217)
.unwrap();
let payment_id_2 = node_a.bolt11_payment().send(&invoice_2, None).unwrap();
expect_payment_received_event!(node_b, amount_msat);
expect_payment_successful_event!(node_a, Some(payment_id_2), None);

let payment_hash_2 = PaymentHash(invoice_2.payment_hash().0);
assert_eq!(
node_b.bolt11_payment().cancel_invoice(payment_hash_2),
Err(NodeError::InvalidPaymentHash)
);

node_a.stop().unwrap();
node_b.stop().unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn drop_in_async_context() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
Expand Down
Loading