When dumptxoutset creates a rollback snapshot, the copy loop skips coins on read failure. The cursor catches the new decompression exception and returns false, so this PR
adds another trigger for that issue. Could we abort the copy on a failed read, either here or in a separate follow-up? Otherwise, dumptxoutset can succeed with a coin missing.
scantxoutset uses the same cursor reader, but a failed read currently looks like cancellation (success: false with partial results). Could we return the UTXO read error there as well?
The existing corruption test can cover both RPCs before triggering the validation abort.
<details>
<summary>Report unreadable coins in rollback exports and UTXO scans</summary>
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index ae75072462..27223dda57 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -2258,7 +2258,7 @@ bool FindScriptPubKey(std::atomic<int>& scan_progress, const std::atomic<bool>&
while (cursor->Valid()) {
COutPoint key;
Coin coin;
- if (!cursor->GetKey(key) || !cursor->GetValue(coin)) return false;
+ if (!cursor->GetKey(key) || !cursor->GetValue(coin)) throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
if (++count % 8192 == 0) {
interruption_point();
if (should_abort) {
@@ -3304,6 +3304,8 @@ UniValue CreateRolledBackUTXOSnapshot(
if (coins_count % 100'000 == 0) {
temp_cache.Flush();
}
+ } else {
+ throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
}
cursor->Next();
}
diff --git a/test/functional/feature_utxo_abort_on_error.py b/test/functional/feature_utxo_abort_on_error.py
index 5e69333c13..dbfcd0e262 100755
--- a/test/functional/feature_utxo_abort_on_error.py
+++ b/test/functional/feature_utxo_abort_on_error.py
@@ -22,7 +22,7 @@ except ImportError:
from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework, SkipTest
-from test_framework.util import assert_equal
+from test_framework.util import assert_equal, assert_raises_rpc_error
from test_framework.wallet import MiniWallet
@@ -87,6 +87,12 @@ class UTXOAbortOnErrorTest(BitcoinTestFramework):
# touches them. The node starts cleanly regardless of the tampered entry.
assert_equal(node0.getblockcount(), COINBASE_MATURITY + 1)
+ self.log.info("Rollback export must fail when it cannot read a coin")
+ assert_raises_rpc_error(-32603, "Unable to read UTXO set", node0.dumptxoutset, "rollback.dat", rollback=COINBASE_MATURITY)
+
+ self.log.info("UTXO set scan must fail when it cannot read a coin")
+ assert_raises_rpc_error(-32603, "Unable to read UTXO set", node0.scantxoutset, "start", ["raw(51)"])
+
# Now spend the corrupted UTXO
self.log.info("node1 broadcasts the spend and mines a block")
node1.sendrawtransaction(spend_tx_hex)
</details>