Skip to content

Commit fa530ec

Browse files
author
MarcoFalke
committed
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.
1 parent faa5c86 commit fa530ec

File tree

3 files changed

+36
-47
lines changed

3 files changed

+36
-47
lines changed

src/validation.cpp

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (c) 2009-2010 Satoshi Nakamoto
2-
// Copyright (c) 2009-2022 The Bitcoin Core developers
2+
// Copyright (c) 2009-present The Bitcoin Core developers
33
// Distributed under the MIT software license, see the accompanying
44
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
55

@@ -5754,9 +5754,9 @@ util::Result<void> ChainstateManager::ActivateSnapshot(
57545754
return util::Error{std::move(reason)};
57555755
};
57565756

5757-
if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)) {
5757+
if (auto res{this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)}; !res) {
57585758
LOCK(::cs_main);
5759-
return cleanup_bad_snapshot(Untranslated("population failed"));
5759+
return cleanup_bad_snapshot(strprintf(Untranslated("Population failed: %s"), util::ErrorString(res)));
57605760
}
57615761

57625762
LOCK(::cs_main); // cs_main required for rest of snapshot activation.
@@ -5821,7 +5821,7 @@ static void SnapshotUTXOHashBreakpoint(const util::SignalInterrupt& interrupt)
58215821
if (interrupt) throw StopHashingException();
58225822
}
58235823

5824-
bool ChainstateManager::PopulateAndValidateSnapshot(
5824+
util::Result<void> ChainstateManager::PopulateAndValidateSnapshot(
58255825
Chainstate& snapshot_chainstate,
58265826
AutoFile& coins_file,
58275827
const SnapshotMetadata& metadata)
@@ -5837,18 +5837,16 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
58375837
if (!snapshot_start_block) {
58385838
// Needed for ComputeUTXOStats to determine the
58395839
// height and to avoid a crash when base_blockhash.IsNull()
5840-
LogPrintf("[snapshot] Did not find snapshot start blockheader %s\n",
5841-
base_blockhash.ToString());
5842-
return false;
5840+
return util::Error{strprintf(Untranslated("Did not find snapshot start blockheader %s"),
5841+
base_blockhash.ToString())};
58435842
}
58445843

58455844
int base_height = snapshot_start_block->nHeight;
58465845
const auto& maybe_au_data = GetParams().AssumeutxoForHeight(base_height);
58475846

58485847
if (!maybe_au_data) {
5849-
LogPrintf("[snapshot] assumeutxo height in snapshot metadata not recognized "
5850-
"(%d) - refusing to load snapshot\n", base_height);
5851-
return false;
5848+
return util::Error{strprintf(Untranslated("Assumeutxo height in snapshot metadata not recognized "
5849+
"(%d) - refusing to load snapshot"), base_height)};
58525850
}
58535851

58545852
const AssumeutxoData& au_data = *maybe_au_data;
@@ -5857,8 +5855,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
58575855
// ActivateSnapshot(), but is done so that we avoid doing the long work of staging
58585856
// a snapshot that isn't actually usable.
58595857
if (WITH_LOCK(::cs_main, return !CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) {
5860-
LogPrintf("[snapshot] activation failed - work does not exceed active chainstate\n");
5861-
return false;
5858+
return util::Error{Untranslated("Work does not exceed active chainstate")};
58625859
}
58635860

58645861
const uint64_t coins_count = metadata.m_coins_count;
@@ -5875,8 +5872,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
58755872
coins_per_txid = ReadCompactSize(coins_file);
58765873

58775874
if (coins_per_txid > coins_left) {
5878-
LogPrintf("[snapshot] mismatch in coins count in snapshot metadata and actual snapshot data\n");
5879-
return false;
5875+
return util::Error{Untranslated("Mismatch in coins count in snapshot metadata and actual snapshot data")};
58805876
}
58815877

58825878
for (size_t i = 0; i < coins_per_txid; i++) {
@@ -5888,14 +5884,12 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
58885884
if (coin.nHeight > base_height ||
58895885
outpoint.n >= std::numeric_limits<decltype(outpoint.n)>::max() // Avoid integer wrap-around in coinstats.cpp:ApplyHash
58905886
) {
5891-
LogPrintf("[snapshot] bad snapshot data after deserializing %d coins\n",
5892-
coins_count - coins_left);
5893-
return false;
5887+
return util::Error{strprintf(Untranslated("Bad snapshot data after deserializing %d coins"),
5888+
coins_count - coins_left)};
58945889
}
58955890
if (!MoneyRange(coin.out.nValue)) {
5896-
LogPrintf("[snapshot] bad snapshot data after deserializing %d coins - bad tx out value\n",
5897-
coins_count - coins_left);
5898-
return false;
5891+
return util::Error{strprintf(Untranslated("Bad snapshot data after deserializing %d coins - bad tx out value"),
5892+
coins_count - coins_left)};
58995893
}
59005894
coins_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
59015895

@@ -5915,7 +5909,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
59155909
// means <5MB of memory imprecision.
59165910
if (coins_processed % 120000 == 0) {
59175911
if (m_interrupt) {
5918-
return false;
5912+
return util::Error{Untranslated("Aborting after an interrupt was requested")};
59195913
}
59205914

59215915
const auto snapshot_cache_state = WITH_LOCK(::cs_main,
@@ -5933,9 +5927,8 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
59335927
}
59345928
}
59355929
} catch (const std::ios_base::failure&) {
5936-
LogPrintf("[snapshot] bad snapshot format or truncated snapshot after deserializing %d coins\n",
5937-
coins_processed);
5938-
return false;
5930+
return util::Error{strprintf(Untranslated("Bad snapshot format or truncated snapshot after deserializing %d coins"),
5931+
coins_processed)};
59395932
}
59405933
}
59415934

@@ -5955,9 +5948,8 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
59555948
out_of_coins = true;
59565949
}
59575950
if (!out_of_coins) {
5958-
LogPrintf("[snapshot] bad snapshot - coins left over after deserializing %d coins\n",
5959-
coins_count);
5960-
return false;
5951+
return util::Error{strprintf(Untranslated("Bad snapshot - coins left over after deserializing %d coins"),
5952+
coins_count)};
59615953
}
59625954

