Skip to content

Commit 542e13b

Browse files
committed
rpc: Enhance metadata of the dumptxoutset output
The following data is added: - A newly introduced utxo set magic - A version number - The network magic - The block height
1 parent 4d8e5ed commit 542e13b

File tree

6 files changed

+143
-14
lines changed

6 files changed

+143
-14
lines changed

src/kernel/chainparams.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,3 +542,33 @@ std::unique_ptr<const CChainParams> CChainParams::TestNet()
542542
{
543543
return std::make_unique<const CTestNetParams>();
544544
}
545+
546+
std::vector<int> CChainParams::GetAvailableSnapshotHeights() const
547+
{
548+
std::vector<int> heights;
549+
heights.reserve(m_assumeutxo_data.size());
550+
551+
for (const auto& data : m_assumeutxo_data) {
552+
heights.emplace_back(data.height);
553+
}
554+
return heights;
555+
}
556+
557+
std::optional<ChainType> GetNetworkForMagic(MessageStartChars& message)
558+
{
559+
const auto mainnet_msg = CChainParams::Main()->MessageStart();
560+
const auto testnet_msg = CChainParams::TestNet()->MessageStart();
561+
const auto regtest_msg = CChainParams::RegTest({})->MessageStart();
562+
const auto signet_msg = CChainParams::SigNet({})->MessageStart();
563+
564+
if (std::equal(message.begin(), message.end(), mainnet_msg.data())) {
565+
return ChainType::MAIN;
566+
} else if (std::equal(message.begin(), message.end(), testnet_msg.data())) {
567+
return ChainType::TESTNET;
568+
} else if (std::equal(message.begin(), message.end(), regtest_msg.data())) {
569+
return ChainType::REGTEST;
570+
} else if (std::equal(message.begin(), message.end(), signet_msg.data())) {
571+
return ChainType::SIGNET;
572+
}
573+
return std::nullopt;
574+
}

src/kernel/chainparams.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class CChainParams
9393
const Consensus::Params& GetConsensus() const { return consensus; }
9494
const MessageStartChars& MessageStart() const { return pchMessageStart; }
9595
uint16_t GetDefaultPort() const { return nDefaultPort; }
96+
std::vector<int> GetAvailableSnapshotHeights() const;
9697

9798
const CBlock& GenesisBlock() const { return genesis; }
9899
/** Default value for -checkmempool and -checkblockindex argument */
@@ -183,4 +184,6 @@ class CChainParams
183184
ChainTxData chainTxData;
184185
};
185186

187+
std::optional<ChainType> GetNetworkForMagic(MessageStartChars& pchMessageStart);
188+
186189
#endif // BITCOIN_KERNEL_CHAINPARAMS_H

src/node/utxo_snapshot.h

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,37 @@
66
#ifndef BITCOIN_NODE_UTXO_SNAPSHOT_H
77
#define BITCOIN_NODE_UTXO_SNAPSHOT_H
88

9+
#include <chainparams.h>
10+
#include <kernel/chainparams.h>
911
#include <kernel/cs_main.h>
1012
#include <serialize.h>
1113
#include <sync.h>
1214
#include <uint256.h>
15+
#include <util/chaintype.h>
1316
#include <util/fs.h>
1417

1518
#include <cstdint>
1619
#include <optional>
1720
#include <string_view>
1821

22+
// UTXO set snapshot magic bytes
23+
static constexpr std::array<uint8_t, 5> SNAPSHOT_MAGIC_BYTES = {'u', 't', 'x', 'o', 0xff};
24+
1925
class Chainstate;
2026

