Skip to content

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

Open
elnafateh wants to merge 5 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 5 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
log_error!(self.logger, "Failed to send BOLT11 invoice: DuplicatePayment. This is part of a unified payment. Aborting to avoid duplicate payment.");
return Err(Error::DuplicatePayment);
},
Err(e) => {

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.

It looks like this error can be just a persistence error happening in send/send_internal, with the payment being initiated. Fall back would be a duplicate payment.

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.

No, persistence failures return a separate PersistenceFailed variant, so aborting on DuplicatePayment here is safe.

@joostjager joostjager Aug 18, 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.

PersistenceFailed is the actual concern. It is caught by the Err(e) branch, and then leads to fallback, even though the payment might already have been initiated?

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.

I understand your concern here, but that's a real pre-existing bug. It's also orthogonal to this PR, so I'll file it as a second follow-up rather than widen this change, as this PR is scoped to #1033.

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.

It seemed similar enough to me to fix here too. But indeed, this PR is an improvement on its own ofc. Can you post the follow-up issue here too?

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.

Actually I now see the original issue cannot be closed with this PR, because it says:

"It may also be worth reviewing other Lightning errors and separating them into:

Errors for which fallback is safe.
Errors indicating that a payment already exists or may have been initiated, for which fallback must stop."

Maybe worth seeing if that's just a few more lines vs a bigger fix?

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.

I've audited every error the BOLT11 and BOLT12 legs of UnifiedPayment::send can surface.

There are only two that indicate a payment was already initiated (or may have been): DuplicatePayment and PersistenceFailed.

Now both errors surface after pay_for_bolt11_invoice / pay_for_offer returns Ok, i.e. once the ChannelManager has the payment in-flight.

Every other error (PaymentSendingFailed, InvalidInvoice, route failures, etc.) is returned before that call succeeds, so falling back to on-chain is safe. So there are only two terminal errors, the rest safe.

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.

here's how i want to resolve them errors:

  • I want to keep the BOLT11 DuplicatePayment impl and also fold the PersistenceFailed terminal arm there, so the BOLT11 leg is fully handled.
  • A follow-up issue/PR covers the BOLT12 leg (DuplicatePayment + PersistenceFailed). WDYT?

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.

Sounds good. That fully addresses the original issue. Curious though how much bolt12 is. If that is similarly minimal perhaps it can all be one PR, but up to you.

@elnafateh elnafateh Aug 20, 2026

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.

the change is essentially the same, but I'd like to keep it separate.

I also have updated #1060 for Bolt12.

@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
…ayments

Error::DuplicatePayment is now terminal in UnifiedPayment::send, preventing
a duplicate Lightning payment from falling back to an on-chain payment.
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
The expect_payment_successful_event! macro takes a bare PaymentId, not an
Option<PaymentId>. Adjust the lightningdevkit#1033 regression test accordingly so it
compiles against current upstream/main.
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?

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