-
Notifications
You must be signed in to change notification settings - Fork 50
Wallet Transaction Conflict Tracking
The wallet uses heuristics to distinguish between unconfirmed, conflicted, and abandoned transactions. Transaction states are updated based on mempool and block connected notifications, abandontransaction
calls, and the wallet's incomplete internal database of transactions, which tracks different transactions spending the same outputs (mapWallet
, mapTxSpends
).
This page exists to document ideas and requirements for improving transaction state heuristics.
- Transaction states
- Previous bugs
- Ideas for improvement
- Ideas for test coverage
- Ideas for refactoring
- History
Wallet transactions are represented by CWalletTx
objects. A transaction can be confirmed in a block, or unconfirmed and in the mempool, or unconfirmed and not in the mempool because it conflicts with a block transaction, or unconfirmed and not in the mempool because it conflicts with a mempool transaction, or unconfirmed and not in the mempool for some other reason.
If a transaction isn't in the mempool, the wallet can't reliably determine if it conflicts with something, but it tries to guess if it conflicts with a confirmed block transaction, and store what the earliest conflicting block seems to be. The wallet currently treats MemPoolRemovalReason::CONFLICT
transactions that conflict with the mempool as unconfirmed, not conflicted. This can cause inputs to these transactions to misleadingly appear as spent and wallet balances be lower, so the wallet provides an abandontransaction
RPC to manually exclude these transactions and ignore them. abandontransaction
doesn't need to be called very often because most of the time, a conflicting transaction will spend the same or more inputs as the original transaction, so there is no need to intervene and mark any unspent. abandontransaction
doesn't affect the way transaction outputs are treated because outputs from transactions that are not in the mempool are already not trusted for spending or included in balances.
Transaction state is serialized and persisted CWalletTx::hashBlock
and CWalletTx::nIndex
fields with the following interpretation:
uint256 hashBlock |
int nIndex |
interpretation |
---|---|---|
0 | 0 | unconfirmed |
>1 | >=0 | confirmed |
>1 | -1 | conflicted since #7105, unconfirmed prior |
1 | -1 | abandoned since #7312, unconfirmed prior |
These bugs have now been fixed by #27145, but could still use additional test coverage and further internal consistency checking.
-
bug-stale-conflicted – After a reorg, transactions that are no longer conflicted will stay in the conflicted state until the next time the wallet is loaded, because
blockDisconnected
notification code was never written to handle this case. A possible fix there would be to mark all conflicted transactions as unconflicted if their conflicting block height is greater or equal to the disconnect block height (idea-disconnect-conflicted). This idea would just make state updates that will already happen on the next reload happen earlier. An alternative fix would be to rerun wallet conflict detection logic on transactions in disconnected blocks: idea-refresh-conflicted. It could make sense to implement one or both of these fixes. The first fix would be more general and future proof, but the second fix would also be logical could have better peformance. -
bug-reorg-depth – As a consequence of the bug-stale-conflicted issue above, after a reorg, transactions that are no longer conflicted will return meaningless and usually incorrect
GetDepthInMainChain
values. This began with0ff0387
from #15931, because the safeguard ingetBlockDepth
that returned 0 when the block is not part of the active chain was bypassed. The correct value to return is 0 (unconfirmed), but right now the implementation returns negative (conflicted) values if the current proof of work tip has a greater or equal height as the originally conflicting block, or positive (confirmed) values if current tip is shorter than the originally conflicting block height. It does return the correct 0 (unconfirmed) value by accident if the current tip height is exactly one less than the originally conflicting block height. Either of the idea-disconnect-conflicted or idea-refresh-conflicted fixes below for the bug-stale-conflicted issue above would fix this bug as well, but ideally this bug would also be directly addressed and tested as described idea-reorg-depth. After40ede99
this bug is less severe and fixes itself when the wallet is reloaded. But until the wallet is reloaded, this bug can cause transaction inputs that should be considered spent to be spendable, and transactions that should be possible to abandon not possible to abandon. -
bug-reorg-spent – As a consquence of the bug-stale-conflicted issue above, after a reorg, inputs of transactions that are no longer conflicted may be considered spent when they are not spent, due to
CWalletTx
balance caching andMarkInputsDirty
never being called on the now-unconfirmed transactions. This problem was originally reported in #7315 but was made worse later by bug-reorg-depth. Either of the idea-disconnect-conflicted or idea-refresh-conflicted fixes below for the bug-stale-conflicted issue above would fix this bug as well, but it could also be addressed as described in idea-disconnect-inputs by marking all inputs of reorged transactions dirty. It would also be good to write an independent test for this bug that passes when this bug is fixed but bug-stale-conflicted is not fixed, so there is test coverage of balance caching logic being reliable even if conflict detection logic is broken. It could also be good to have checks running in debug builds that are skipped in release builds to verifyCWalletTx
cached balances are always correct.
-
idea-disconnect-conflicted – When a block is disconnected, it would be good to loop over all conflicted transactions with conflicted block height greater or equal to the disconnected block height and mark them as unconfirmed instead of conflicted. This would fix bug-stale-conflicted, bug-reorg-depth, and bug-reorg-spent bugs above. If this is fixed, good test coverage should check not only that transaction is no longer marked conflicted, but also check that
MarkInputsDirty
is called on the transaction (test should fail if it is not called), so its inputs are considered unspent even if they were cached as spent previously. -
idea-disconnect-inputs – Suggestion from #7315 to call
MarkInputsDirty
on all block transactions in theblockDisconnected
handler regardless of whetherAddToWalletIfInvolvingMe
returns true or false, instead of only when it returns true. This would fix the bug-reorg-spent issue above, but would probably be overkill because other fixes are possible and because it is only technically necessary to mark inputs of transactions dirty when the transactions switch between conflicted and unconflicted states, not when they switch between confirmed and unconfirmed states. -
idea-refresh-conflicted – [Implemented in #27145] When a transaction from a disconnected block is removed, it would be good to refresh other transactions sharing same inputs and mark them as unconfirmed if they are conflicted. Commit
3c8bd68
from #17543 implemented this but the change was not merged. This change would fix the same bugs as the idea-disconnect-conflicted change and should be accompanied by the same suggested new test coverage if it is picked up. This change could be implemented as a faster alternative to that change, or both changes could be implemented together in a way that lets the wallet do more internal consistency checking. -
idea-reorg-depth – To fix bug-reorg-depth,
GetDepthInMainChain
could return 0 instead of -1 if a conflicting block is not in the active chain. Or after bug-stale-conflicted is fixed, it could just assert that the block hash must be in the active chain and that the block height value is valid and less than or equal to the last block processed height. Checking if the block is in the active chain is relatively expensive so might make sense to skip in release builds and only do in debug builds. Ideally a fix for this issue would include test coverage for all three cases where formerly conflicted transactions currently return negative, positive, and 0 depth values. -
idea-abandon-bumped – It might make sense to mark bumpfee replaced transactions as abandoned, so their inputs will no longer be considered spent. There is not a need right now because replacement transactions are covered by conflict tracking and mempool checks, and spend all the same inputs as the original transactions anyway.
-
idea-sticky-abandoned – Conflicted and abandoned states shouldn't be exclusive. Because they are exclusive currently, the abandoned state takes precedence over the conflicted state. This is so if there is a reorg and an abandoned and previously-conflicted transaction becomes no longer conflicted, it will remain abandoned, and won't be rebroadcast, or spend its inputs, or affect the wallet balance. But more ideally it would be possible to see if a transaction is conflicted even if it is also abandoned. Having a stickier abandoned state might be useful in other cases. For example a
blockDisconnected
comment suggests continuing to treat a transaction abandoned if it is confirmed and then reorged out without being added back to the mempool. This is a narrower corner case, though. -
idea-mempool-conflicted – Instead of only considering transactions that conflict with block transactions as conflicted, the wallet could also treat transactions that conflict with mempool transactions as conflicted, rather than unconfirmed. Reasons this is not done currently are noted in a
transactionRemovedFromMempool
code comment. An additional reason might be related to #7315: as long as the conflicted transaction is kept in an unconfirmed state instead of the conflicted state, its other nonconflicting inputs are not considered spendable, which could be safer for coin selection. On the other hand, treating these transactions as conflicted could remove some cases where it's neccessary to use theabandontransaction
RPC, and might even make it possible to eliminate the RPC entirely.bdf40f3
from #18600 was an attempt to implement mempool conflict detection by treating mempool conflicts the same as block conflicts. But it did not add code to detect transactions becoming no longer conflicted. And probably it would make more sense to treat block and mempool conflicts differently, perhaps just tracking mempool conflicts in-memory, instead of trying to serialize mempool conflict state to disk. #18600 also went further and exposed conflict state in RPCs and sent new-walletnotify
notifications for mempool conflicted transactions. -
idea-more-conflicts – validation code's block connection logic is able to identify conflicts between transactions that the wallet conflict tracking code cannot always detect due to not having a complete view of all transaction inputs. Before
cdb8934
from #17477, lists of conflicting transactions were passed to the walletblockConnected
handler, and in commita31be09
from #16624 these were marked as conflicted. But this change was temporary (immediately reverted in the following commit) and the change also had a bug because it did not record the conflicting block hash. If desired though, it would be possible to restore some of this code to mark more transactions conflicted. This could help avoid the need to useabandontransaction
in the case of indirect conflicts. However, it is probably not worth complexity to node and wallet code to implement this. It would also not be safe to implement this without first implementing the idea-disconnect-conflicted fix to mark these transactions unconflicted if the conflicting block is disconnected. #8692 has some older notes on this. The history section below covers the history of this code.
A top priority for improvement, and a requirement for any changes should be adding more test coverage. Existing code has poor coverage and will pass tests in many cases even if it's deliberately broken or has pieces removed. It would be possible to add lower-level C++ tests or higher-level python tests. Best would be high level python tests exhaustively covering different mempool and chain states and block and transaction notification sequences, verifying that transactions get updated to expected states, and that states result in expected spending and balance behavior. But even just adding tests for existing bugs would be a big improvement to prevent recurring problems and regressions such as #18878. Testing ideas:
-
It would be good to add test coverage for cases described in previous bugs and ideas for improvement sections above.
-
It would be good to add test coverage for the previously fixed offline reorg bug
40ede99
from #16624, which doesn't have any coverage currently. Particularly all tests seem to pass currently if theLoadToWallet
wtx.isConflicted()
condition is removed. -
It would be good to add test coverage for
LoadToWallet
conflict detection code which was almost accidentally removed incd15809a
from #17543 -
It would be good to enumerate ways transactions can transition between states and check they are handled correctly:
- Check that when a wallet transaction is added to a block, it is marked confirmed.
- Check that when directly conflicting transactions are added to a block, wallet transactions are marked conflicted.
- Check that when indirectly conflicting transactions are added to a block, wallet transactions are marked as not in the mempool and remain unconfirmed or abandoned.
- Check that when a confirmed wallet transaction is in a disconnected block and doesn't conflict, it is marked unconfirmed.
- Check that when a confirmed wallet transaction is in a disconnected block and there is a direct conflict, it is marked conflicted and not in the mempool.
- Check that when a confirmed wallet transaction is in a disconnected block and there is an indirect conflict, it is marked unconfirmed and not in the mempool.
- Check that when an unconfirmed transaction is added or removed from the mempool, it stays unconfirmed, but balances update because its outputs become trusted or untrusted.
- Check that when an abandoned transaction is added to mempool, it becomes unconfirmed instead of abandoned.
- Check that when a conflicted transaction is added to the mempool, it becomes unconfirmed instead of conflicted. (Or if idea-disconnect-conflicted or idea-refresh-conflicted ideas are implemented, add an assert to verify this case is impossible.)
- Check that when an abandoned transaction is detected to be conflicted it remains abandoned.
Existing wallet_txn_doublespend.py, feature_notifications.py, and wallet_conflicts.py tests cover some of these cases and show test setup that can be used to reproduce them.
- It would be good to improve
CWalletTx
code quality to check at runtime that inconsistent states are impossible to set, or change the data structure so invalid states have a different representation than valid states and there is no way to set invalid states accidentally. A great first step would be to eliminiate the storedCWalletTx::Confirmation::status
field and replace it with a method to return transaction status on the fly. Better would be to eliminate the confirmation struct entirely and replace it with unambiguous sum (variant) type that clearly separates the different possible states and state attributes. #18600 (comment) breaks down all the possible valid and invalid states and would be a good starting point for a variant definition.
The conflicted state was first added in #7105 and the abandoned state was added in #7312. Various changes and improvements have been made since then:
-
PR #7105 – Track wallet conflicts instead of using mempool – sipa – Dec 2015
PR marking wallet transactions that spend the same inputs as conflicted and storing conflicted status in serializedCWalletTx
objects, instead of treating all unconfirmed transactions not in the mempool as conflicted. -
PR #7306 – Make conflicted tx's update balances – morcos – Jan 2016
PR marking conflicted transaction inputs dirty to recompute balances correctly after #7105. -
PR #7312 – Add abandontransaction RPC – morcos – Jan 2016
PR addingabandontransaction
RPC to mark transactions and store abandoned state in serializedCWalletTx
objects so inputs used by transactions that are not in the mempool can be spendable again after #7105. -
Issue #7315 – Minor wallet issue with conflicted txs – morcos – Jan 2015
Bug report for bug-reorg-spent problems caused by #7105 when transactions marked conflicted are no longer conflicted after a reorg. -
Issue #8692 – Marking chains of txs conflicted properly – morcos – Sep 2016
Notes on block connected conflict code first added in #3694, mistakenly thought to be unnecessary after #7105, then removed in #9240, then added back again in #9371, then accidentally removed again in commit7e899941
from #16624, and added back again with more changes in #18982. Ideas for improving this are discussed idea-more-conflicts. -
PR #9240 – Remove txConflicted – morcos – Dec 2016
PR implementing a simplification/optimization to stop callingSyncTransaction
for conflicted transactions when a new block is connected, since this was assumed to be no longer necessary after #7105 (http://www.erisian.com.au/bitcoin-core-dev/log-2016-09-09.html#l-322). However, removing this turned out to drop important-walletnotify
notifications, so it was added back with some changes in #9371. -
Issue #9479 – -walletnotify isn't fired TheBlueMatt – Jan 2017
Bug caused by PR #9240 (comment), http://www.erisian.com.au/bitcoin-core-dev/log-2016-12-14.html#l-195. -
PR #9371 – Notify on removal – morcos – Jan 2017
PR adding back-walletnotify
calls for conflicted transactions on block connection with newMemPoolConflictRemovalTracker
helper. -
PR #9725 – CValidationInterface cleanups – TheBlueMatt – Apr 2017
PR moving transaction sync calls from validation code to the wallet without changing behavior (commit461e49f
). -
PR #10179 – CValidationInterface notifications on CScheduler Thread – TheBlueMatt – Jul 2017
PR making wallet notifications asynchronous. -
PR #10286 – Call wallet notify callbacks in scheduler thread – TheBlueMatt – Nov 2017
PR addingCWalletTx::fInMempool
cache field, setting and using it. -
PR #15931 – Remove GetDepthInMainChain dependency on locked chain interface – ariard – May 2019
PR changingGetDepthInMainChain
and other wallet code to use most recent updated block instead of chain tip to avoid the need for locking the chain to return consistent results.bug-reorg-depth bug was introduced commit
0ff0387
where transactions that were formerly conflicted but then unconfirmed after a reorg would still be treated as conflicted. Later commit40ede99
in #16624 made this problem less severe by fixing it when the wallet was reloaded, but bugs can still occur after the reorg until the reload. -
PR #16624 – Encapsulate transactions state – ariard – Sep 2019
This PR was intended to be a refactoring, but commit7e899941
unintentionally caused conflict notifications to be lost, as later reported in #18325 and fixed in #18982. The commit was an attempt to simplify transaction state logic, made without realizing the point of the removedSyncTransaction
call was actually to send external notifications, not to update transaction state. The purpose of the code at that point was less clear than when it was originally added in4afbde60
from #9371.Commit
40ede99
from this PR was also notable because it made effects of the bug-stale-conflicted and bug-reorg-depth issues described above less severe. After this commit, wallet transactions no longer conflicted after a reorg stop being treated as conflicted the next time the wallet is reloaded. However, they are still mistakenly treated as conflicted until it is reloaded. -
PR #17477 – Remove mempool NotifyEntryAdded and NotifyEntryRemoved – jnewbery – Nov 2019
PR droppingvtxConflicted
information no longer used after #16624. -
PR #17543 undo conflicts properly in case of blocks disconnection – ariard – Nov 2019
Up for grabs PR implementing idea-refresh-conflicted change in commit3c8bd681
along with other cleanups. Might make sense to split into separate PRs and could use some more tests. -
PR #18600 – Track conflicted transactions removed from mempool – ariard – Apr 2020
Up for grabs PR originally intended to fix-walletnotify
bug reported #18325 caused by7e899941
from #16624. But the PR also goes further, implementing a version of the idea-mempool-conficted change described above, and exposing a new RPC conflicted field. -
PR #18878 Add test for conflicted wallet tx notifications – ryanofsky – May 2020
PR adding test for-walletnotify
bug reported #18325 caused by7e899941
from #16624. -
PR #18982 Minimal fix to restore conflicted transaction notifications – ryanofsky – June 2020
PR implementing fix for-walletnotify
bug reported #18325 caused by7e899941
from #16624. This PR effectively just reverts the buggy commit, but had to take a more complicated approach because of mempool changes made in the interim. Like the previous conflict notification code, which was originally added in4afbde60
from #9371 the new code carefully sends notifications about conflicted transactions before notifications about connected transactions, to avoid a race condition where connected notifications makeabandontransaction
calls on conflicted transactions, but the abandoned states get thrown away bySyncTransaction
calls on the conflicted transactions, due toCWalletTx
merging code that merges unconfirmed transactions into abandoned transactions as unconfirmed transactions. In the new code, the race is avoided by relying onTransactionRemovedFromMempool
notifications being received beforeBlockConnected
notifications. -
PR #27145 wallet: when a block is disconnected, update transactions that are no longer conflicted – ishaanam – May 2023
This PR picks up #17543 and implements idea-refresh-conflicted. This PR fixes bug-stale-conflicted, bug-reorg-depth, and bug-reorg-spent.