59635955
LogPrintf("[snapshot] loaded %d (%.2f MB) coins from snapshot %s\n",
@@ -5980,18 +5972,16 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
59805972
maybe_stats = ComputeUTXOStats(
59815973
CoinStatsHashType::HASH_SERIALIZED, snapshot_coinsdb, m_blockman, [&interrupt = m_interrupt] { SnapshotUTXOHashBreakpoint(interrupt); });
59825974
} catch (StopHashingException const&) {
5983-
return false;
5975+
return util::Error{Untranslated("Aborting after an interrupt was requested")};
59845976
}
59855977
if (!maybe_stats.has_value()) {
5986-
LogPrintf("[snapshot] failed to generate coins stats\n");
5987-
return false;
5978+
return util::Error{Untranslated("Failed to generate coins stats")};
59885979
}
59895980

59905981
// Assert that the deserialized chainstate contents match the expected assumeutxo value.
59915982
if (AssumeutxoHash{maybe_stats->hashSerialized} != au_data.hash_serialized) {
5992-
LogPrintf("[snapshot] bad snapshot content hash: expected %s, got %s\n",
5993-
au_data.hash_serialized.ToString(), maybe_stats->hashSerialized.ToString());
5994-
return false;
5983+
return util::Error{strprintf(Untranslated("Bad snapshot content hash: expected %s, got %s"),
5984+
au_data.hash_serialized.ToString(), maybe_stats->hashSerialized.ToString())};
59955985
}
59965986

59975987
snapshot_chainstate.m_chain.SetTip(*snapshot_start_block);
@@ -6031,7 +6021,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
60316021

60326022
LogPrintf("[snapshot] validated snapshot (%.2f MB)\n",
60336023
coins_cache.DynamicMemoryUsage() / (1000 * 1000));
6034-
return true;
6024+
return {};
60356025
}
60366026

60376027
// Currently, this function holds cs_main for its duration, which could be for

src/validation.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -918,7 +918,7 @@ class ChainstateManager
918918
//! To reduce space the serialization format of the snapshot avoids
919919
//! duplication of tx hashes. The code takes advantage of the guarantee by
920920
//! leveldb that keys are lexicographically sorted.
921-
[[nodiscard]] bool PopulateAndValidateSnapshot(
921+
[[nodiscard]] util::Result<void> PopulateAndValidateSnapshot(
922922
Chainstate& snapshot_chainstate,
923923
AutoFile& coins_file,
924924
const node::SnapshotMetadata& metadata);

test/functional/feature_assumeutxo.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,8 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path):
6060
bad_snapshot_path = valid_snapshot_path + '.mod'
6161
node = self.nodes[1]
6262

