rpc: Return precise loadtxoutset error messages

The error messages should never happen in normal operation. However, if
they do, they are helpful to return to the user to debug the issue. For
example, to notice a truncated file.
This commit is contained in:
MarcoFalke 2024-07-22 12:21:14 +02:00
parent faa5c86dbf
commit fa530ec543
No known key found for this signature in database
3 changed files with 36 additions and 47 deletions

View file

@ -1,5 +1,5 @@
// Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2022 The Bitcoin Core developers // Copyright (c) 2009-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
@ -5754,9 +5754,9 @@ util::Result<void> ChainstateManager::ActivateSnapshot(
return util::Error{std::move(reason)}; return util::Error{std::move(reason)};
}; };
if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)) { if (auto res{this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)}; !res) {
LOCK(::cs_main); LOCK(::cs_main);
return cleanup_bad_snapshot(Untranslated("population failed")); return cleanup_bad_snapshot(strprintf(Untranslated("Population failed: %s"), util::ErrorString(res)));
} }
LOCK(::cs_main); // cs_main required for rest of snapshot activation. LOCK(::cs_main); // cs_main required for rest of snapshot activation.
@ -5821,7 +5821,7 @@ static void SnapshotUTXOHashBreakpoint(const util::SignalInterrupt& interrupt)
if (interrupt) throw StopHashingException(); if (interrupt) throw StopHashingException();
} }
bool ChainstateManager::PopulateAndValidateSnapshot( util::Result<void> ChainstateManager::PopulateAndValidateSnapshot(
Chainstate& snapshot_chainstate, Chainstate& snapshot_chainstate,
AutoFile& coins_file, AutoFile& coins_file,
const SnapshotMetadata& metadata) const SnapshotMetadata& metadata)
@ -5837,18 +5837,16 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
if (!snapshot_start_block) { if (!snapshot_start_block) {
// Needed for ComputeUTXOStats to determine the // Needed for ComputeUTXOStats to determine the
// height and to avoid a crash when base_blockhash.IsNull() // height and to avoid a crash when base_blockhash.IsNull()
LogPrintf("[snapshot] Did not find snapshot start blockheader %s\n", return util::Error{strprintf(Untranslated("Did not find snapshot start blockheader %s"),
base_blockhash.ToString()); base_blockhash.ToString())};
return false;
} }
int base_height = snapshot_start_block->nHeight; int base_height = snapshot_start_block->nHeight;
const auto& maybe_au_data = GetParams().AssumeutxoForHeight(base_height); const auto& maybe_au_data = GetParams().AssumeutxoForHeight(base_height);
if (!maybe_au_data) { if (!maybe_au_data) {
LogPrintf("[snapshot] assumeutxo height in snapshot metadata not recognized " return util::Error{strprintf(Untranslated("Assumeutxo height in snapshot metadata not recognized "
"(%d) - refusing to load snapshot\n", base_height); "(%d) - refusing to load snapshot"), base_height)};
return false;
} }
const AssumeutxoData& au_data = *maybe_au_data; const AssumeutxoData& au_data = *maybe_au_data;
@ -5857,8 +5855,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
// ActivateSnapshot(), but is done so that we avoid doing the long work of staging // ActivateSnapshot(), but is done so that we avoid doing the long work of staging
// a snapshot that isn't actually usable. // a snapshot that isn't actually usable.
if (WITH_LOCK(::cs_main, return !CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) { if (WITH_LOCK(::cs_main, return !CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) {
LogPrintf("[snapshot] activation failed - work does not exceed active chainstate\n"); return util::Error{Untranslated("Work does not exceed active chainstate")};
return false;
} }
const uint64_t coins_count = metadata.m_coins_count; const uint64_t coins_count = metadata.m_coins_count;
@ -5875,8 +5872,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
coins_per_txid = ReadCompactSize(coins_file); coins_per_txid = ReadCompactSize(coins_file);
if (coins_per_txid > coins_left) { if (coins_per_txid > coins_left) {
LogPrintf("[snapshot] mismatch in coins count in snapshot metadata and actual snapshot data\n"); return util::Error{Untranslated("Mismatch in coins count in snapshot metadata and actual snapshot data")};
return false;
} }
for (size_t i = 0; i < coins_per_txid; i++) { for (size_t i = 0; i < coins_per_txid; i++) {
@ -5888,14 +5884,12 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
if (coin.nHeight > base_height || if (coin.nHeight > base_height ||
outpoint.n >= std::numeric_limits<decltype(outpoint.n)>::max() // Avoid integer wrap-around in coinstats.cpp:ApplyHash outpoint.n >= std::numeric_limits<decltype(outpoint.n)>::max() // Avoid integer wrap-around in coinstats.cpp:ApplyHash
) { ) {
LogPrintf("[snapshot] bad snapshot data after deserializing %d coins\n", return util::Error{strprintf(Untranslated("Bad snapshot data after deserializing %d coins"),
coins_count - coins_left); coins_count - coins_left)};
return false;
} }
if (!MoneyRange(coin.out.nValue)) { if (!MoneyRange(coin.out.nValue)) {
LogPrintf("[snapshot] bad snapshot data after deserializing %d coins - bad tx out value\n", return util::Error{strprintf(Untranslated("Bad snapshot data after deserializing %d coins - bad tx out value"),
coins_count - coins_left); coins_count - coins_left)};
return false;
} }
coins_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin)); coins_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
@ -5915,7 +5909,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
// means <5MB of memory imprecision. // means <5MB of memory imprecision.
if (coins_processed % 120000 == 0) { if (coins_processed % 120000 == 0) {
if (m_interrupt) { if (m_interrupt) {
return false; return util::Error{Untranslated("Aborting after an interrupt was requested")};
} }
const auto snapshot_cache_state = WITH_LOCK(::cs_main, const auto snapshot_cache_state = WITH_LOCK(::cs_main,
@ -5933,9 +5927,8 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
} }
} }
} catch (const std::ios_base::failure&) { } catch (const std::ios_base::failure&) {
LogPrintf("[snapshot] bad snapshot format or truncated snapshot after deserializing %d coins\n", return util::Error{strprintf(Untranslated("Bad snapshot format or truncated snapshot after deserializing %d coins"),
coins_processed); coins_processed)};
return false;
} }
} }
@ -5955,9 +5948,8 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
out_of_coins = true; out_of_coins = true;
} }
if (!out_of_coins) { if (!out_of_coins) {
LogPrintf("[snapshot] bad snapshot - coins left over after deserializing %d coins\n", return util::Error{strprintf(Untranslated("Bad snapshot - coins left over after deserializing %d coins"),
coins_count); coins_count)};
return false;
} }
LogPrintf("[snapshot] loaded %d (%.2f MB) coins from snapshot %s\n", LogPrintf("[snapshot] loaded %d (%.2f MB) coins from snapshot %s\n",
@ -5980,18 +5972,16 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
maybe_stats = ComputeUTXOStats( maybe_stats = ComputeUTXOStats(
CoinStatsHashType::HASH_SERIALIZED, snapshot_coinsdb, m_blockman, [&interrupt = m_interrupt] { SnapshotUTXOHashBreakpoint(interrupt); }); CoinStatsHashType::HASH_SERIALIZED, snapshot_coinsdb, m_blockman, [&interrupt = m_interrupt] { SnapshotUTXOHashBreakpoint(interrupt); });
} catch (StopHashingException const&) { } catch (StopHashingException const&) {
return false; return util::Error{Untranslated("Aborting after an interrupt was requested")};
} }
if (!maybe_stats.has_value()) { if (!maybe_stats.has_value()) {
LogPrintf("[snapshot] failed to generate coins stats\n"); return util::Error{Untranslated("Failed to generate coins stats")};
return false;
} }
// Assert that the deserialized chainstate contents match the expected assumeutxo value. // Assert that the deserialized chainstate contents match the expected assumeutxo value.
if (AssumeutxoHash{maybe_stats->hashSerialized} != au_data.hash_serialized) { if (AssumeutxoHash{maybe_stats->hashSerialized} != au_data.hash_serialized) {
LogPrintf("[snapshot] bad snapshot content hash: expected %s, got %s\n", return util::Error{strprintf(Untranslated("Bad snapshot content hash: expected %s, got %s"),
au_data.hash_serialized.ToString(), maybe_stats->hashSerialized.ToString()); au_data.hash_serialized.ToString(), maybe_stats->hashSerialized.ToString())};
return false;
} }
snapshot_chainstate.m_chain.SetTip(*snapshot_start_block); snapshot_chainstate.m_chain.SetTip(*snapshot_start_block);
@ -6031,7 +6021,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
LogPrintf("[snapshot] validated snapshot (%.2f MB)\n", LogPrintf("[snapshot] validated snapshot (%.2f MB)\n",
coins_cache.DynamicMemoryUsage() / (1000 * 1000)); coins_cache.DynamicMemoryUsage() / (1000 * 1000));
return true; return {};
} }
// Currently, this function holds cs_main for its duration, which could be for // Currently, this function holds cs_main for its duration, which could be for

