Skip to content

Commit 74c4c41

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 b56c167 commit 74c4c41

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
@@ -2912,6 +2912,84 @@ async fn unified_send_receive_bip21_uri() {
29122912
assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000);
29132913
}
29142914

2915+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2916+
async fn unified_send_bolt11_duplicate_payment_no_onchain_fallback() {
2917+
// Regression test for https://github.com/lightningdevkit/ldk-node/issues/1033
2918+
//
2919+
// Sending a unified BIP21 payment that resolves to BOLT11 should return
2920+
// Error::DuplicatePayment on retry, not fall back to the on-chain method.
2921+
2922+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2923+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2924+
2925+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, false);
2926+
2927+
let address_a = node_a.onchain_payment().new_address().unwrap();
2928+
let premined_sats = 5_000_000;
2929+
2930+
premine_and_distribute_funds(
2931+
&bitcoind.client,
2932+
&electrsd.client,
2933+
vec![address_a],
2934+
Amount::from_sat(premined_sats),
2935+
)
2936+
.await;
2937+
2938+
node_a.sync_wallets().unwrap();
2939+
open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await;
2940+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2941+
2942+
node_a.sync_wallets().unwrap();
2943+
node_b.sync_wallets().unwrap();
2944+
2945+
expect_channel_ready_event!(node_a, node_b.node_id());
2946+
expect_channel_ready_event!(node_b, node_a.node_id());
2947+
2948+
// Sleep until we broadcast a node announcement.
2949+
while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() {
2950+
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2951+
}
2952+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
2953+
2954+
let expected_amount_sats = 100_000;
2955+
let expiry_sec = 4_000;
2956+
2957+
// Receive a unified payment on node_b — this will produce a URI with BOLT12 offer + BOLT11 invoice.
2958+
let uri_str = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec).unwrap();
2959+
2960+
// Strip the BOLT12 offer so the URI resolves to BOLT11 only (no BOLT12, no on-chain fallback).
2961+
let uri_str_bolt11_only = uri_str.split("&lno=").next().unwrap();
2962+
2963+
// First send: should succeed via BOLT11.
2964+
let first_result = node_a.unified_payment().send(uri_str_bolt11_only, None, None).await;
2965+
let first_payment_id = match first_result {
2966+
Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => payment_id,
2967+
Ok(other) => panic!("Expected Bolt11 result on first send, got: {:?}", other),
2968+
Err(e) => panic!("Expected Bolt11 result on first send, got error: {:?}", e),
2969+
};
2970+
expect_payment_successful_event!(node_a, Some(first_payment_id), None);
2971+
2972+
// Second send with the same URI: should return DuplicatePayment, NOT fall back to on-chain.
2973+
let second_result = node_a.unified_payment().send(uri_str_bolt11_only, None, None).await;
2974+
match second_result {
2975+
Err(NodeError::DuplicatePayment) => {
2976+
// Expected — this is the fix for #1033.
2977+
},
2978+
Ok(UnifiedPaymentResult::Onchain { txid }) => {
2979+
panic!(
2980+
"Regression: Duplicate BOLT11 payment fell back to on-chain. txid={}. See #1033",
2981+
txid
2982+
);
2983+
},
2984+
Ok(other) => {
2985+
panic!("Expected DuplicatePayment error on retry, got: {:?}", other);
2986+
},
2987+
Err(other) => {
2988+
panic!("Expected DuplicatePayment error on retry, got: {:?}", other);
2989+
},
2990+
}
2991+
}
2992+
29152993
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
29162994
async fn lsps2_client_service_integration() {
29172995
do_lsps2_client_service_integration(true).await;

0 commit comments

Comments
 (0)