mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-08 10:31:50 -05:00
Merge bitcoin/bitcoin#29612: rpc: Optimize serialization and enhance metadata of dumptxoutset output
542e13b293
rpc: Enhance metadata of the dumptxoutset output (Fabian Jahr)4d8e5edbaa
assumeutxo: Add documentation on dumptxoutset serialization format (Fabian Jahr)c14ed7f384
assumeutxo: Add test for changed coin size value (Fabian Jahr)de95953d87
rpc: Optimize serialization disk space of dumptxoutset (Fabian Jahr) Pull request description: The second attempt at implementing the `dumptxoutset` space optimization as suggested in #25675. Closes #25675. This builds on the work done in #26045, addresses open feedback, adds some further improvements (most importantly usage of compact size), documentation, and an additional test. The [original snapshot at height 830,000](https://github.com/bitcoin/bitcoin/pull/29551) came in at 10.82 GB. With this change, the same snapshot is 8.94 GB, a reduction of 17.4%. This also enhances the metadata of the output file and adds the following data to allow for better error handling and make future upgrades easier: - A newly introduced utxo set magic - A version number - The network magic - The block height ACKs for top commit: achow101: ACK542e13b293
TheCharlatan: Re-ACK542e13b293
theStack: ACK542e13b293
Tree-SHA512: 0825d30e5c3c364062db3c6cbca4e3c680e6e6d3e259fa70c0c2b2a7020f24a47406a623582040988d5c7745b08649c31110df4c10656aa25f3f27eb35843d99
This commit is contained in:
commit
413844f1c2
9 changed files with 261 additions and 76 deletions
|
@ -542,3 +542,33 @@ std::unique_ptr<const CChainParams> CChainParams::TestNet()
|
||||||
{
|
{
|
||||||
return std::make_unique<const CTestNetParams>();
|
return std::make_unique<const CTestNetParams>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<int> CChainParams::GetAvailableSnapshotHeights() const
|
||||||
|
{
|
||||||
|
std::vector<int> heights;
|
||||||
|
heights.reserve(m_assumeutxo_data.size());
|
||||||
|
|
||||||
|
for (const auto& data : m_assumeutxo_data) {
|
||||||
|
heights.emplace_back(data.height);
|
||||||
|
}
|
||||||
|
return heights;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ChainType> GetNetworkForMagic(MessageStartChars& message)
|
||||||
|
{
|
||||||
|
const auto mainnet_msg = CChainParams::Main()->MessageStart();
|
||||||
|
const auto testnet_msg = CChainParams::TestNet()->MessageStart();
|
||||||
|
const auto regtest_msg = CChainParams::RegTest({})->MessageStart();
|
||||||
|
const auto signet_msg = CChainParams::SigNet({})->MessageStart();
|
||||||
|
|
||||||
|
if (std::equal(message.begin(), message.end(), mainnet_msg.data())) {
|
||||||
|
return ChainType::MAIN;
|
||||||
|
} else if (std::equal(message.begin(), message.end(), testnet_msg.data())) {
|
||||||
|
return ChainType::TESTNET;
|
||||||
|
} else if (std::equal(message.begin(), message.end(), regtest_msg.data())) {
|
||||||
|
return ChainType::REGTEST;
|
||||||
|
} else if (std::equal(message.begin(), message.end(), signet_msg.data())) {
|
||||||
|
return ChainType::SIGNET;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
|
@ -93,6 +93,7 @@ public:
|
||||||
const Consensus::Params& GetConsensus() const { return consensus; }
|
const Consensus::Params& GetConsensus() const { return consensus; }
|
||||||
const MessageStartChars& MessageStart() const { return pchMessageStart; }
|
const MessageStartChars& MessageStart() const { return pchMessageStart; }
|
||||||
uint16_t GetDefaultPort() const { return nDefaultPort; }
|
uint16_t GetDefaultPort() const { return nDefaultPort; }
|
||||||
|
std::vector<int> GetAvailableSnapshotHeights() const;
|
||||||
|
|
||||||
const CBlock& GenesisBlock() const { return genesis; }
|
const CBlock& GenesisBlock() const { return genesis; }
|
||||||
/** Default value for -checkmempool and -checkblockindex argument */
|
/** Default value for -checkmempool and -checkblockindex argument */
|
||||||
|
@ -183,4 +184,6 @@ protected:
|
||||||
ChainTxData chainTxData;
|
ChainTxData chainTxData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std::optional<ChainType> GetNetworkForMagic(MessageStartChars& pchMessageStart);
|
||||||
|
|
||||||
#endif // BITCOIN_KERNEL_CHAINPARAMS_H
|
#endif // BITCOIN_KERNEL_CHAINPARAMS_H
|
||||||
|
|
|
@ -6,16 +6,22 @@
|
||||||
#ifndef BITCOIN_NODE_UTXO_SNAPSHOT_H
|
#ifndef BITCOIN_NODE_UTXO_SNAPSHOT_H
|
||||||
#define BITCOIN_NODE_UTXO_SNAPSHOT_H
|
#define BITCOIN_NODE_UTXO_SNAPSHOT_H
|
||||||
|
|
||||||
|
#include <chainparams.h>
|
||||||
|
#include <kernel/chainparams.h>
|
||||||
#include <kernel/cs_main.h>
|
#include <kernel/cs_main.h>
|
||||||
#include <serialize.h>
|
#include <serialize.h>
|
||||||
#include <sync.h>
|
#include <sync.h>
|
||||||
#include <uint256.h>
|
#include <uint256.h>
|
||||||
|
#include <util/chaintype.h>
|
||||||
#include <util/fs.h>
|
#include <util/fs.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
|
// UTXO set snapshot magic bytes
|
||||||
|
static constexpr std::array<uint8_t, 5> SNAPSHOT_MAGIC_BYTES = {'u', 't', 'x', 'o', 0xff};
|
||||||
|
|
||||||
class Chainstate;
|
class Chainstate;
|
||||||
|
|
||||||
namespace node {
|
namespace node {
|
||||||
|
@ -23,10 +29,14 @@ namespace node {
|
||||||
//! assumeutxo Chainstate can be constructed.
|
//! assumeutxo Chainstate can be constructed.
|
||||||
class SnapshotMetadata
|
class SnapshotMetadata
|
||||||
{
|
{
|
||||||
|
const uint16_t m_version{1};
|
||||||
|
const std::set<uint16_t> m_supported_versions{1};
|
||||||
public:
|
public:
|
||||||
//! The hash of the block that reflects the tip of the chain for the
|
//! The hash of the block that reflects the tip of the chain for the
|
||||||
//! UTXO set contained in this snapshot.
|
//! UTXO set contained in this snapshot.
|
||||||
uint256 m_base_blockhash;
|
uint256 m_base_blockhash;
|
||||||
|
uint32_t m_base_blockheight;
|
||||||
|
|
||||||
|
|
||||||
//! The number of coins in the UTXO set contained in this snapshot. Used
|
//! The number of coins in the UTXO set contained in this snapshot. Used
|
||||||
//! during snapshot load to estimate progress of UTXO set reconstruction.
|
//! during snapshot load to estimate progress of UTXO set reconstruction.
|
||||||
|
@ -35,11 +45,55 @@ public:
|
||||||
SnapshotMetadata() { }
|
SnapshotMetadata() { }
|
||||||
SnapshotMetadata(
|
SnapshotMetadata(
|
||||||
const uint256& base_blockhash,
|
const uint256& base_blockhash,
|
||||||
|
const int base_blockheight,
|
||||||
uint64_t coins_count) :
|
uint64_t coins_count) :
|
||||||
m_base_blockhash(base_blockhash),
|
m_base_blockhash(base_blockhash),
|
||||||
|
m_base_blockheight(base_blockheight),
|
||||||
m_coins_count(coins_count) { }
|
m_coins_count(coins_count) { }
|
||||||
|
|
||||||
SERIALIZE_METHODS(SnapshotMetadata, obj) { READWRITE(obj.m_base_blockhash, obj.m_coins_count); }
|
template <typename Stream>
|
||||||
|
inline void Serialize(Stream& s) const {
|
||||||
|
s << SNAPSHOT_MAGIC_BYTES;
|
||||||
|
s << m_version;
|
||||||
|
s << Params().MessageStart();
|
||||||
|
s << m_base_blockheight;
|
||||||
|
s << m_base_blockhash;
|
||||||
|
s << m_coins_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Stream>
|
||||||
|
inline void Unserialize(Stream& s) {
|
||||||
|
// Read the snapshot magic bytes
|
||||||
|
std::array<uint8_t, SNAPSHOT_MAGIC_BYTES.size()> snapshot_magic;
|
||||||
|
s >> snapshot_magic;
|
||||||
|
if (snapshot_magic != SNAPSHOT_MAGIC_BYTES) {
|
||||||
|
throw std::ios_base::failure("Invalid UTXO set snapshot magic bytes. Please check if this is indeed a snapshot file or if you are using an outdated snapshot format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the version
|
||||||
|
uint16_t version;
|
||||||
|
s >> version;
|
||||||
|
if (m_supported_versions.find(version) == m_supported_versions.end()) {
|
||||||
|
throw std::ios_base::failure(strprintf("Version of snapshot %s does not match any of the supported versions.", version));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the network magic (pchMessageStart)
|
||||||
|
MessageStartChars message;
|
||||||
|
s >> message;
|
||||||
|
if (!std::equal(message.begin(), message.end(), Params().MessageStart().data())) {
|
||||||
|
auto metadata_network = GetNetworkForMagic(message);
|
||||||
|
if (metadata_network) {
|
||||||
|
std::string network_string{ChainTypeToString(metadata_network.value())};
|
||||||
|
throw std::ios_base::failure(strprintf("The network of the snapshot (%s) does not match the network of this node (%s).", network_string, Params().GetChainTypeString()));
|
||||||
|
} else {
|
||||||
|
throw std::ios_base::failure("This snapshot has been created for an unrecognized network. This could be a custom signet, a new testnet or possibly caused by data corruption.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s >> m_base_blockheight;
|
||||||
|
s >> m_base_blockhash;
|
||||||
|
s >> m_coins_count;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//! The file in the snapshot chainstate dir which stores the base blockhash. This is
|
//! The file in the snapshot chainstate dir which stores the base blockhash. This is
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
#include <rpc/server_util.h>
|
#include <rpc/server_util.h>
|
||||||
#include <rpc/util.h>
|
#include <rpc/util.h>
|
||||||
#include <script/descriptor.h>
|
#include <script/descriptor.h>
|
||||||
|
#include <serialize.h>
|
||||||
#include <streams.h>
|
#include <streams.h>
|
||||||
#include <sync.h>
|
#include <sync.h>
|
||||||
#include <txdb.h>
|
#include <txdb.h>
|
||||||
|
@ -2696,29 +2697,60 @@ UniValue CreateUTXOSnapshot(
|
||||||
tip->nHeight, tip->GetBlockHash().ToString(),
|
tip->nHeight, tip->GetBlockHash().ToString(),
|
||||||
fs::PathToString(path), fs::PathToString(temppath)));
|
fs::PathToString(path), fs::PathToString(temppath)));
|
||||||
|
|
||||||
SnapshotMetadata metadata{tip->GetBlockHash(), maybe_stats->coins_count};
|
SnapshotMetadata metadata{tip->GetBlockHash(), tip->nHeight, maybe_stats->coins_count};
|
||||||
|
|
||||||
afile << metadata;
|
afile << metadata;
|
||||||
|
|
||||||
COutPoint key;
|
COutPoint key;
|
||||||
|
Txid last_hash;
|
||||||
Coin coin;
|
Coin coin;
|
||||||
unsigned int iter{0};
|
unsigned int iter{0};
|
||||||
|
size_t written_coins_count{0};
|
||||||
|
std::vector<std::pair<uint32_t, Coin>> coins;
|
||||||
|
|
||||||
|
// To reduce space the serialization format of the snapshot avoids
|
||||||
|
// duplication of tx hashes. The code takes advantage of the guarantee by
|
||||||
|
// leveldb that keys are lexicographically sorted.
|
||||||
|
// In the coins vector we collect all coins that belong to a certain tx hash
|
||||||
|
// (key.hash) and when we have them all (key.hash != last_hash) we write
|
||||||
|
// them to file using the below lambda function.
|
||||||
|
// See also https://github.com/bitcoin/bitcoin/issues/25675
|
||||||
|
auto write_coins_to_file = [&](AutoFile& afile, const Txid& last_hash, const std::vector<std::pair<uint32_t, Coin>>& coins, size_t& written_coins_count) {
|
||||||
|
afile << last_hash;
|
||||||
|
WriteCompactSize(afile, coins.size());
|
||||||
|
for (const auto& [n, coin] : coins) {
|
||||||
|
WriteCompactSize(afile, n);
|
||||||
|
afile << coin;
|
||||||
|
++written_coins_count;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pcursor->GetKey(key);
|
||||||
|
last_hash = key.hash;
|
||||||
while (pcursor->Valid()) {
|
while (pcursor->Valid()) {
|
||||||
if (iter % 5000 == 0) node.rpc_interruption_point();
|
if (iter % 5000 == 0) node.rpc_interruption_point();
|
||||||
++iter;
|
++iter;
|
||||||
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
|
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
|
||||||
afile << key;
|
if (key.hash != last_hash) {
|
||||||
afile << coin;
|
write_coins_to_file(afile, last_hash, coins, written_coins_count);
|
||||||
|
last_hash = key.hash;
|
||||||
|
coins.clear();
|
||||||
|
}
|
||||||
|
coins.emplace_back(key.n, coin);
|
||||||
}
|
}
|
||||||
|
|
||||||
pcursor->Next();
|
pcursor->Next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!coins.empty()) {
|
||||||
|
write_coins_to_file(afile, last_hash, coins, written_coins_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK_NONFATAL(written_coins_count == maybe_stats->coins_count);
|
||||||
|
|
||||||
afile.fclose();
|
afile.fclose();
|
||||||
|
|
||||||
UniValue result(UniValue::VOBJ);
|
UniValue result(UniValue::VOBJ);
|
||||||
result.pushKV("coins_written", maybe_stats->coins_count);
|
result.pushKV("coins_written", written_coins_count);
|
||||||
result.pushKV("base_hash", tip->GetBlockHash().ToString());
|
result.pushKV("base_hash", tip->GetBlockHash().ToString());
|
||||||
result.pushKV("base_height", tip->nHeight);
|
result.pushKV("base_height", tip->nHeight);
|
||||||
result.pushKV("path", path.utf8string());
|
result.pushKV("path", path.utf8string());
|
||||||
|
@ -2778,12 +2810,22 @@ static RPCHelpMan loadtxoutset()
|
||||||
}
|
}
|
||||||
|
|
||||||
SnapshotMetadata metadata;
|
SnapshotMetadata metadata;
|
||||||
|
try {
|
||||||
afile >> metadata;
|
afile >> metadata;
|
||||||
|
} catch (const std::ios_base::failure& e) {
|
||||||
|
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("Unable to parse metadata: %s", e.what()));
|
||||||
|
}
|
||||||
|
|
||||||
uint256 base_blockhash = metadata.m_base_blockhash;
|
uint256 base_blockhash = metadata.m_base_blockhash;
|
||||||
|
int base_blockheight = metadata.m_base_blockheight;
|
||||||
if (!chainman.GetParams().AssumeutxoForBlockhash(base_blockhash).has_value()) {
|
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, "
|
throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Unable to load UTXO snapshot, "
|
||||||
"assumeutxo block hash in snapshot metadata not recognized (%s)", base_blockhash.ToString()));
|
"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,
|
CBlockIndex* snapshot_start_block = WITH_LOCK(::cs_main,
|
||||||
return chainman.m_blockman.LookupBlockIndex(base_blockhash));
|
return chainman.m_blockman.LookupBlockIndex(base_blockhash));
|
||||||
|
|
|
@ -226,10 +226,13 @@ struct SnapshotTestSetup : TestChain100Setup {
|
||||||
// A UTXO is missing but count is correct
|
// A UTXO is missing but count is correct
|
||||||
metadata.m_coins_count -= 1;
|
metadata.m_coins_count -= 1;
|
||||||
|
|
||||||
COutPoint outpoint;
|
Txid txid;
|
||||||
|
auto_infile >> txid;
|
||||||
|
// coins size
|
||||||
|
(void)ReadCompactSize(auto_infile);
|
||||||
|
// vout index
|
||||||
|
(void)ReadCompactSize(auto_infile);
|
||||||
Coin coin;
|
Coin coin;
|
||||||
|
|
||||||
auto_infile >> outpoint;
|
|
||||||
auto_infile >> coin;
|
auto_infile >> coin;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -5666,23 +5666,30 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
COutPoint outpoint;
|
|
||||||
Coin coin;
|
|
||||||
const uint64_t coins_count = metadata.m_coins_count;
|
const uint64_t coins_count = metadata.m_coins_count;
|
||||||
uint64_t coins_left = metadata.m_coins_count;
|
uint64_t coins_left = metadata.m_coins_count;
|
||||||
|
|
||||||
LogPrintf("[snapshot] loading coins from snapshot %s\n", base_blockhash.ToString());
|
LogPrintf("[snapshot] loading %d coins from snapshot %s\n", coins_left, base_blockhash.ToString());
|
||||||
int64_t coins_processed{0};
|
int64_t coins_processed{0};
|
||||||
|
|
||||||
while (coins_left > 0) {
|
while (coins_left > 0) {
|
||||||
try {
|
try {
|
||||||
coins_file >> outpoint;
|
Txid txid;
|
||||||
coins_file >> coin;
|
coins_file >> txid;
|
||||||
} catch (const std::ios_base::failure&) {
|
size_t coins_per_txid{0};
|
||||||
LogPrintf("[snapshot] bad snapshot format or truncated snapshot after deserializing %d coins\n",
|
coins_per_txid = ReadCompactSize(coins_file);
|
||||||
coins_count - coins_left);
|
|
||||||
|
if (coins_per_txid > coins_left) {
|
||||||
|
LogPrintf("[snapshot] mismatch in coins count in snapshot metadata and actual snapshot data\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < coins_per_txid; i++) {
|
||||||
|
COutPoint outpoint;
|
||||||
|
Coin coin;
|
||||||
|
outpoint.n = static_cast<uint32_t>(ReadCompactSize(coins_file));
|
||||||
|
outpoint.hash = txid;
|
||||||
|
coins_file >> coin;
|
||||||
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
|
||||||
) {
|
) {
|
||||||
|
@ -5695,7 +5702,6 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
|
||||||
coins_count - coins_left);
|
coins_count - coins_left);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
coins_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
|
coins_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
|
||||||
|
|
||||||
--coins_left;
|
--coins_left;
|
||||||
|
@ -5731,6 +5737,12 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (const std::ios_base::failure&) {
|
||||||
|
LogPrintf("[snapshot] bad snapshot format or truncated snapshot after deserializing %d coins\n",
|
||||||
|
coins_processed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Important that we set this. This and the coins_cache accesses above are
|
// Important that we set this. This and the coins_cache accesses above are
|
||||||
// sort of a layer violation, but either we reach into the innards of
|
// sort of a layer violation, but either we reach into the innards of
|
||||||
|
@ -5741,7 +5753,8 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
|
||||||
|
|
||||||
bool out_of_coins{false};
|
bool out_of_coins{false};
|
||||||
try {
|
try {
|
||||||
coins_file >> outpoint;
|
Txid txid;
|
||||||
|
coins_file >> txid;
|
||||||
} catch (const std::ios_base::failure&) {
|
} catch (const std::ios_base::failure&) {
|
||||||
// We expect an exception since we should be out of coins.
|
// We expect an exception since we should be out of coins.
|
||||||
out_of_coins = true;
|
out_of_coins = true;
|
||||||
|
|
|
@ -885,6 +885,12 @@ private:
|
||||||
CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr};
|
CBlockIndex* m_best_invalid GUARDED_BY(::cs_main){nullptr};
|
||||||
|
|
||||||
//! Internal helper for ActivateSnapshot().
|
//! Internal helper for ActivateSnapshot().
|
||||||
|
//!
|
||||||
|
//! De-serialization of a snapshot that is created with
|
||||||
|
//! CreateUTXOSnapshot() in rpc/blockchain.cpp.
|
||||||
|
//! To reduce space the serialization format of the snapshot avoids
|
||||||
|
//! duplication of tx hashes. The code takes advantage of the guarantee by
|
||||||
|
//! leveldb that keys are lexicographically sorted.
|
||||||
[[nodiscard]] bool PopulateAndValidateSnapshot(
|
[[nodiscard]] bool PopulateAndValidateSnapshot(
|
||||||
Chainstate& snapshot_chainstate,
|
Chainstate& snapshot_chainstate,
|
||||||
AutoFile& coins_file,
|
AutoFile& coins_file,
|
||||||
|
|
|
@ -75,41 +75,75 @@ class AssumeutxoTest(BitcoinTestFramework):
|
||||||
with self.nodes[1].assert_debug_log([log_msg]):
|
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)
|
assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", self.nodes[1].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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.log.info(" - snapshot file with mismatching network magic")
|
||||||
|
invalid_magics = [
|
||||||
|
# magic, name, real
|
||||||
|
[0xf9beb4d9, "main", True],
|
||||||
|
[0x0b110907, "test", True],
|
||||||
|
[0x0a03cf40, "signet", True],
|
||||||
|
[0x00000000, "", False],
|
||||||
|
[0xffffffff, "", False],
|
||||||
|
]
|
||||||
|
for [magic, name, real] in invalid_magics:
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
self.log.info(" - snapshot file referring to a block that is not in the assumeutxo parameters")
|
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)
|
prev_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1)
|
||||||
bogus_block_hash = "0" * 64 # Represents any unknown block hash
|
bogus_block_hash = "0" * 64 # Represents any unknown block hash
|
||||||
|
# The height is not used for anything critical currently, so we just
|
||||||
|
# confirm the manipulation in the error message
|
||||||
|
bogus_height = 1337
|
||||||
for bad_block_hash in [bogus_block_hash, prev_block_hash]:
|
for bad_block_hash in [bogus_block_hash, prev_block_hash]:
|
||||||
with open(bad_snapshot_path, 'wb') as f:
|
with open(bad_snapshot_path, 'wb') as f:
|
||||||
# block hash of the snapshot base is stored right at the start (first 32 bytes)
|
f.write(valid_snapshot_contents[:11] + bogus_height.to_bytes(4, "little") + bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[47:])
|
||||||
f.write(bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[32:])
|
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."
|
||||||
error_details = f", assumeutxo block hash in snapshot metadata not recognized ({bad_block_hash})"
|
|
||||||
expected_error(rpc_details=error_details)
|
expected_error(rpc_details=error_details)
|
||||||
|
|
||||||
self.log.info(" - snapshot file with wrong number of coins")
|
self.log.info(" - snapshot file with wrong number of coins")
|
||||||
valid_num_coins = int.from_bytes(valid_snapshot_contents[32:32 + 8], "little")
|
valid_num_coins = int.from_bytes(valid_snapshot_contents[47:47 + 8], "little")
|
||||||
for off in [-1, +1]:
|
for off in [-1, +1]:
|
||||||
with open(bad_snapshot_path, 'wb') as f:
|
with open(bad_snapshot_path, 'wb') as f:
|
||||||
f.write(valid_snapshot_contents[:32])
|
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[32 + 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(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")
|
||||||
|
|
||||||
self.log.info(" - snapshot file with alternated UTXO data")
|
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
|
||||||
[(1).to_bytes(4, "little"), 32, "9f4d897031ab8547665b4153317ae2fdbf0130c7840b66427ebc48b881cb80ad", None], # wrong outpoint index
|
[(2).to_bytes(1, "little"), 32, None, "[snapshot] bad snapshot data after deserializing 1 coins"], # wrong outpoint hash
|
||||||
[b"\x81", 36, "3da966ba9826fb6d2604260e01607b55ba44e1a5de298606b08704bc62570ea8", None], # wrong coin code VARINT
|
[b"\x01", 33, "9f4d897031ab8547665b4153317ae2fdbf0130c7840b66427ebc48b881cb80ad", None], # wrong outpoint index
|
||||||
[b"\x80", 36, "091e893b3ccb4334378709578025356c8bcb0a623f37c7c4e493133c988648e5", None], # another wrong coin code
|
[b"\x81", 34, "3da966ba9826fb6d2604260e01607b55ba44e1a5de298606b08704bc62570ea8", None], # wrong coin code VARINT
|
||||||
[b"\x84\x58", 36, None, "[snapshot] bad snapshot data after deserializing 0 coins"], # wrong coin case with height 364 and coinbase 0
|
[b"\x80", 34, "091e893b3ccb4334378709578025356c8bcb0a623f37c7c4e493133c988648e5", None], # another wrong coin code
|
||||||
[b"\xCA\xD2\x8F\x5A", 41, None, "[snapshot] bad snapshot data after deserializing 0 coins - bad tx out value"], # Amount exceeds MAX_MONEY
|
[b"\x84\x58", 34, None, "[snapshot] 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
|
||||||
]
|
]
|
||||||
|
|
||||||
for content, offset, wrong_hash, custom_message in cases:
|
for content, offset, wrong_hash, custom_message in cases:
|
||||||
with open(bad_snapshot_path, "wb") as f:
|
with open(bad_snapshot_path, "wb") as f:
|
||||||
f.write(valid_snapshot_contents[:(32 + 8 + offset)])
|
# Prior to offset: Snapshot magic, snapshot version, network magic, height, hash, coins count
|
||||||
|
f.write(valid_snapshot_contents[:(5 + 2 + 4 + 4 + 32 + 8 + offset)])
|
||||||
f.write(content)
|
f.write(content)
|
||||||
f.write(valid_snapshot_contents[(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}"
|
log_msg = custom_message if custom_message is not None else f"[snapshot] bad snapshot content hash: expected a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27, got {wrong_hash}"
|
||||||
expected_error(log_msg=log_msg)
|
expected_error(log_msg=log_msg)
|
||||||
|
|
|
@ -43,7 +43,7 @@ class DumptxoutsetTest(BitcoinTestFramework):
|
||||||
# UTXO snapshot hash should be deterministic based on mocked time.
|
# UTXO snapshot hash should be deterministic based on mocked time.
|
||||||
assert_equal(
|
assert_equal(
|
||||||
sha256sum_file(str(expected_path)).hex(),
|
sha256sum_file(str(expected_path)).hex(),
|
||||||
'b1bacb602eacf5fbc9a7c2ef6eeb0d229c04e98bdf0c2ea5929012cd0eae3830')
|
'2f775f82811150d310527b5ff773f81fb0fb517e941c543c1f7c4d38fd2717b3')
|
||||||
|
|
||||||
assert_equal(
|
assert_equal(
|
||||||
out['txoutset_hash'], 'a0b7baa3bf5ccbd3279728f230d7ca0c44a76e9923fca8f32dbfd08d65ea496a')
|
out['txoutset_hash'], 'a0b7baa3bf5ccbd3279728f230d7ca0c44a76e9923fca8f32dbfd08d65ea496a')
|
||||||
|
|
Loading…
Add table
Reference in a new issue