2127
namespace node {
2228
//! Metadata describing a serialized version of a UTXO set from which an
2329
//! assumeutxo Chainstate can be constructed.
2430
class SnapshotMetadata
2531
{
32+
const uint16_t m_version{1};
33+
const std::set<uint16_t> m_supported_versions{1};
2634
public:
2735
//! The hash of the block that reflects the tip of the chain for the
2836
//! UTXO set contained in this snapshot.
2937
uint256 m_base_blockhash;
38+
uint32_t m_base_blockheight;
39+
3040

3141
//! The number of coins in the UTXO set contained in this snapshot. Used
3242
//! during snapshot load to estimate progress of UTXO set reconstruction.
@@ -35,11 +45,55 @@ class SnapshotMetadata
3545
SnapshotMetadata() { }
3646
SnapshotMetadata(
3747
const uint256& base_blockhash,
48+
const int base_blockheight,
3849
uint64_t coins_count) :
3950
m_base_blockhash(base_blockhash),
51+
m_base_blockheight(base_blockheight),
4052
m_coins_count(coins_count) { }
4153

42-
SERIALIZE_METHODS(SnapshotMetadata, obj) { READWRITE(obj.m_base_blockhash, obj.m_coins_count); }
54+
template <typename Stream>
55+
inline void Serialize(Stream& s) const {
56+
s << SNAPSHOT_MAGIC_BYTES;
57+
s << m_version;
58+
s << Params().MessageStart();
59+
s << m_base_blockheight;
60+
s << m_base_blockhash;
61+
s << m_coins_count;
62+
}
63+
64+
template <typename Stream>
65+
inline void Unserialize(Stream& s) {
66+
// Read the snapshot magic bytes
67+
std::array<uint8_t, SNAPSHOT_MAGIC_BYTES.size()> snapshot_magic;
68+
s >> snapshot_magic;
69+
if (snapshot_magic != SNAPSHOT_MAGIC_BYTES) {
70+
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.");
71+
}
72+
73+
// Read the version
74+
uint16_t version;
75+
s >> version;
76+
if (m_supported_versions.find(version) == m_supported_versions.end()) {
77+
throw std::ios_base::failure(strprintf("Version of snapshot %s does not match any of the supported versions.", version));
78+
}
79+
80+
// Read the network magic (pchMessageStart)
81+
MessageStartChars message;
82+
s >> message;
83+
if (!std::equal(message.begin(), message.end(), Params().MessageStart().data())) {
84+
auto metadata_network = GetNetworkForMagic(message);
85+
if (metadata_network) {
86+
std::string network_string{ChainTypeToString(metadata_network.value())};
87+
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()));
88+
} else {
89+
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.");
90+
}
91+
}
92+
93+
s >> m_base_blockheight;
94+
s >> m_base_blockhash;
95+
s >> m_coins_count;
96+
}
4397
};
4498

4599
//! The file in the snapshot chainstate dir which stores the base blockhash. This is

src/rpc/blockchain.cpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2691,7 +2691,7 @@ UniValue CreateUTXOSnapshot(
26912691
tip->nHeight, tip->GetBlockHash().ToString(),
26922692
fs::PathToString(path), fs::PathToString(temppath)));
26932693

2694-
SnapshotMetadata metadata{tip->GetBlockHash(), maybe_stats->coins_count};
2694+
SnapshotMetadata metadata{tip->GetBlockHash(), tip->nHeight, maybe_stats->coins_count};
26952695

26962696
afile << metadata;
26972697

@@ -2804,12 +2804,22 @@ static RPCHelpMan loadtxoutset()
28042804
}
28052805

28062806
SnapshotMetadata metadata;
2807-
afile >> metadata;
2807+
try {
2808+
afile >> metadata;
2809+
} catch (const std::ios_base::failure& e) {
2810+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("Unable to parse metadata: %s", e.what()));
2811+
}
28082812

28092813
uint256 base_blockhash = metadata.m_base_blockhash;
2814+
int base_blockheight = metadata.m_base_blockheight;
28102815
if (!chainman.GetParams().AssumeutxoForBlockhash(base_blockhash).has_value()) {
2816+
auto available_heights = chainman.GetParams().GetAvailableSnapshotHeights();
2817+
std::string heights_formatted = Join(available_heights, ", ", [&](const auto& i) { return ToString(i); });
28112818
throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Unable to load UTXO snapshot, "
2812-
"assumeutxo block hash in snapshot metadata not recognized (%s)", base_blockhash.ToString()));
2819+
"assumeutxo block hash in snapshot metadata not recognized (hash: %s, height: %s). The following snapshot heights are available: %s.",
2820+
base_blockhash.ToString(),
2821+
base_blockheight,
2822+
heights_formatted));
28132823
}
28142824
CBlockIndex* snapshot_start_block = WITH_LOCK(::cs_main,
28152825
return chainman.m_blockman.LookupBlockIndex(base_blockhash));

test/functional/feature_assumeutxo.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,26 +75,57 @@ def expected_error(log_msg="", rpc_details=""):
7575
with self.nodes[1].assert_debug_log([log_msg]):
7676
assert_raises_rpc_error(-32603, f"Unable to load UTXO snapshot{rpc_details}", self.nodes[1].loadtxoutset, bad_snapshot_path)
7777

