Skip to content

Commit

Permalink
refactor: Move early loadtxoutset checks into ActiveSnapshot
Browse files Browse the repository at this point in the history
Also changes the return type of ActiveSnapshot to allow returning the
error message to the user of the loadtxoutset RPC.
  • Loading branch information
fjahr committed Jun 19, 2024
1 parent 9c5cdf0 commit 80315c0
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 53 deletions.
31 changes: 5 additions & 26 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
},
Expand Down
2 changes: 1 addition & 1 deletion src/test/fuzz/utxo_snapshot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
4 changes: 2 additions & 2 deletions src/test/util/chainstate.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


Expand Down
30 changes: 22 additions & 8 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5647,23 +5647,38 @@ 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())};
}

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.")};
}
}

Expand Down Expand Up @@ -5713,7 +5728,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
Expand All @@ -5729,7 +5743,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)) {
Expand Down Expand Up @@ -5772,7 +5786,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)
Expand Down
2 changes: 1 addition & 1 deletion src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,7 @@ class ChainstateManager
//! 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
Expand Down
31 changes: 16 additions & 15 deletions test/functional/feature_assumeutxo.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,24 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path):
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 = [
Expand All @@ -101,9 +102,9 @@ def expected_error(log_msg="", rpc_details=""):
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)
Expand All @@ -114,8 +115,9 @@ def expected_error(log_msg="", rpc_details=""):
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")
Expand Down Expand Up @@ -151,9 +153,8 @@ def expected_error(log_msg="", rpc_details=""):

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")
Expand Down Expand Up @@ -185,8 +186,8 @@ def test_invalid_mempool_state(self, dump_output_path):
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])

Expand Down Expand Up @@ -450,8 +451,8 @@ def check_tx_counts(final: bool) -> None:
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)
Expand Down

0 comments on commit 80315c0

Please sign in to comment.