Skip to content

On-chain payment records can stay Pending forever when their transaction is double-spent #1078

Description

@jkczyz

#1057 adds a Failed transition for funding payments (channel opens and splices). A funding record's candidate history is its funding transaction and any RBF replacements; once wallet sync records a conflict from outside that history and the conflict confirms past ANTI_REORG_DELAY while no candidate can still confirm, the record is marked Failed (fail_funding_payment_lost_to_conflict in src/wallet/mod.rs). That covers the case found in review (thanks @Jolah1): a cooperative close double-spending a pending splice's shared input.

Three cases still leave a record at Pending forever. No funds are at risk in any of them — the outcome is decided on chain; the record just misreports it. Case 1 exists on main today; cases 2 and 3 are limits of #1057's Failed transition, so they appear once it lands.

1. Non-funding records have no failure transition

A plain send or receive leaves Pending only when its own transaction reaches ANTI_REORG_DELAY depth and is marked Succeeded; if a double-spend confirms instead, the record stays Pending. #1057 covered only funding records because a funding payment can only complete through one of its candidates, so once a conflict from outside that list is buried and no candidate can still confirm, the payment can never complete. A non-funding payment could still complete through a new transaction spending different inputs, so the open question is what a confirmed conflict should mean here: mark the record Failed and treat any re-send as a new payment, or let the record follow a replacement transaction the way funding records follow their candidates.

2. Esplora/Electrum never see a force-close commitment

The Failed transition fires only if wallet sync recorded the conflict, and BDK records conflicts only between transactions in its graph. The Bitcoin Core backend inserts conflicting mempool and block transactions into the graph, so a force-close commitment shows up and the splice gets a TxReplaced event naming it. Esplora and Electrum sync only the wallet's script histories: a cooperative close pays our shutdown script — a wallet address — and appears, but a force-close commitment pays no wallet output, so no conflict is ever recorded. A better fix probably skips the wallet entirely — LDK knows the channel closed, and failing its pending funding records from the closure event would cover every backend, plus case 3's channel-close variant.

3. Eviction before the conflict exists records no conflict

A splice can fall out of the mempool before its eventual conflict — say, a channel close — even exists; the eviction gets a TxDropped event, which names no conflicting txid. When the close later appears and confirms, sync emits nothing for the already-evicted splice, so the Failed transition never fires — on every backend, Bitcoin Core included. The closure-driven detection from case 2 covers the channel-close variant; a double-spend of a contributed input under this ordering would additionally need detection that checks the wallet's transactions for anything spending the record's inputs, rather than waiting for a sync event to report the replacement.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions