The PR branch HEAD was 89df798 at the time of this review club meeting.
Notes
A transaction is considered conflicted when one or more of its inputs has been spent by another
confirmed transaction. A conflicted transaction is marked with negative depth equal to the number of
confirmations on the conflicting transaction.
It’s possible for a transaction to have previously been in a block that used to be part of the
most-work chain but has since been reorged out.
The wallet keeps track of relevant transactions and their confirmation status. This information is
used to calculate the wallet’s balance(s). For example:
If a transaction that is 100 blocks deep in the most-work chain, the wallet can
reasonably include its UTXOs in the balance displayed to the user.
If a transaction conflicts with another transaction 100 blocks deep in the most-work chain,
the wallet can be equally sure that, even though the transaction may have a valid signature,
its UTXOs do not count towards the user’s balance.
If a transaction is unconfirmed and in the node’s mempool, the wallet should account for its
UTXOs, but not consider them as safe as confirmed ones.
The author has provided more notes on transaction states and their effects on balance calculation here.
Wallet Transaction Conflict
Tracking
across chainstate and mempool events is tricky. As described in Issue #7315,
when a block is disconnected, the wallet should be marking conflicted transactions as inactive, but
isn’t currently doing so. PR #27145 updates the behavior to mark
transactions that are no longer conflicting after a reorg as inactive.
What is the issue this PR addresses? Can you reproduce the problem described? (Hint: try running
wallet_conflicts.py on master. What error do you get?)
What are the different
states
a CWalletTx can be in?
Which, if any, of the TxStates are “final,” i.e. once a transaction reaches this
state, it will never change again?
Where in net_processing.cpp is CWallet::blockDisconnected() triggered to be executed (Hint: it is
not directly called. See
CValidationInterface)?
Which thread executes this wallet function?
What does RecursiveUpdateTxState() do and why is it “recursive”? What are its callsites?
(Before you grep, where do you think this function should be called?)
What is tested in wallet_conflicts.py? Can you think of any other cases that should be tested?
<abubakarsadiq> This PR address issue whereby if a block is disconnected, the state of all the transaction in the block that our node/wallet know will change to inactive and have 0 confirmations.
<josie> abubakarsadiq: I didn't actually verify what the behavior was before this PR, so you maybe correct. I'd say the main issue this PR attempts to address is *not* marking txs as inactive on blockDisconnect, when they should be marked inactive
<josie> unrelated question: did anyone get a chance to read the wiki or the gist linked in the notes? if not, I'd recommend it! I learned a lot about re-orgs and transaction states by reading them
<josie> SebastianvStaa, abubakarsadiq: yep! curious what you think about Unrecognized? I was trying to think of an example of an Unrecognized state and couldn't come up with one
<josie> abubakarsadiq: it's certainly possible to have external inputs in a tx, but I don't think this would apply here as the inputs would either be in a confirmed or unconfirmed state. if its confirmed the node definitely knows about it since it appears in a block
<josie> SebastianvStaa: correct! TxStateConfirmed is considered (increasingly) final the more confirmations it has. The PR specifically mentions 100 blocks as a number where a TxState is definitely considered final
<josie> abubakarsadiq: I'm not sure I follow your question? The conflicted transaction shouldn't be mine-able as it would be spending inputs that are already confirmed spent in the longest chain
<josie> Pins, SebastianvStaa: ah! so it seems we agree that TxConflicted is not really a "final" state. based on this PR, what state does a TxConflicted tx get updated to? (e.g on blockDisconnect)
<josie> so we have TxConfirmed (the tx in a block that is part of the heaviest chain), and TxInactive (txs that were at one point in a conflicting block, but that block is no longer part of the longest chain)
<josie> but an Inactive tx which spends inputs that were spent many blocks back by a different transaction I would definitely consider to be in a final state
<josie> SebastianvStaa: great link! thanks for sharing. https://obc.256k1.dev/ is also a great architecture overview, which might be slightly more up to date
<lightlike> as for q5: looks like ActivateBestChain() is called in various places in net_processing, which can lead to DisconnectTip() in validation being called, which then creates the BlockDisconnected() signal which is picked up by the wallet later.