bitcoindevkit / bitcoindevkit/bdk
bdk_bitcoind_rpc Emitter fails to report evicted transactions after a reorg (Ghost Pending Payments)
- Dominant language
- Rust
- Stars
- 1.1k
- Forks
- 483
- Avg merge
- 20d 3h
- Merged PRs (30d)
- 3
Description
**Describe the bug**
There is a state synchronisation issue in the bdk_bitcoind_rpc crate where a transaction that is confirmed in a block, reorged out, and never returns to the mempool is never reported as evicted. This results in the wallet displaying the payment as "pending" indefinitely.
The Emitter tracks mempool state in mempool_snapshot and reports evictions by diffing the snapshot against getrawmempool. However, there are two gaps that make confirmed-then-vanished transactions untracked:
- When a block is emitted, its transactions are removed from mempool_snapshot because they are "confirmed" and no longer tracked as part of the mempool.
- On a reorg (PollResponse::AgreementFound), the popped block's transactions are not re-added to the snapshot.
As a result, if a transaction (T) was confirmed in an orphaned block and then vanishes (e.g., double-spent in the replacement chain, or dropped for another reason):
- It is never re-emitted as mempool-seen (since it's not in the mempool).
- It is never reported as evicted (since eviction set = snapshot minus mempool; T is in neither).
Downstream, T keeps its dangling anchor and its old seen_at. The Canonicalization process sends it to the leftover path (LeftOverTxs), which keeps it canonical as ObservedIn::Block(height) → resolved to ChainPosition::Unconfirmed with last_seen: None.
Result: T's outputs count toward the trusted_pending/untrusted_pending balances indefinitely, its spent wallet inputs stay locked, and no future Emitter event can ever update it.
**To Reproduce**
1. Transaction T pays the wallet 50,000 sats. The wallet sees it in the mempool (mempool()), then confirmed in block B1 (next_block()). The wallet marks it confirmed, and the Emitter drops T from mempool_snapshot.
2. Reorg: B1 is replaced by B2. B2 contains a conflicting double-spend of T's inputs, meaning T is permanently gone from both the chain and the mempool.
3. Emitter re-emits: The agreement walk finds the fork and emits B2. The wallet's LocalChain moves to B2 (B1 is invalidated).
4. Mempool at tip: The eviction set calculates as snapshot − mempool = ∅. T's disappearance is never reported to the wallet.
5. Wallet View: T is stuck as canonical-unconfirmed forever. The wallet incorrectly asserts confirmed = 0 and trusted_pending = 50_000.
**Expected behavior**
After the reorg where block B1 is replaced by B2, and transaction T is no longer present in the active chain or the mempool, the Emitter should successfully detect that T has vanished and emit an eviction event for it.
The wallet should process this eviction by removing T from the pending state, clearing it from the trusted_pending/untrusted_pending balances, and unlocking any previously spent inputs. In the scenario above, the final state should correctly assert confirmed = 0 and trusted_pending = 0.
**Build environment**
- BDK tag/commit: 337e9d68414c54d2ab74082bab94b58ac130479c
**Which backend(s) are relevant (if any)?**
- [ ] Electrum
- [ ] Esplora
- [x] Bitcoin Core RPC
- [ ] None / not backend-related (e.g. `bdk_chain`, `bdk_core`)
- [ ] Other (please specify): `____`
**Is this blocking production use?**
- [ ] Yes
- [x] No
Contributor guide
Assessment
This issue has not been assessed yet.