Skip to content

Commit 4de807a

Browse files
committed
Fix BOLT11 DuplicatePayment triggering on-chain fallback in unified payments
Error::DuplicatePayment is now terminal in UnifiedPayment::send, preventing a duplicate Lightning payment from falling back to an on-chain payment.
1 parent 8da21bc commit 4de807a

2 files changed

Lines changed: 105 additions & 9 deletions

File tree

src/payment/unified.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,22 @@ impl UnifiedPayment {
287287

288288
let payment_result = if let Ok(hrn) = HumanReadableName::from_encoded(uri_str) {
289289
let hrn = maybe_wrap(hrn.clone());
290-
self.bolt12_payment.send_using_amount_inner(&offer, amount_msat.unwrap_or(0), None, None, route_parameters, Some(hrn))
290+
self.bolt12_payment.send_using_amount_inner(
291+
&offer,
292+
amount_msat.unwrap_or(0),
293+
None,
294+
None,
295+
route_parameters,
296+
Some(hrn),
297+
)
291298
} else if let Some(amount_msat) = amount_msat {
292-
self.bolt12_payment.send_using_amount(&offer, amount_msat, None, None, route_parameters)
299+
self.bolt12_payment.send_using_amount(
300+
&offer,
301+
amount_msat,
302+
None,
303+
None,
304+
route_parameters,
305+
)
293306
} else {
294307
self.bolt12_payment.send(&offer, None, None, route_parameters)
295308
}
@@ -304,14 +317,19 @@ impl UnifiedPayment {
304317
},
305318
PaymentMethod::LightningBolt11(invoice) => {
306319
let invoice = maybe_wrap(invoice.clone());
307-
let payment_result = self.bolt11_invoice.send(&invoice, route_parameters)
308-
.map_err(|e| {
320+
let payment_result = self.bolt11_invoice.send(&invoice, route_parameters);
321+
322+
match payment_result {
323+
Ok(payment_id) => {
324+
return Ok(UnifiedPaymentResult::Bolt11 { payment_id });
325+
},
326+
Err(Error::DuplicatePayment) => {
327+
log_error!(self.logger, "Failed to send BOLT11 invoice: DuplicatePayment. This is part of a unified payment. Aborting to avoid duplicate payment.");
328+
return Err(Error::DuplicatePayment);
329+
},
330+
Err(e) => {
309331
log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified payment. Falling back to the on-chain transaction.", e);
310-
e
311-
});
312-
313-
if let Ok(payment_id) = payment_result {
314-
return Ok(UnifiedPaymentResult::Bolt11 { payment_id });
332+
},
315333
}
316334
},
317335
PaymentMethod::OnChain(address) => {

tests/integration_tests_rust.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2967,6 +2967,84 @@ async fn unified_send_receive_bip21_uri() {
29672967
assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000);
29682968
}
29692969

2970+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2971+
async fn unified_send_bolt11_duplicate_payment_no_onchain_fallback() {
2972+
// Regression test for https://github.com/lightningdevkit/ldk-node/issues/1033
2973+
//
2974+
// Sending a unified BIP21 payment that resolves to BOLT11 should return
2975+
// Error::DuplicatePayment on retry, not fall back to the on-chain method.
2976+
2977+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2978+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2979+
2980+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, false);
2981+
2982+
let address_a = node_a.onchain_payment().new_address().unwrap();
2983+
let premined_sats = 5_000_000;
2984+
2985+
premine_and_distribute_funds(
2986+
&bitcoind.client,
2987+
&electrsd.client,
2988+
vec![address_a],
2989+
Amount::from_sat(premined_sats),
2990+
)
2991+
.await;
2992+
2993+
node_a.sync_wallets().unwrap();
2994+
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await;
2995+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2996+
2997+
node_a.sync_wallets().unwrap();
2998+
node_b.sync_wallets().unwrap();
2999+
3000+
expect_channel_ready_event!(node_a, node_b.node_id());
3001+
expect_channel_ready_event!(node_b, node_a.node_id());
3002+
3003+
// Sleep until we broadcast a node announcement.
3004+
while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() {
3005+
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
3006+
}
3007+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
3008+
3009+
let expected_amount_sats = 100_000;
3010+
let expiry_sec = 4_000;
3011+
3012+
// Receive a unified payment on node_b — this will produce a URI with BOLT12 offer + BOLT11 invoice.
3013+
let uri_str = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec).unwrap();
3014+
3015+
// Strip the BOLT12 offer so the URI resolves to BOLT11 only (no BOLT12, no on-chain fallback).
3016+
let uri_str_bolt11_only = uri_str.split("&lno=").next().unwrap();
3017+
3018+
// First send: should succeed via BOLT11.
3019+
let first_result = node_a.unified_payment().send(uri_str_bolt11_only, None, None).await;
3020+
let first_payment_id = match first_result {
3021+
Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => payment_id,
3022+
Ok(other) => panic!("Expected Bolt11 result on first send, got: {:?}", other),
3023+
Err(e) => panic!("Expected Bolt11 result on first send, got error: {:?}", e),
3024+
};
3025+
expect_payment_successful_event!(node_a, Some(first_payment_id), None);
3026+
3027+
// Second send with the same URI: should return DuplicatePayment, NOT fall back to on-chain.
3028+
let second_result = node_a.unified_payment().send(uri_str_bolt11_only, None, None).await;
3029+
match second_result {
3030+
Err(NodeError::DuplicatePayment) => {
3031+
// Expected — this is the fix for #1033.
3032+
},
3033+
Ok(UnifiedPaymentResult::Onchain { txid }) => {
3034+
panic!(
3035+
"Regression: Duplicate BOLT11 payment fell back to on-chain. txid={}. See #1033",
3036+
txid
3037+
);
3038+
},
3039+
Ok(other) => {
3040+
panic!("Expected DuplicatePayment error on retry, got: {:?}", other);
3041+
},
3042+
Err(other) => {
3043+
panic!("Expected DuplicatePayment error on retry, got: {:?}", other);
3044+
},
3045+
}
3046+
}
3047+
29703048
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
29713049
async fn lsps2_client_service_integration() {
29723050
do_lsps2_client_service_integration(true).await;

0 commit comments

Comments
 (0)