Skip to content

Fix BOLT11 DuplicatePayment triggering on-chain fallback in unified payment - #1038

Open
elnafateh wants to merge 2 commits into
lightningdevkit:mainfrom
elnafateh:fix/unified-payment-duplicate-fallback
Open

Fix BOLT11 DuplicatePayment triggering on-chain fallback in unified payment#1038
elnafateh wants to merge 2 commits into
lightningdevkit:mainfrom
elnafateh:fix/unified-payment-duplicate-fallback

Conversation

@elnafateh

Copy link
Copy Markdown
Contributor

UnifiedPayment::send previously fell back to the on-chain method after any
BOLT11 error, including Error::DuplicatePayment. Retrying a unified BIP21
payment could pay the recipient twice — once over Lightning, once on-chain.

Error::DuplicatePayment is now treated as terminal and returned to the
caller immediately, preventing the unsafe fallback.

Adds an integration test covering the retry scenario.
#1033

@ldk-reviews-bot

ldk-reviews-bot commented Aug 10, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @joostjager as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@ldk-reviews-bot
ldk-reviews-bot requested a review from tnull August 10, 2026 15:10
@elnafateh
elnafateh force-pushed the fix/unified-payment-duplicate-fallback branch from d2e30c9 to 981bc8a Compare August 10, 2026 21:30
Comment thread src/payment/unified.rs
@elnafateh
elnafateh requested a review from ajaysehwal August 11, 2026 20:46
Comment thread src/payment/unified.rs
@elnafateh
elnafateh requested a review from joostjager August 20, 2026 10:43
@joostjager
joostjager requested review from ajaysehwal and removed request for ajaysehwal and joostjager August 20, 2026 11:59
elnafateh added a commit to elnafateh/ldk-node that referenced this pull request Aug 20, 2026
In `UnifiedPayment::send`, the BOLT11 leg's `bolt11_invoice.send` only
returns `Err(PersistenceFailed)` *after* `pay_for_bolt11_invoice` has
already succeeded and the Lightning payment is in-flight. The previous
match treated every error (via `Err(e)`) as a fall-through to the next
payment method, so a persistence failure after initiation would
broadcast an on-chain transaction for the same URI — a duplicate
payment.

We now treat `Err(Error::PersistenceFailed)` on the BOLT11 leg as
terminal, mirroring how `DuplicatePayment` is already handled, and abort
the unified payment instead of falling back to on-chain.

This is a regression hazard raised during review of the lightningdevkit#1033 fix
(PR lightningdevkit#1038). It is pre-existing and orthogonal to lightningdevkit#1033 (which only made
`DuplicatePayment` terminal); tracked separately as the unified variant
of the broader post-commit persistence hazard.

Adds `unified_send_bolt11_persistence_failure_no_onchain_fallback`, which
arms a failing payment-store write on a `KVStore`-backed node and asserts
that `send` returns `PersistenceFailed` without recording any on-chain
payment.

Co-Authored-By: Claude <noreply@anthropic.com>
@elnafateh
elnafateh force-pushed the fix/unified-payment-duplicate-fallback branch from 981bc8a to 678e1bc Compare August 20, 2026 21:42
elnafateh added a commit to elnafateh/ldk-node that referenced this pull request Aug 20, 2026
In `UnifiedPayment::send`, the BOLT11 leg's `bolt11_invoice.send` only
returns `Err(PersistenceFailed)` *after* `pay_for_bolt11_invoice` has
already succeeded and the Lightning payment is in-flight. The previous
match treated every error (via `Err(e)`) as a fall-through to the next
payment method, so a persistence failure after initiation would
broadcast an on-chain transaction for the same URI — a duplicate
payment.

We now treat `Err(Error::PersistenceFailed)` on the BOLT11 leg as
terminal, mirroring how `DuplicatePayment` is already handled, and abort
the unified payment instead of falling back to on-chain.

This is a regression hazard raised during review of the lightningdevkit#1033 fix
(PR lightningdevkit#1038). It is pre-existing and orthogonal to lightningdevkit#1033 (which only made
`DuplicatePayment` terminal); tracked separately as the unified variant
of the broader post-commit persistence hazard.

Adds `unified_send_bolt11_persistence_failure_no_onchain_fallback`, which
arms a failing payment-store write on a `KVStore`-backed node and asserts
that `send` returns `PersistenceFailed` without recording any on-chain
payment.
@elnafateh
elnafateh force-pushed the fix/unified-payment-duplicate-fallback branch from 678e1bc to 9c2d37c Compare August 20, 2026 21:54
@elnafateh
elnafateh requested a review from joostjager August 21, 2026 10:22

@joostjager joostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to make sure each commit compiles, passes tests and is rustfmt'ed.

}
}

impl PaginatedKVStore for PaymentFailingStore {

@joostjager joostjager Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of test code added. Isn't there a more compact way to cover this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! Extracted the shared node and collapsed the duplicate arms.

…ayments

UnifiedPayment::send previously treated any error from the BOLT11 leg
of a unified payment as non-terminal and fell through to the on-chain
payment method. This meant a retried BOLT11 payment that returns
Error::DuplicatePayment would still result in an on-chain transaction
being broadcast for the same invoice — a duplicate payment.

Error::DuplicatePayment is now terminal in UnifiedPayment::send: the
unified payment aborts instead of falling back to on-chain.

Fixes lightningdevkit#1033.

Adds a regression test, unified_send_bolt11_duplicate_payment_no_onchain_fallback,
along with fund_and_open_ready_channel(), wait_for_node_announcement(),
and receive_bolt11_only_uri() test helpers reused by the PersistenceFailed
regression test in the next commit.
In UnifiedPayment::send, the BOLT11 leg's bolt11_invoice.send only
returns Err(PersistenceFailed) *after* pay_for_bolt11_invoice has
already succeeded and the Lightning payment is in-flight. The previous
match treated every remaining error as a fall-through to the next
payment method, so a persistence failure after initiation would
broadcast an on-chain transaction for the same URI — a duplicate
payment.

Err(Error::PersistenceFailed) on the BOLT11 leg is now terminal,
mirroring how DuplicatePayment is handled, and aborts the unified
payment instead of falling back to on-chain.

This is a regression hazard raised during review of the DuplicatePayment
fix (PR lightningdevkit#1038). It is pre-existing and orthogonal to lightningdevkit#1033; tracked
here as the unified variant of the broader post-commit persistence
hazard.

Adds PaymentFailingStore, a KVStore wrapper that fails writes to the
payments namespace on demand, and a regression test,
unified_send_bolt11_persistence_failure_no_onchain_fallback, which
arms it and asserts send() returns PersistenceFailed without recording
an on-chain payment. Reuses the fund_and_open_ready_channel(),
wait_for_node_announcement(), and receive_bolt11_only_uri() helpers
from the previous commit.
@elnafateh
elnafateh force-pushed the fix/unified-payment-duplicate-fallback branch from 9c2d37c to 93c18b3 Compare August 22, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants