Skip to content

Commit 9251bc7

Browse files
committed
Merge bitcoin/bitcoin#30267: assumeutxo: Check snapshot base block is not in invalid chain
2f9bde6 test: Remove unnecessary restart in assumeutxo test (Fabian Jahr) 19ce3d4 assumeutxo: Check snapshot base block is not marked invalid (Fabian Jahr) 80315c0 refactor: Move early loadtxoutset checks into ActiveSnapshot (Fabian Jahr) Pull request description: This was discovered in a discussion in #29996 If the base block of the snapshot is marked invalid or part of an invalid chain, we currently still load the snapshot and get stuck in a weird state where we have the snapshot chainstate but it will never connect to our valid chain. While this scenario is highly unlikely to occur on mainnet, it still seems good to prevent this inconsistent state. The behavior change described above is in the second commit. The first commit refactors the early checks in the `loadtxoutset` RPC by moving them into `ActivateSnapshot()` in order to have the chance to cover them by unit tests in the future and have a more consistent interface. Previously checks were spread out between `rpc/blockchain.cpp` and `validation.cpp`. In order to be able to return the error message to users of the RPC, the return type of `ActivateSnapshot()` is changed from `bool` to `util::Result`. The third commit removes an unnecessary restart introduced in #29428. ACKs for top commit: mzumsande: re-ACK 2f9bde6 alfonsoromanz: Re-ACK 2f9bde6. The RPC code looks much cleaner after the refactor. Also, it seems very useful to get the error message in the RPC response rather than having to rely on the logs in some scenarios if you are an RPC user. achow101: ACK 2f9bde6 Tree-SHA512: 5328dd88c3c7be3f1be97c9eef52ac3666c27188c30a798b3e949f3ffcb83be075127c107e4046f7f39f961a79911ea3d61b61f3c11e451b3e4c541c264eeed4
2 parents 74d6115 + 2f9bde6 commit 9251bc7

File tree

6 files changed

+66
-54
lines changed

6 files changed

+66
-54
lines changed

src/rpc/blockchain.cpp

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@ using kernel::CoinStatsHashType;
6262
using node::BlockManager;
6363
using node::NodeContext;
6464
using node::SnapshotMetadata;
65-
using util::Join;
6665
using util::MakeUnorderedList;
67-
using util::ToString;
6866

6967
struct CUpdatedBlock
7068
{
@@ -2821,34 +2819,15 @@ static RPCHelpMan loadtxoutset()
28212819
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("Unable to parse metadata: %s", e.what()));
28222820
}
28232821

2824-
uint256 base_blockhash = metadata.m_base_blockhash;
2825-
int base_blockheight = metadata.m_base_blockheight;
2826-
if (!chainman.GetParams().AssumeutxoForBlockhash(base_blockhash).has_value()) {
2827-
auto available_heights = chainman.GetParams().GetAvailableSnapshotHeights();
2828-
std::string heights_formatted = Join(available_heights, ", ", [&](const auto& i) { return ToString(i); });
2829-
throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Unable to load UTXO snapshot, "
2830-
"assumeutxo block hash in snapshot metadata not recognized (hash: %s, height: %s). The following snapshot heights are available: %s.",
2831-
base_blockhash.ToString(),
2832-
base_blockheight,
2833-
heights_formatted));
2834-
}
2835-
CBlockIndex* snapshot_start_block = WITH_LOCK(::cs_main,
2836-
return chainman.m_blockman.LookupBlockIndex(base_blockhash));
2837-
2838-
if (!snapshot_start_block) {
2839-
throw JSONRPCError(
2840-
RPC_INTERNAL_ERROR,
2841-
strprintf("The base block header (%s) must appear in the headers chain. Make sure all headers are syncing, and call this RPC again.",
2842-
base_blockhash.ToString()));
2843-
}
2844-
if (!chainman.ActivateSnapshot(afile, metadata, false)) {
2845-
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to load UTXO snapshot " + fs::PathToString(path));
2822+
auto activation_result{chainman.ActivateSnapshot(afile, metadata, false)};
2823+
if (!activation_result) {
2824+
throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf(_("Unable to load UTXO snapshot: %s\n"), util::ErrorString(activation_result)).original);
28462825
}
28472826

28482827
UniValue result(UniValue::VOBJ);
28492828
result.pushKV("coins_loaded", metadata.m_coins_count);
2850-
result.pushKV("tip_hash", snapshot_start_block->GetBlockHash().ToString());
2851-
result.pushKV("base_height", snapshot_start_block->nHeight);
2829+
result.pushKV("tip_hash", metadata.m_base_blockhash.ToString());
2830+
result.pushKV("base_height", metadata.m_base_blockheight);
28522831
result.pushKV("path", fs::PathToString(path));
28532832
return result;
28542833
},

src/test/fuzz/utxo_snapshot.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ FUZZ_TARGET(utxo_snapshot, .init = initialize_chain)
5454
} catch (const std::ios_base::failure&) {
5555
return false;
5656
}
57-
return chainman.ActivateSnapshot(infile, metadata, /*in_memory=*/true);
57+
return !!chainman.ActivateSnapshot(infile, metadata, /*in_memory=*/true);
5858
}};
5959

6060
if (fuzzed_data_provider.ConsumeBool()) {

src/test/util/chainstate.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ CreateAndActivateUTXOSnapshot(
124124
new_active.m_chain.SetTip(*(tip->pprev));
125125
}
126126

127-
bool res = node.chainman->ActivateSnapshot(auto_infile, metadata, in_memory_chainstate);
127+
auto res = node.chainman->ActivateSnapshot(auto_infile, metadata, in_memory_chainstate);
128128

129129
// Restore the old tip.
130130
new_active.m_chain.SetTip(*tip);
131-
return res;
131+
return !!res;
132132
}
133133

134134

src/validation.cpp

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5646,23 +5646,43 @@ Chainstate& ChainstateManager::InitializeChainstate(CTxMemPool* mempool)
56465646
return destroyed && !fs::exists(db_path);
56475647
}
56485648

5649-
bool ChainstateManager::ActivateSnapshot(
5649+
util::Result<void> ChainstateManager::ActivateSnapshot(
56505650
AutoFile& coins_file,
56515651
const SnapshotMetadata& metadata,
56525652
bool in_memory)
56535653
{
56545654
uint256 base_blockhash = metadata.m_base_blockhash;
5655+
int base_blockheight = metadata.m_base_blockheight;
56555656

56565657
if (this->SnapshotBlockhash()) {
5657-
LogPrintf("[snapshot] can't activate a snapshot-based chainstate more than once\n");
5658-
return false;
5658+
return util::Error{_("Can't activate a snapshot-based chainstate more than once")};
56595659
}
56605660

56615661
{
56625662
LOCK(::cs_main);
5663+
5664+
if (!GetParams().AssumeutxoForBlockhash(base_blockhash).has_value()) {
5665+
auto available_heights = GetParams().GetAvailableSnapshotHeights();
5666+
std::string heights_formatted = util::Join(available_heights, ", ", [&](const auto& i) { return util::ToString(i); });
5667+
return util::Error{strprintf(_("assumeutxo block hash in snapshot metadata not recognized (hash: %s, height: %s). The following snapshot heights are available: %s."),
5668+
base_blockhash.ToString(),
5669+
base_blockheight,
5670+
heights_formatted)};
5671+
}
5672+
5673+
CBlockIndex* snapshot_start_block = m_blockman.LookupBlockIndex(base_blockhash);
5674+
if (!snapshot_start_block) {
5675+
return util::Error{strprintf(_("The base block header (%s) must appear in the headers chain. Make sure all headers are syncing, and call loadtxoutset again."),
5676+
base_blockhash.ToString())};
5677+
}
5678+
5679+
bool start_block_invalid = snapshot_start_block->nStatus & BLOCK_FAILED_MASK;
5680+
if (start_block_invalid) {
5681+
return util::Error{strprintf(_("The base block header (%s) is part of an invalid chain."), base_blockhash.ToString())};
5682+
}
5683+
56635684
if (Assert(m_active_chainstate->GetMempool())->size() > 0) {
5664-
LogPrintf("[snapshot] can't activate a snapshot when mempool not empty\n");
5665-
return false;
5685+
return util::Error{_("Can't activate a snapshot when mempool not empty.")};
56665686
}
56675687
}
56685688

@@ -5712,7 +5732,6 @@ bool ChainstateManager::ActivateSnapshot(
57125732
}
57135733

57145734
auto cleanup_bad_snapshot = [&](const char* reason) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
5715-
LogPrintf("[snapshot] activation failed - %s\n", reason);
57165735
this->MaybeRebalanceCaches();
57175736

57185737
// PopulateAndValidateSnapshot can return (in error) before the leveldb datadir
@@ -5728,7 +5747,7 @@ bool ChainstateManager::ActivateSnapshot(
57285747
"Manually remove it before restarting.\n"), fs::PathToString(*snapshot_datadir)));
57295748
}
57305749
}
5731-
return false;
5750+
return util::Error{_(reason)};
57325751
};
57335752

57345753
if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)) {
@@ -5771,7 +5790,7 @@ bool ChainstateManager::ActivateSnapshot(
57715790
m_snapshot_chainstate->CoinsTip().DynamicMemoryUsage() / (1000 * 1000));
57725791

57735792
this->MaybeRebalanceCaches();
5774-
return true;
5793+
return {};
57755794
}
57765795

57775796
static void FlushSnapshotToDisk(CCoinsViewCache& coins_cache, bool snapshot_loaded)

src/validation.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,7 @@ class ChainstateManager
10541054
//! faking nTx* block index data along the way.
10551055
//! - Move the new chainstate to `m_snapshot_chainstate` and make it our
10561056
//! ChainstateActive().
1057-
[[nodiscard]] bool ActivateSnapshot(
1057+
[[nodiscard]] util::Result<void> ActivateSnapshot(
10581058
AutoFile& coins_file, const node::SnapshotMetadata& metadata, bool in_memory);
10591059

10601060
//! Once the background validation chainstate has reached the height which

test/functional/feature_assumeutxo.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,24 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path):
7070
with open(valid_snapshot_path, 'rb') as f:
7171
valid_snapshot_contents = f.read()
7272
bad_snapshot_path = valid_snapshot_path + '.mod'
73+
node = self.nodes[1]
7374

7475
def expected_error(log_msg="", rpc_details=""):
75-
with self.nodes[1].assert_debug_log([log_msg]):
76-
assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", self.nodes[1].loadtxoutset, bad_snapshot_path)
76+
with node.assert_debug_log([log_msg]):
77+
assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", node.loadtxoutset, bad_snapshot_path)
7778

7879
self.log.info(" - snapshot file with invalid file magic")
7980
parsing_error_code = -22
8081
bad_magic = 0xf00f00f000
8182
with open(bad_snapshot_path, 'wb') as f:
8283
f.write(bad_magic.to_bytes(5, "big") + valid_snapshot_contents[5:])
83-
assert_raises_rpc_error(parsing_error_code, "Unable to parse metadata: Invalid UTXO set snapshot magic bytes. Please check if this is indeed a snapshot file or if you are using an outdated snapshot format.", self.nodes[1].loadtxoutset, bad_snapshot_path)
84+
assert_raises_rpc_error(parsing_error_code, "Unable to parse metadata: Invalid UTXO set snapshot magic bytes. Please check if this is indeed a snapshot file or if you are using an outdated snapshot format.", node.loadtxoutset, bad_snapshot_path)
8485

