#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.
#1057 adds a
Failedtransition 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 pastANTI_REORG_DELAYwhile no candidate can still confirm, the record is markedFailed(fail_funding_payment_lost_to_conflictinsrc/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
Pendingforever. 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'sFailedtransition, so they appear once it lands.1. Non-funding records have no failure transition
A plain send or receive leaves
Pendingonly when its own transaction reachesANTI_REORG_DELAYdepth and is markedSucceeded; if a double-spend confirms instead, the record staysPending. #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 recordFailedand 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
Failedtransition 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 aTxReplacedevent 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
TxDroppedevent, which names no conflicting txid. When the close later appears and confirms, sync emits nothing for the already-evicted splice, so theFailedtransition 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.