63-
def expected_error(log_msg="", rpc_details=""):
64-
with node.assert_debug_log([log_msg]):
65-
assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", node.loadtxoutset, bad_snapshot_path)
63+
def expected_error(msg):
64+
assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot: Population failed: {msg}", node.loadtxoutset, bad_snapshot_path)
6665

6766
self.log.info(" - snapshot file with invalid file magic")
6867
parsing_error_code = -22
@@ -114,19 +113,19 @@ def expected_error(log_msg="", rpc_details=""):
114113
f.write(valid_snapshot_contents[:47])
115114
f.write((valid_num_coins + off).to_bytes(8, "little"))
116115
f.write(valid_snapshot_contents[47 + 8:])
117-
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")
116+
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.")
118117

119118
self.log.info(" - snapshot file with alternated but parsable UTXO data results in different hash")
120119
cases = [
121120
# (content, offset, wrong_hash, custom_message)
122121
[b"\xff" * 32, 0, "7d52155c9a9fdc4525b637ef6170568e5dad6fabd0b1fdbb9432010b8453095b", None], # wrong outpoint hash
123-
[(2).to_bytes(1, "little"), 32, None, "[snapshot] bad snapshot data after deserializing 1 coins"], # wrong txid coins count
124-
[b"\xfd\xff\xff", 32, None, "[snapshot] mismatch in coins count in snapshot metadata and actual snapshot data"], # txid coins count exceeds coins left
122+
[(2).to_bytes(1, "little"), 32, None, "Bad snapshot data after deserializing 1 coins."], # wrong txid coins count
123+
[b"\xfd\xff\xff", 32, None, "Mismatch in coins count in snapshot metadata and actual snapshot data"], # txid coins count exceeds coins left
125124
[b"\x01", 33, "9f4d897031ab8547665b4153317ae2fdbf0130c7840b66427ebc48b881cb80ad", None], # wrong outpoint index
126125
[b"\x81", 34, "3da966ba9826fb6d2604260e01607b55ba44e1a5de298606b08704bc62570ea8", None], # wrong coin code VARINT
127126
[b"\x80", 34, "091e893b3ccb4334378709578025356c8bcb0a623f37c7c4e493133c988648e5", None], # another wrong coin code
128-
[b"\x84\x58", 34, None, "[snapshot] bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0
129-
[b"\xCA\xD2\x8F\x5A", 39, None, "[snapshot] bad snapshot data after deserializing 0 coins - bad tx out value"], # Amount exceeds MAX_MONEY
127+
[b"\x84\x58", 34, None, "Bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0
128+
[b"\xCA\xD2\x8F\x5A", 39, None, "Bad snapshot data after deserializing 0 coins - bad tx out value"], # Amount exceeds MAX_MONEY
130129
]
131130

132131
for content, offset, wrong_hash, custom_message in cases:
@@ -136,8 +135,8 @@ def expected_error(log_msg="", rpc_details=""):
136135
f.write(content)
137136
f.write(valid_snapshot_contents[(5 + 2 + 4 + 4 + 32 + 8 + offset + len(content)):])
138137

139-
log_msg = custom_message if custom_message is not None else f"[snapshot] bad snapshot content hash: expected a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27, got {wrong_hash}"
140-
expected_error(log_msg=log_msg)
138+
msg = custom_message if custom_message is not None else f"Bad snapshot content hash: expected a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27, got {wrong_hash}."
139+
expected_error(msg)
141140

142141
def test_headers_not_synced(self, valid_snapshot_path):
143142
for node in self.nodes[1:]:
@@ -188,8 +187,8 @@ def test_invalid_file_path(self):
188187
def test_snapshot_with_less_work(self, dump_output_path):
189188
self.log.info("Test bitcoind should fail when snapshot has less accumulated work than this node.")
190189
node = self.nodes[0]
191-
with node.assert_debug_log(expected_msgs=["[snapshot] activation failed - work does not exceed active chainstate"]):
192-
assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", node.loadtxoutset, dump_output_path)
190+
msg = "Unable to load UTXO snapshot: Population failed: Work does not exceed active chainstate."
191+
assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path)
193192

194193
def test_snapshot_block_invalidated(self, dump_output_path):
195194
self.log.info("Test snapshot is not loaded when base block is invalid.")

0 commit comments

Comments
 (0)