8586
self.log.info(" - snapshot file with unsupported version")
8687
for version in [0, 2]:
8788
with open(bad_snapshot_path, 'wb') as f:
8889
f.write(valid_snapshot_contents[:5] + version.to_bytes(2, "little") + valid_snapshot_contents[7:])
89-
assert_raises_rpc_error(parsing_error_code, f"Unable to parse metadata: Version of snapshot {version} does not match any of the supported versions.", self.nodes[1].loadtxoutset, bad_snapshot_path)
90+
assert_raises_rpc_error(parsing_error_code, f"Unable to parse metadata: Version of snapshot {version} does not match any of the supported versions.", node.loadtxoutset, bad_snapshot_path)
9091

9192
self.log.info(" - snapshot file with mismatching network magic")
9293
invalid_magics = [
@@ -101,9 +102,9 @@ def expected_error(log_msg="", rpc_details=""):
101102
with open(bad_snapshot_path, 'wb') as f:
102103
f.write(valid_snapshot_contents[:7] + magic.to_bytes(4, 'big') + valid_snapshot_contents[11:])
103104
if real:
104-
assert_raises_rpc_error(parsing_error_code, f"Unable to parse metadata: The network of the snapshot ({name}) does not match the network of this node (regtest).", self.nodes[1].loadtxoutset, bad_snapshot_path)
105+
assert_raises_rpc_error(parsing_error_code, f"Unable to parse metadata: The network of the snapshot ({name}) does not match the network of this node (regtest).", node.loadtxoutset, bad_snapshot_path)
105106
else:
106-
assert_raises_rpc_error(parsing_error_code, "Unable to parse metadata: This snapshot has been created for an unrecognized network. This could be a custom signet, a new testnet or possibly caused by data corruption.", self.nodes[1].loadtxoutset, bad_snapshot_path)
107+
assert_raises_rpc_error(parsing_error_code, "Unable to parse metadata: This snapshot has been created for an unrecognized network. This could be a custom signet, a new testnet or possibly caused by data corruption.", node.loadtxoutset, bad_snapshot_path)
107108

108109
self.log.info(" - snapshot file referring to a block that is not in the assumeutxo parameters")
109110
prev_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1)
@@ -114,8 +115,9 @@ def expected_error(log_msg="", rpc_details=""):
114115
for bad_block_hash in [bogus_block_hash, prev_block_hash]:
115116
with open(bad_snapshot_path, 'wb') as f:
116117
f.write(valid_snapshot_contents[:11] + bogus_height.to_bytes(4, "little") + bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[47:])
117-
error_details = f", assumeutxo block hash in snapshot metadata not recognized (hash: {bad_block_hash}, height: {bogus_height}). The following snapshot heights are available: 110, 299."
118-
expected_error(rpc_details=error_details)
118+
119+
msg = f"Unable to load UTXO snapshot: assumeutxo block hash in snapshot metadata not recognized (hash: {bad_block_hash}, height: {bogus_height}). The following snapshot heights are available: 110, 299."
120+
assert_raises_rpc_error(-32603, msg, node.loadtxoutset, bad_snapshot_path)
119121

120122
self.log.info(" - snapshot file with wrong number of coins")
121123
valid_num_coins = int.from_bytes(valid_snapshot_contents[47:47 + 8], "little")
@@ -151,9 +153,8 @@ def expected_error(log_msg="", rpc_details=""):
151153