78+
self.log.info(" - snapshot file with invalid file magic")
79+
parsing_error_code = -22
80+
bad_magic = 0xf00f00f000
81+
with open(bad_snapshot_path, 'wb') as f:
82+
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+
85+
self.log.info(" - snapshot file with unsupported version")
86+
for version in [0, 2]:
87+
with open(bad_snapshot_path, 'wb') as f:
88+
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+
91+
self.log.info(" - snapshot file with mismatching network magic")
92+
invalid_magics = [
93+
# magic, name, real
94+
[0xf9beb4d9, "main", True],
95+
[0x0b110907, "test", True],
96+
[0x0a03cf40, "signet", True],
97+
[0x00000000, "", False],
98+
[0xffffffff, "", False],
99+
]
100+
for [magic, name, real] in invalid_magics:
101+
with open(bad_snapshot_path, 'wb') as f:
102+
f.write(valid_snapshot_contents[:7] + magic.to_bytes(4, 'big') + valid_snapshot_contents[11:])
103+
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+
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+
78108
self.log.info(" - snapshot file referring to a block that is not in the assumeutxo parameters")
79109
prev_block_hash = self.nodes[0].getblockhash(SNAPSHOT_BASE_HEIGHT - 1)
80110
bogus_block_hash = "0" * 64 # Represents any unknown block hash
111+
# The height is not used for anything critical currently, so we just
112+
# confirm the manipulation in the error message
113+
bogus_height = 1337
81114
for bad_block_hash in [bogus_block_hash, prev_block_hash]:
82115
with open(bad_snapshot_path, 'wb') as f:
83-
# block hash of the snapshot base is stored right at the start (first 32 bytes)
84-
f.write(bytes.fromhex(bad_block_hash)[::-1] + valid_snapshot_contents[32:])
85-
error_details = f", assumeutxo block hash in snapshot metadata not recognized ({bad_block_hash})"
116+
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."
86118
expected_error(rpc_details=error_details)
87119

88120
self.log.info(" - snapshot file with wrong number of coins")
89-
valid_num_coins = int.from_bytes(valid_snapshot_contents[32:32 + 8], "little")
121+
valid_num_coins = int.from_bytes(valid_snapshot_contents[47:47 + 8], "little")
90122
for off in [-1, +1]:
91123
with open(bad_snapshot_path, 'wb') as f:
92-
f.write(valid_snapshot_contents[:32])
124+
f.write(valid_snapshot_contents[:47])
93125
f.write((valid_num_coins + off).to_bytes(8, "little"))
94-
f.write(valid_snapshot_contents[32 + 8:])
126+
f.write(valid_snapshot_contents[47 + 8:])
95127
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")
96128

97-
98129
self.log.info(" - snapshot file with alternated but parsable UTXO data results in different hash")
99130
cases = [
100131
# (content, offset, wrong_hash, custom_message)
@@ -109,9 +140,10 @@ def expected_error(log_msg="", rpc_details=""):
109140

110141
for content, offset, wrong_hash, custom_message in cases:
111142
with open(bad_snapshot_path, "wb") as f:
112-
f.write(valid_snapshot_contents[:(32 + 8 + offset)])
143+
# Prior to offset: Snapshot magic, snapshot version, network magic, height, hash, coins count
144+
f.write(valid_snapshot_contents[:(5 + 2 + 4 + 4 + 32 + 8 + offset)])
113145
f.write(content)
114-
f.write(valid_snapshot_contents[(32 + 8 + offset + len(content)):])
146+
f.write(valid_snapshot_contents[(5 + 2 + 4 + 4 + 32 + 8 + offset + len(content)):])
115147

116148
log_msg = custom_message if custom_message is not None else f"[snapshot] bad snapshot content hash: expected a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27, got {wrong_hash}"
117149
expected_error(log_msg=log_msg)

test/functional/rpc_dumptxoutset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def run_test(self):
4343
# UTXO snapshot hash should be deterministic based on mocked time.
4444
assert_equal(
4545
sha256sum_file(str(expected_path)).hex(),
46-
'3263fc0311ea46415b85513a59ad8fe67806b3cdce66147175ecb9da768d4a99')
46+
'2f775f82811150d310527b5ff773f81fb0fb517e941c543c1f7c4d38fd2717b3')
4747

4848
assert_equal(
4949
out['txoutset_hash'], 'a0b7baa3bf5ccbd3279728f230d7ca0c44a76e9923fca8f32dbfd08d65ea496a')

0 commit comments

Comments
 (0)