Summary
On a reorg, roll_back undoes spend markers with:
UPDATE wallet_state_utxos SET spent_height = NULL, spent_txid = NULL, spent_in_block = NULL WHERE spent_height > ?
but wallet_state_utxos.spent_height is never assigned a non NULL value anywhere in the codebase, so this statement can never match a row. A spend recorded in a block that is later rolled back is never undone.
Watch only UTXOs are not affected. watch_only_utxos.spent_height is written in watch_only.rs:762, so the equivalent rollback on the next line of roll_back works. The asymmetry suggests an oversight rather than a deliberate choice.
Where
Spends on own UTXOs are recorded as two separate columns, neither of which is spent_height:
spent_in_block, wallet_state_table.rs:427
spent_txid, wallet_state_table.rs:439
The only statements naming wallet_state_utxos.spent_height are the schema definition and the rollback UPDATE itself.
Impact
balance.rs:101 treats a UTXO as unspent only when spent_in_block.is_none(). After a reorg drops the block that spent it, the marker stays set, so the UTXO is permanently excluded from the balance and from input selection, even though it is spendable on chain. Recovering it presumably needs a rescan or a reimport.
This is more than a display defect, since it affects which funds the wallet believes it can spend.
Proposed fix
Write spent_height at the same point as spent_in_block. The value is already in hand there: UtxoBlockInfo carries block_height, which balance.rs:33 already reads. With the column populated, the existing rollback works as written and no change to roll_back is needed.
Open question
Worth deciding separately what should happen to a pending record whose confirming block is rolled back. roll_back calls try_clean_pending_by_utxo, which deletes pending rows for UTXOs confirmed above the rollback height, but that keys on outputs. A send whose inputs were spent in the rolled back block may not be covered by it.
References
wallet_state_table.rs:427, 439, 560-600, watch_only.rs:762, balance.rs:101.
Split out from #44.
Summary
On a reorg,
roll_backundoes spend markers with:but
wallet_state_utxos.spent_heightis never assigned a non NULL value anywhere in the codebase, so this statement can never match a row. A spend recorded in a block that is later rolled back is never undone.Watch only UTXOs are not affected.
watch_only_utxos.spent_heightis written inwatch_only.rs:762, so the equivalent rollback on the next line ofroll_backworks. The asymmetry suggests an oversight rather than a deliberate choice.Where
Spends on own UTXOs are recorded as two separate columns, neither of which is
spent_height:spent_in_block,wallet_state_table.rs:427spent_txid,wallet_state_table.rs:439The only statements naming
wallet_state_utxos.spent_heightare the schema definition and the rollback UPDATE itself.Impact
balance.rs:101treats a UTXO as unspent only whenspent_in_block.is_none(). After a reorg drops the block that spent it, the marker stays set, so the UTXO is permanently excluded from the balance and from input selection, even though it is spendable on chain. Recovering it presumably needs a rescan or a reimport.This is more than a display defect, since it affects which funds the wallet believes it can spend.
Proposed fix
Write
spent_heightat the same point asspent_in_block. The value is already in hand there:UtxoBlockInfocarriesblock_height, whichbalance.rs:33already reads. With the column populated, the existing rollback works as written and no change toroll_backis needed.Open question
Worth deciding separately what should happen to a pending record whose confirming block is rolled back.
roll_backcallstry_clean_pending_by_utxo, which deletes pending rows for UTXOs confirmed above the rollback height, but that keys on outputs. A send whose inputs were spent in the rolled back block may not be covered by it.References
wallet_state_table.rs:427, 439, 560-600,watch_only.rs:762,balance.rs:101.Split out from #44.