Skip to content

Commit 894ceb6

Browse files
committed
fix(payment): skip store insertion on routing failures
Adjust the error handling for outbound payments so that when a payment fails at the pathfinding stage (e.g., `RetryableSendFailure::RouteNotFound`), we bypass inserting a `PaymentStatus::Failed` entry into the database. Fix #903
1 parent 109978d commit 894ceb6

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

src/payment/bolt11.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ impl Bolt11Payment {
307307
log_error!(self.logger, "Failed to send payment: {:?}", e);
308308
match e {
309309
RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment),
310+
RetryableSendFailure::RouteNotFound => Err(Error::PaymentSendingFailed),
310311
_ => {
311312
let kind = PaymentKind::Bolt11 {
312313
hash: payment_hash,
@@ -422,6 +423,7 @@ impl Bolt11Payment {
422423
log_error!(self.logger, "Failed to send payment: {:?}", e);
423424
match e {
424425
RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment),
426+
RetryableSendFailure::RouteNotFound => Err(Error::PaymentSendingFailed),
425427
_ => {
426428
let kind = PaymentKind::Bolt11 {
427429
hash: payment_hash,

src/payment/spontaneous.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ impl SpontaneousPayment {
139139

140140
match e {
141141
RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment),
142+
RetryableSendFailure::RouteNotFound => Err(Error::PaymentSendingFailed),
142143
_ => {
143144
let kind = PaymentKind::Spontaneous {
144145
hash: payment_hash,

tests/integration_tests_rust.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2957,3 +2957,72 @@ async fn splice_in_with_all_balance() {
29572957
node_a.stop().unwrap();
29582958
node_b.stop().unwrap();
29592959
}
2960+
2961+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2962+
async fn test_retry_on_routing_failure() {
2963+
/// Although `send()` technically allows retries for `Failed` statuses,
2964+
/// `RouteNotFound` should never persist a record to the store in the first place.
2965+
/// This asserts that the payment store remains clean after a routing failure.
2966+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2967+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2968+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true);
2969+
2970+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2971+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2972+
2973+
let premine_amount_sat = 500_000;
2974+
2975+
premine_and_distribute_funds(
2976+
&bitcoind.client,
2977+
&electrsd.client,
2978+
vec![addr_a, addr_b],
2979+
Amount::from_sat(premine_amount_sat),
2980+
)
2981+
.await;
2982+
node_a.sync_wallets().unwrap();
2983+
node_b.sync_wallets().unwrap();
2984+
assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
2985+
2986+
let _funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await;
2987+
2988+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2989+
2990+
node_a.sync_wallets().unwrap();
2991+
node_b.sync_wallets().unwrap();
2992+
2993+
let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id());
2994+
let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id());
2995+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2996+
2997+
let invoice_description =
2998+
Bolt11InvoiceDescription::Direct(Description::new(String::from("test-retry")).unwrap());
2999+
3000+
// set the payment sum more than channel capacity to fail with RouteNotFound due to liquidity constraint
3001+
let invoice = node_b
3002+
.bolt11_payment()
3003+
.receive(1_000_000_000, &invoice_description.clone().into(), 3600)
3004+
.unwrap();
3005+
3006+
// first attempt should fail with RouteNotFound as expected
3007+
let first_attempt = node_a.bolt11_payment().send(&invoice, None);
3008+
assert!(first_attempt.is_err());
3009+
assert_eq!(first_attempt.unwrap_err(), NodeError::PaymentSendingFailed);
3010+
3011+
// check that the payment is not in the store as HTCL was not sent
3012+
let payments = node_a.list_payments();
3013+
let payment_id = PaymentId(invoice.payment_hash().0);
3014+
let payment_in_store = payments.iter().find(|p| p.id == payment_id);
3015+
3016+
assert!(
3017+
payment_in_store.is_none(),
3018+
"Check not working: payment with RouteNotFound error was saved into payment_store!"
3019+
);
3020+
3021+
// second attempt to make sure that payment in the store and not treated as duplicate
3022+
let second_attempt = node_a.bolt11_payment().send(&invoice, None);
3023+
assert_ne!(second_attempt.unwrap_err(), NodeError::DuplicatePayment);
3024+
assert_eq!(second_attempt.unwrap_err(), NodeError::PaymentSendingFailed);
3025+
3026+
node_a.stop().unwrap();
3027+
node_b.stop().unwrap();
3028+
}

0 commit comments

Comments
 (0)