diff options
-rw-r--r-- | src/rpc/blockchain.cpp | 31 | ||||
-rw-r--r-- | src/test/fuzz/utxo_snapshot.cpp | 2 | ||||
-rw-r--r-- | src/test/util/chainstate.h | 4 | ||||
-rw-r--r-- | src/validation.cpp | 35 | ||||
-rw-r--r-- | src/validation.h | 2 | ||||
-rwxr-xr-x | test/functional/feature_assumeutxo.py | 46 |
6 files changed, 66 insertions, 54 deletions
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index e785678614..4de636b69f 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -62,9 +62,7 @@ using kernel::CoinStatsHashType; using node::BlockManager; using node::NodeContext; using node::SnapshotMetadata; -using util::Join; using util::MakeUnorderedList; -using util::ToString; struct CUpdatedBlock { @@ -2821,34 +2819,15 @@ static RPCHelpMan loadtxoutset() throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("Unable to parse metadata: %s", e.what())); } - uint256 base_blockhash = metadata.m_base_blockhash; - int base_blockheight = metadata.m_base_blockheight; - if (!chainman.GetParams().AssumeutxoForBlockhash(base_blockhash).has_value()) { - auto available_heights = chainman.GetParams().GetAvailableSnapshotHeights(); - std::string heights_formatted = Join(available_heights, ", ", [&](const auto& i) { return ToString(i); }); - throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Unable to load UTXO snapshot, " - "assumeutxo block hash in snapshot metadata not recognized (hash: %s, height: %s). The following snapshot heights are available: %s.", - base_blockhash.ToString(), - base_blockheight, - heights_formatted)); - } - CBlockIndex* snapshot_start_block = WITH_LOCK(::cs_main, - return chainman.m_blockman.LookupBlockIndex(base_blockhash)); - - if (!snapshot_start_block) { - throw JSONRPCError( - RPC_INTERNAL_ERROR, - strprintf("The base block header (%s) must appear in the headers chain. Make sure all headers are syncing, and call this RPC again.", - base_blockhash.ToString())); - } - if (!chainman.ActivateSnapshot(afile, metadata, false)) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to load UTXO snapshot " + fs::PathToString(path)); + auto activation_result{chainman.ActivateSnapshot(afile, metadata, false)}; + if (!activation_result) { + throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf(_("Unable to load UTXO snapshot: %s\n"), util::ErrorString(activation_result)).original); } UniValue result(UniValue::VOBJ); result.pushKV("coins_loaded", metadata.m_coins_count); - result.pushKV("tip_hash", snapshot_start_block->GetBlockHash().ToString()); - result.pushKV("base_height", snapshot_start_block->nHeight); + result.pushKV("tip_hash", metadata.m_base_blockhash.ToString()); + result.pushKV("base_height", metadata.m_base_blockheight); result.pushKV("path", fs::PathToString(path)); return result; }, diff --git a/src/test/fuzz/utxo_snapshot.cpp b/src/test/fuzz/utxo_snapshot.cpp index 8c9c67a91c..fa608385d9 100644 --- a/src/test/fuzz/utxo_snapshot.cpp +++ b/src/test/fuzz/utxo_snapshot.cpp @@ -54,7 +54,7 @@ FUZZ_TARGET(utxo_snapshot, .init = initialize_chain) } catch (const std::ios_base::failure&) { return false; } - return chainman.ActivateSnapshot(infile, metadata, /*in_memory=*/true); + return !!chainman.ActivateSnapshot(infile, metadata, /*in_memory=*/true); }}; if (fuzzed_data_provider.ConsumeBool()) { diff --git a/src/test/util/chainstate.h b/src/test/util/chainstate.h index 03b44fc894..a4636365ca 100644 --- a/src/test/util/chainstate.h +++ b/src/test/util/chainstate.h @@ -124,11 +124,11 @@ CreateAndActivateUTXOSnapshot( new_active.m_chain.SetTip(*(tip->pprev)); } - bool res = node.chainman->ActivateSnapshot(auto_infile, metadata, in_memory_chainstate); + auto res = node.chainman->ActivateSnapshot(auto_infile, metadata, in_memory_chainstate); // Restore the old tip. new_active.m_chain.SetTip(*tip); - return res; + return !!res; } diff --git a/src/validation.cpp b/src/validation.cpp index 3e9ba08bb1..4241d51d15 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5646,23 +5646,43 @@ Chainstate& ChainstateManager::InitializeChainstate(CTxMemPool* mempool) return destroyed && !fs::exists(db_path); } -bool ChainstateManager::ActivateSnapshot( +util::Result<void> ChainstateManager::ActivateSnapshot( AutoFile& coins_file, const SnapshotMetadata& metadata, bool in_memory) { uint256 base_blockhash = metadata.m_base_blockhash; + int base_blockheight = metadata.m_base_blockheight; if (this->SnapshotBlockhash()) { - LogPrintf("[snapshot] can't activate a snapshot-based chainstate more than once\n"); - return false; + return util::Error{_("Can't activate a snapshot-based chainstate more than once")}; } { LOCK(::cs_main); + + if (!GetParams().AssumeutxoForBlockhash(base_blockhash).has_value()) { + auto available_heights = GetParams().GetAvailableSnapshotHeights(); + std::string heights_formatted = util::Join(available_heights, ", ", [&](const auto& i) { return util::ToString(i); }); + return util::Error{strprintf(_("assumeutxo block hash in snapshot metadata not recognized (hash: %s, height: %s). The following snapshot heights are available: %s."), + base_blockhash.ToString(), + base_blockheight, + heights_formatted)}; + } + + CBlockIndex* snapshot_start_block = m_blockman.LookupBlockIndex(base_blockhash); + if (!snapshot_start_block) { + 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."), + base_blockhash.ToString())}; + } + + bool start_block_invalid = snapshot_start_block->nStatus & BLOCK_FAILED_MASK; + if (start_block_invalid) { + return util::Error{strprintf(_("The base block header (%s) is part of an invalid chain."), base_blockhash.ToString())}; + } + if (Assert(m_active_chainstate->GetMempool())->size() > 0) { - LogPrintf("[snapshot] can't activate a snapshot when mempool not empty\n"); - return false; + return util::Error{_("Can't activate a snapshot when mempool not empty.")}; } } @@ -5712,7 +5732,6 @@ bool ChainstateManager::ActivateSnapshot( } auto cleanup_bad_snapshot = [&](const char* reason) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { - LogPrintf("[snapshot] activation failed - %s\n", reason); this->MaybeRebalanceCaches(); // PopulateAndValidateSnapshot can return (in error) before the leveldb datadir @@ -5728,7 +5747,7 @@ bool ChainstateManager::ActivateSnapshot( "Manually remove it before restarting.\n"), fs::PathToString(*snapshot_datadir))); } } - return false; + return util::Error{_(reason)}; }; if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)) { @@ -5771,7 +5790,7 @@ bool ChainstateManager::ActivateSnapshot( m_snapshot_chainstate->CoinsTip().DynamicMemoryUsage() / (1000 * 1000)); this->MaybeRebalanceCaches(); - return true; + return {}; } static void FlushSnapshotToDisk(CCoinsViewCache& coins_cache, bool snapshot_loaded) diff --git a/src/validation.h b/src/validation.h index ab7891539a..2f5f2e2b95 100644 --- a/src/validation.h +++ b/src/validation.h @@ -1054,7 +1054,7 @@ public: //! faking nTx* block index data along the way. //! - Move the new chainstate to `m_snapshot_chainstate` and make it our //! ChainstateActive(). - [[nodiscard]] bool ActivateSnapshot( + [[nodiscard]] util::Result<void> ActivateSnapshot( AutoFile& coins_file, const node::SnapshotMetadata& metadata, bool in_memory); //! Once the background validation chainstate has reached the height which diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py index 658eea0a0e..0b434d1616 100755 --- a/test/functional/feature_assumeutxo.py +++ b/test/functional/feature_assumeutxo.py @@ -70,23 +70,24 @@ class AssumeutxoTest(BitcoinTestFramework): with open(valid_snapshot_path, 'rb') as f: valid_snapshot_contents = f.read() bad_snapshot_path = valid_snapshot_path + '.mod' + node = self.nodes[1] def expected_error(log_msg="", rpc_details=""): - with self.nodes[1].assert_debug_log([log_msg]): - assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", self.nodes[1].loadtxoutset, bad_snapshot_path) + with node.assert_debug_log([log_msg]): + 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") parsing_error_code = -22 bad_magic = 0xf00f00f000 with open(bad_snapshot_path, 'wb') as f: f.write(bad_magic.to_bytes(5, "big") + valid_snapshot_contents[5:]) - 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) + 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) self.log.info(" - snapshot file with unsupported version") for version in [0, 2]: with open(bad_snapshot_path, 'wb') as f: f.write(valid_snapshot_contents[:5] + version.to_bytes(2, "little") + valid_snapshot_contents[7:]) - 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) + 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) self.log.info(" - snapshot file with mismatching network magic") invalid_magics = [ @@ -101,9 +102,9 @@ class AssumeutxoTest(BitcoinTestFramework): with open(bad_snapshot_path, 'wb') as f: f.write(valid_snapshot_contents[:7] + magic.to_bytes(4, 'big') + valid_snapshot_contents[11:]) if real: - 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) + 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) else: - 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) + 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) self.log.info(" - snapshot file referring to a block that is not in the assumeutxo parameters") prev_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1) @@ -114,8 +115,9 @@ class AssumeutxoTest(BitcoinTestFramework): for bad_block_hash in [bogus_block_hash, prev_block_hash]: with open(bad_snapshot_path, 'wb') as f: f.write(valid_snapshot_contents[:11] + bogus_height.to_bytes(4, "little") + bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[47:]) - 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." - expected_error(rpc_details=error_details) + + 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." + assert_raises_rpc_error(-32603, msg, node.loadtxoutset, bad_snapshot_path) self.log.info(" - snapshot file with wrong number of coins") valid_num_coins = int.from_bytes(valid_snapshot_contents[47:47 + 8], "little") @@ -151,9 +153,8 @@ class AssumeutxoTest(BitcoinTestFramework): def test_headers_not_synced(self, valid_snapshot_path): for node in self.nodes[1:]: - 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.", - node.loadtxoutset, - valid_snapshot_path) + 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." + assert_raises_rpc_error(-32603, msg, node.loadtxoutset, valid_snapshot_path) def test_invalid_chainstate_scenarios(self): self.log.info("Test different scenarios of invalid snapshot chainstate in datadir") @@ -185,8 +186,8 @@ class AssumeutxoTest(BitcoinTestFramework): assert tx['txid'] in node.getrawmempool() # Attempt to load the snapshot on Node 2 and expect it to fail - with node.assert_debug_log(expected_msgs=["[snapshot] can't activate a snapshot when mempool not empty"]): - assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", node.loadtxoutset, dump_output_path) + msg = "Unable to load UTXO snapshot: Can't activate a snapshot when mempool not empty" + assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path) self.restart_node(2, extra_args=self.extra_args[2]) @@ -202,7 +203,19 @@ class AssumeutxoTest(BitcoinTestFramework): assert_equal(node.getblockcount(), FINAL_HEIGHT) with node.assert_debug_log(expected_msgs=["[snapshot] activation failed - work does not exceed active chainstate"]): assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", node.loadtxoutset, dump_output_path) - self.restart_node(0, extra_args=self.extra_args[0]) + + def test_snapshot_block_invalidated(self, dump_output_path): + self.log.info("Test snapshot is not loaded when base block is invalid.") + node = self.nodes[0] + # We are testing the case where the base block is invalidated itself + # and also the case where one of its parents is invalidated. + for height in [SNAPSHOT_BASE_HEIGHT, SNAPSHOT_BASE_HEIGHT - 1]: + block_hash = node.getblockhash(height) + node.invalidateblock(block_hash) + assert_equal(node.getblockcount(), height - 1) + msg = "Unable to load UTXO snapshot: The base block header (3bb7ce5eba0be48939b7a521ac1ba9316afee2c7bada3a0cca24188e6d7d96c0) is part of an invalid chain." + assert_raises_rpc_error(-32603, msg, node.loadtxoutset, dump_output_path) + node.reconsiderblock(block_hash) def run_test(self): """ @@ -290,6 +303,7 @@ class AssumeutxoTest(BitcoinTestFramework): self.test_invalid_snapshot_scenarios(dump_output['path']) self.test_invalid_chainstate_scenarios() self.test_invalid_file_path() + self.test_snapshot_block_invalidated(dump_output['path']) self.log.info(f"Loading snapshot into second node from {dump_output['path']}") loaded = n1.loadtxoutset(dump_output['path']) @@ -450,8 +464,8 @@ class AssumeutxoTest(BitcoinTestFramework): assert_equal(snapshot['validated'], False) self.log.info("Check that loading the snapshot again will fail because there is already an active snapshot.") - with n2.assert_debug_log(expected_msgs=["[snapshot] can't activate a snapshot-based chainstate more than once"]): - assert_raises_rpc_error(-32603, "Unable to load UTXO snapshot", n2.loadtxoutset, dump_output['path']) + msg = "Unable to load UTXO snapshot: Can't activate a snapshot-based chainstate more than once" + assert_raises_rpc_error(-32603, msg, n2.loadtxoutset, dump_output['path']) self.connect_nodes(0, 2) self.wait_until(lambda: n2.getchainstates()['chainstates'][-1]['blocks'] == FINAL_HEIGHT) |