fix(utxo): ReadUtxo + DisconnectBlock reconstruct UTXOs from txindex.vSpent

ReadUtxo (src/txdb-base.cpp) lacked the lazy-fallback path that HaveUtxo
already had. When the UTXO snapshot is incomplete (as Sami reported) or
the chain DB was migrated incompletely, ReadUtxo returns false even
though the output is actually unspent on chain — blocks spending those
outputs get rejected with 'input not found', and the chain stalls.

GLM-5.2 and DeepSeek-V4-Pro independently identified this as the
primary sync staller when auditing the chain freeze at block 2,224,763.

Fix: when UTXO DB doesn't have the entry but txindex.vSpent[n] is null
(output was never spent), read the transaction from disk and reconstruct
the full CUtxoEntry (value, script, flags, tx time) plus the exact
block height via mapBlockIndex lookup.

DisconnectBlock (src/main.cpp) had the same nHeight=0 approximation in
the restore-input path; applied the same height-reconstruction pattern
for consistency.

Validation safety: every block 0 to 2,224,763 that successfully connected
on the live chain did so via the UTXO DB entry written by ConnectBlock
at the time. This fallback only activates when the UTXO DB entry is
MISSING, which cannot happen for any block that ever validated. Zero
historical block validation changes.
This commit is contained in:
Krystie
2026-08-03 02:07:47 -07:00
parent 0411be6ff0
commit 761d1d2b15
2 changed files with 74 additions and 1 deletions
+14 -1
View File
@@ -2174,11 +2174,24 @@ bool CBlock::DisconnectBlock(CTxDBBase& txdb, CBlockIndex* pindex)
const CTxOut& prevout = txPrev.vout[txin.prevout.n];
CUtxoEntry utxo;
utxo.nValue = prevout.nValue;
utxo.nHeight = 0; // approximation; exact height not critical for restored UTXOs
utxo.scriptPubKey = prevout.scriptPubKey;
utxo.fCoinBase = txPrev.IsCoinBase();
utxo.fCoinStake = txPrev.IsCoinStake();
utxo.nTxTime = txPrev.nTime;
// Reconstruct exact height via block index lookup.
// Falls back to 0 if mapBlockIndex doesn't have the
// tx's block yet (safe — ConnectInputs maturity
// check then requires COINBASE_MATURITY confirmations).
utxo.nHeight = 0;
CBlock blockHeader;
if (blockHeader.ReadFromDisk(txindex.pos.nFile, txindex.pos.nBlockPos, false))
{
auto bmi = mapBlockIndex.find(blockHeader.GetHash());
if (bmi != mapBlockIndex.end())
utxo.nHeight = bmi->second->nHeight;
}
txdb.WriteUtxo(txin.prevout.hash, txin.prevout.n, utxo);
}
}