152154
def test_headers_not_synced(self, valid_snapshot_path):
153155
for node in self.nodes[1:]:
154-
assert_raises_rpc_error(-32603, "The base block header (3bb7ce5eba0be48939b7a521ac1ba9316afee2c7bada3a0cca24188e6d7d96c0) must appear in the headers chain. Make sure all headers are syncing, and call this RPC again.",
155-
node.loadtxoutset,
156-
valid_snapshot_path)
156+
msg = "Unable to load UTXO snapshot: The base block header (3bb7ce5eba0be48939b7a521ac1ba9316afee2c7bada3a0cca24188e6d7d96c0) must appear in the headers chain. Make sure all headers are syncing, and call loadtxoutset again."
157+
assert_raises_rpc_error(-32603, msg, node.loadtxoutset, valid_snapshot_path)
157158

158159
def test_invalid_chainstate_scenarios(self):
159160
self.log.info("Test different scenarios of invalid snapshot chainstate in datadir")
@@ -185,8 +186,8 @@ def test_invalid_mempool_state(self, dump_output_path):
185186
assert tx['txid'] in node.getrawmempool()
186187

187188
# Attempt to load the snapshot on Node 2 and expect it to fail
188-
with node.assert_debug_log(expected_msgs=["[snapshot] can't activate a snapshot when mempool not empty"]):
189-
assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", node.loadtxoutset, dump_output_path)
189+
msg = "Unable to load UTXO snapshot: Can't activate a snapshot when mempool not empty"
190+
assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path)
190191

191192
self.restart_node(2, extra_args=self.extra_args[2])
192193

@@ -202,7 +203,19 @@ def test_snapshot_with_less_work(self, dump_output_path):
202203
assert_equal(node.getblockcount(), FINAL_HEIGHT)
203204
with node.assert_debug_log(expected_msgs=["[snapshot] activation failed - work does not exceed active chainstate"]):
204205
assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", node.loadtxoutset, dump_output_path)
205-
self.restart_node(0, extra_args=self.extra_args[0])
206+
207+
def test_snapshot_block_invalidated(self, dump_output_path):
208+
self.log.info("Test snapshot is not loaded when base block is invalid.")
209+
node = self.nodes[0]
210+
# We are testing the case where the base block is invalidated itself
211+
# and also the case where one of its parents is invalidated.
212+
for height in [SNAPSHOT_BASE_HEIGHT, SNAPSHOT_BASE_HEIGHT - 1]:
213+
block_hash = node.getblockhash(height)
214+
node.invalidateblock(block_hash)
215+
assert_equal(node.getblockcount(), height - 1)
216+
msg = "Unable to load UTXO snapshot: The base block header (3bb7ce5eba0be48939b7a521ac1ba9316afee2c7bada3a0cca24188e6d7d96c0) is part of an invalid chain."
217+
assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path)
218+
node.reconsiderblock(block_hash)
206219

207220
def run_test(self):
208221
"""
@@ -290,6 +303,7 @@ def run_test(self):
290303
self.test_invalid_snapshot_scenarios(dump_output['path'])
291304
self.test_invalid_chainstate_scenarios()
292305
self.test_invalid_file_path()
306+
self.test_snapshot_block_invalidated(dump_output['path'])
293307

294308
self.log.info(f"Loading snapshot into second node from {dump_output['path']}")
295309
loaded = n1.loadtxoutset(dump_output['path'])
@@ -450,8 +464,8 @@ def check_tx_counts(final: bool) -> None:
450464
assert_equal(snapshot['validated'], False)
451465

452466
self.log.info("Check that loading the snapshot again will fail because there is already an active snapshot.")
453-
with n2.assert_debug_log(expected_msgs=["[snapshot] can't activate a snapshot-based chainstate more than once"]):
454-
assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", n2.loadtxoutset, dump_output['path'])
467+
msg = "Unable to load UTXO snapshot: Can't activate a snapshot-based chainstate more than once"
468+
assert_raises_rpc_error(-32603, msg, n2.loadtxoutset, dump_output['path'])
455469

456470
self.connect_nodes(0, 2)
457471
self.wait_until(lambda: n2.getchainstates()['chainstates'][-1]['blocks'] == FINAL_HEIGHT)

0 commit comments

Comments
 (0)