mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-11 21:20:39 +02:00
75f64e50c6test: exercise node abort on UTXO deserialization failure (furszy)4652cd0d82txdb: detect UTXO deserialization errors via CDBWrapper::TryRead() (furszy)5dfbb91b6cdbwrapper: add TryRead() to distinguish errors from valid outcomes (furszy)f78834fac9test: add missing coverage for CDBWrapper::Read() errors (furszy) Pull request description: 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](8a8edc8d88/src/coins.cpp (L398-L411)) 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. ACKs for top commit: ajtowns: reACK75f64e50c6sedited: ACK75f64e50c6mzumsande: Code Review ACK [75f64e5](75f64e50c6) Tree-SHA512: 51b0114ea443544a2f1fbb8e63be6e1dff94d6f287221d566dbc98d666784a2b4c486acfb87eea5392bc1d092fb6d6dc0ff6782bcdccdcf15939281c895e384d