View file

@ -918,7 +918,7 @@ private:
//! To reduce space the serialization format of the snapshot avoids //! To reduce space the serialization format of the snapshot avoids
//! duplication of tx hashes. The code takes advantage of the guarantee by //! duplication of tx hashes. The code takes advantage of the guarantee by
//! leveldb that keys are lexicographically sorted. //! leveldb that keys are lexicographically sorted.
[[nodiscard]] bool PopulateAndValidateSnapshot( [[nodiscard]] util::Result<void> PopulateAndValidateSnapshot(
Chainstate& snapshot_chainstate, Chainstate& snapshot_chainstate,
AutoFile& coins_file, AutoFile& coins_file,
const node::SnapshotMetadata& metadata); const node::SnapshotMetadata& metadata);

View file

@ -60,9 +60,8 @@ class AssumeutxoTest(BitcoinTestFramework):
bad_snapshot_path = valid_snapshot_path + '.mod' bad_snapshot_path = valid_snapshot_path + '.mod'
node = self.nodes[1] node = self.nodes[1]
def expected_error(log_msg="", rpc_details=""): def expected_error(msg):
with node.assert_debug_log([log_msg]): assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot: Population failed: {msg}", node.loadtxoutset, bad_snapshot_path)
assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", node.loadtxoutset, bad_snapshot_path)
self.log.info(" - snapshot file with invalid file magic") self.log.info(" - snapshot file with invalid file magic")
parsing_error_code = -22 parsing_error_code = -22
@ -114,19 +113,19 @@ class AssumeutxoTest(BitcoinTestFramework):
f.write(valid_snapshot_contents[:47]) f.write(valid_snapshot_contents[:47])
f.write((valid_num_coins + off).to_bytes(8, "little")) f.write((valid_num_coins + off).to_bytes(8, "little"))
f.write(valid_snapshot_contents[47 + 8:]) f.write(valid_snapshot_contents[47 + 8:])
expected_error(log_msg=f"bad snapshot - coins left over after deserializing 298 coins" if off == -1 else f"bad snapshot format or truncated snapshot after deserializing 299 coins") expected_error(msg="Bad snapshot - coins left over after deserializing 298 coins." if off == -1 else "Bad snapshot format or truncated snapshot after deserializing 299 coins.")
self.log.info(" - snapshot file with alternated but parsable UTXO data results in different hash") self.log.info(" - snapshot file with alternated but parsable UTXO data results in different hash")
cases = [ cases = [
# (content, offset, wrong_hash, custom_message) # (content, offset, wrong_hash, custom_message)
[b"\xff" * 32, 0, "7d52155c9a9fdc4525b637ef6170568e5dad6fabd0b1fdbb9432010b8453095b", None], # wrong outpoint hash [b"\xff" * 32, 0, "7d52155c9a9fdc4525b637ef6170568e5dad6fabd0b1fdbb9432010b8453095b", None], # wrong outpoint hash
[(2).to_bytes(1, "little"), 32, None, "[snapshot] bad snapshot data after deserializing 1 coins"], # wrong txid coins count [(2).to_bytes(1, "little"), 32, None, "Bad snapshot data after deserializing 1 coins."], # wrong txid coins count
[b"\xfd\xff\xff", 32, None, "[snapshot] mismatch in coins count in snapshot metadata and actual snapshot data"], # txid coins count exceeds coins left [b"\xfd\xff\xff", 32, None, "Mismatch in coins count in snapshot metadata and actual snapshot data"], # txid coins count exceeds coins left
[b"\x01", 33, "9f4d897031ab8547665b4153317ae2fdbf0130c7840b66427ebc48b881cb80ad", None], # wrong outpoint index [b"\x01", 33, "9f4d897031ab8547665b4153317ae2fdbf0130c7840b66427ebc48b881cb80ad", None], # wrong outpoint index
[b"\x81", 34, "3da966ba9826fb6d2604260e01607b55ba44e1a5de298606b08704bc62570ea8", None], # wrong coin code VARINT [b"\x81", 34, "3da966ba9826fb6d2604260e01607b55ba44e1a5de298606b08704bc62570ea8", None], # wrong coin code VARINT
[b"\x80", 34, "091e893b3ccb4334378709578025356c8bcb0a623f37c7c4e493133c988648e5", None], # another wrong coin code [b"\x80", 34, "091e893b3ccb4334378709578025356c8bcb0a623f37c7c4e493133c988648e5", None], # another wrong coin code
[b"\x84\x58", 34, None, "[snapshot] bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0 [b"\x84\x58", 34, None, "Bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0
[b"\xCA\xD2\x8F\x5A", 39, None, "[snapshot] bad snapshot data after deserializing 0 coins - bad tx out value"], # Amount exceeds MAX_MONEY [b"\xCA\xD2\x8F\x5A", 39, None, "Bad snapshot data after deserializing 0 coins - bad tx out value"], # Amount exceeds MAX_MONEY
] ]
for content, offset, wrong_hash, custom_message in cases: for content, offset, wrong_hash, custom_message in cases:
@ -136,8 +135,8 @@ class AssumeutxoTest(BitcoinTestFramework):
f.write(content) f.write(content)
f.write(valid_snapshot_contents[(5 + 2 + 4 + 4 + 32 + 8 + offset + len(content)):]) f.write(valid_snapshot_contents[(5 + 2 + 4 + 4 + 32 + 8 + offset + len(content)):])
log_msg = custom_message if custom_message is not None else f"[snapshot] bad snapshot content hash: expected a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27, got {wrong_hash}" msg = custom_message if custom_message is not None else f"Bad snapshot content hash: expected a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27, got {wrong_hash}."
expected_error(log_msg=log_msg) expected_error(msg)
def test_headers_not_synced(self, valid_snapshot_path): def test_headers_not_synced(self, valid_snapshot_path):
for node in self.nodes[1:]: for node in self.nodes[1:]:
@ -188,8 +187,8 @@ class AssumeutxoTest(BitcoinTestFramework):
def test_snapshot_with_less_work(self, dump_output_path): def test_snapshot_with_less_work(self, dump_output_path):
self.log.info("Test bitcoind should fail when snapshot has less accumulated work than this node.") self.log.info("Test bitcoind should fail when snapshot has less accumulated work than this node.")
node = self.nodes[0] node = self.nodes[0]
with node.assert_debug_log(expected_msgs=["[snapshot] activation failed - work does not exceed active chainstate"]): msg = "Unable to load UTXO snapshot: Population failed: Work does not exceed active chainstate."
assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", node.loadtxoutset, dump_output_path) assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path)
def test_snapshot_block_invalidated(self, dump_output_path): def test_snapshot_block_invalidated(self, dump_output_path):
self.log.info("Test snapshot is not loaded when base block is invalid.") self.log.info("Test snapshot is not loaded when base block is invalid.")