Early note: the majority of this PR consists of test coverage. The changes per se are small.
If a UTXO entry on disk can’t be deserialized, the node currently treats it as if the coin
wouldn’t exist instead of aborting with an error. A non-existing coin has a very specific
meaning for consensus: any block that spends it would be permanently rejected as invalid
(BLOCK_FAILED_VALID), silently forking the node from the rest of the network. This can’t
currently be triggered in practice (details below), but it’s still the wrong behavior.
The root cause is that CDBWrapper::Read() returns false for both missing entries and
deserialization failures, so CCoinsViewDB::GetCoin() has no way to tell them apart.
CCoinsViewErrorCatcher was built to catch database read errors and abort, but it never
fires during deserialization errors because CDBWrapper::Read() swallows the exception
before it can propagate. This comment in ExecuteBackedWrapper() spells out the code
intent very clearly.
As mentioned initially, this can’t happen in practice today. It would require either a bug
in the coin serialization path, or a memory corruption before the data reaches LevelDB
(at which point we have bigger problems). Random disk-level bit flips are caught earlier
by LevelDB’s verification (verify_checksums=true, enabled by default), which already
propagates correctly as DB_INTERNAL_ERROR. Regardless, a db read issue should
never be silently misinterpreted as a consensus violation.
This PR adds CDBWrapper::TryRead(), which returns a ReadStatus that lets callers
discriminate between all possible outcomes. CCoinsViewDB::GetCoin() switches on the
result and throws on any error, letting ExecuteBackedWrapper() do what it was designed
to do. CDBWrapper::Read() becomes a thin wrapper over TryRead(), preserving backward
compatibility for all other callers (so we don’t have to change non-consensus code here).
PeekCoin() is also covered, as it delegates to CCoinsViewDB::GetCoin() at the database
level.
The idea of the PR is to go slowly over the code changes, first commit locks-in the current
CDBWrapper::Read() behavior . The second adds TryRead() with tests for all four
status codes. The third is the CCoinsViewDB::GetCoin() fix. The fourth is a functional
that ensures the node aborts correctly instead of silently diverging.
Testing Notes:
Cherry-picking the functional test commit on master demonstrates the consensus split
when the coin entry fails to deserialize.
Extra Note:
CDBIterator::GetValue() has the same silent-swallow pattern. Not consensus-critical.
Should be addressed in a follow-up.