utxo_snapshot.cpp raw
1 // Copyright (c) 2021-present The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <chain.h>
6 #include <chainparams.h>
7 #include <coins.h>
8 #include <consensus/consensus.h>
9 #include <consensus/validation.h>
10 #include <node/blockstorage.h>
11 #include <node/utxo_snapshot.h>
12 #include <primitives/block.h>
13 #include <primitives/transaction.h>
14 #include <serialize.h>
15 #include <span.h>
16 #include <streams.h>
17 #include <sync.h>
18 #include <test/fuzz/FuzzedDataProvider.h>
19 #include <test/fuzz/fuzz.h>
20 #include <test/fuzz/util.h>
21 #include <test/util/mining.h>
22 #include <test/util/setup_common.h>
23 #include <uint256.h>
24 #include <util/check.h>
25 #include <util/fs.h>
26 #include <util/result.h>
27 #include <util/time.h>
28 #include <validation.h>
29
30 #include <cstdint>
31 #include <functional>
32 #include <ios>
33 #include <memory>
34 #include <optional>
35 #include <vector>
36
37 using node::SnapshotMetadata;
38
39 namespace {
40
41 const std::vector<std::shared_ptr<CBlock>>* g_chain;
42 TestingSetup* g_setup;
43
44 template <bool INVALID>
45 void initialize_chain()
46 {
47 const auto params{CreateChainParams(ArgsManager{}, ChainType::REGTEST)};
48 static const auto chain{CreateBlockChain(2 * COINBASE_MATURITY, *params)};
49 g_chain = &chain;
50 static const auto setup{
51 MakeNoLogFileContext<TestingSetup>(ChainType::REGTEST,
52 TestOpts{
53 .setup_net = false,
54 .setup_validation_interface = false,
55 .min_validation_cache = true,
56 }),
57 };
58 if constexpr (INVALID) {
59 auto& chainman{*setup->m_node.chainman};
60 for (const auto& block : chain) {
61 BlockValidationState dummy;
62 bool processed{chainman.ProcessNewBlockHeaders({{block->GetBlockHeader()}}, true, dummy)};
63 Assert(processed);
64 const auto* index{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(block->GetHash()))};
65 Assert(index);
66 }
67 }
68 g_setup = setup.get();
69 }
70
71 template <bool INVALID>
72 void utxo_snapshot_fuzz(FuzzBufferType buffer)
73 {
74 SeedRandomStateForTest(SeedRand::ZEROS);
75 FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
76 SetMockTime(ConsumeTime(fuzzed_data_provider, /*min=*/1296688602)); // regtest genesis block timestamp
77 auto& setup{*g_setup};
78 bool dirty_chainman{false}; // Re-use the global chainman, but reset it when it is dirty
79 auto& chainman{*setup.m_node.chainman};
80
81 const auto snapshot_path = gArgs.GetDataDirNet() / "fuzzed_snapshot.dat";
82
83 Assert(!chainman.SnapshotBlockhash());
84
85 {
86 AutoFile outfile{fsbridge::fopen(snapshot_path, "wb")};
87 // Metadata
88 if (fuzzed_data_provider.ConsumeBool()) {
89 std::vector<uint8_t> metadata{ConsumeRandomLengthByteVector(fuzzed_data_provider)};
90 outfile << Span{metadata};
91 } else {
92 auto msg_start = chainman.GetParams().MessageStart();
93 int base_blockheight{fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 2 * COINBASE_MATURITY)};
94 uint256 base_blockhash{g_chain->at(base_blockheight - 1)->GetHash()};
95 uint64_t m_coins_count{fuzzed_data_provider.ConsumeIntegralInRange<uint64_t>(1, 3 * COINBASE_MATURITY)};
96 SnapshotMetadata metadata{msg_start, base_blockhash, m_coins_count};
97 outfile << metadata;
98 }
99 // Coins
100 if (fuzzed_data_provider.ConsumeBool()) {
101 std::vector<uint8_t> file_data{ConsumeRandomLengthByteVector(fuzzed_data_provider)};
102 outfile << Span{file_data};
103 } else {
104 int height{0};
105 for (const auto& block : *g_chain) {
106 auto coinbase{block->vtx.at(0)};
107 outfile << coinbase->GetHash();
108 WriteCompactSize(outfile, 1); // number of coins for the hash
109 WriteCompactSize(outfile, 0); // index of coin
110 outfile << Coin(coinbase->vout[0], height, /*fCoinBaseIn=*/1);
111 height++;
112 }
113 }
114 if constexpr (INVALID) {
115 // Append an invalid coin to ensure invalidity. This error will be
116 // detected late in PopulateAndValidateSnapshot, and allows the
117 // INVALID fuzz target to reach more potential code coverage.
118 const auto& coinbase{g_chain->back()->vtx.back()};
119 outfile << coinbase->GetHash();
120 WriteCompactSize(outfile, 1); // number of coins for the hash
121 WriteCompactSize(outfile, 999); // index of coin
122 outfile << Coin{coinbase->vout[0], /*nHeightIn=*/999, /*fCoinBaseIn=*/0};
123 }
124 assert(outfile.fclose() == 0);
125 }
126
127 const auto ActivateFuzzedSnapshot{[&] {
128 AutoFile infile{fsbridge::fopen(snapshot_path, "rb")};
129 auto msg_start = chainman.GetParams().MessageStart();
130 SnapshotMetadata metadata{msg_start};
131 try {
132 infile >> metadata;
133 } catch (const std::ios_base::failure&) {
134 return false;
135 }
136 return !!chainman.ActivateSnapshot(infile, metadata, /*in_memory=*/true);
137 }};
138
139 if (fuzzed_data_provider.ConsumeBool()) {
140 // Consume the bool, but skip the code for the INVALID fuzz target
141 if constexpr (!INVALID) {
142 for (const auto& block : *g_chain) {
143 BlockValidationState dummy;
144 bool processed{chainman.ProcessNewBlockHeaders({{block->GetBlockHeader()}}, true, dummy)};
145 Assert(processed);
146 const auto* index{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(block->GetHash()))};
147 Assert(index);
148 }
149 dirty_chainman = true;
150 }
151 }
152
153 if (ActivateFuzzedSnapshot()) {
154 LOCK(::cs_main);
155 Assert(!chainman.ActiveChainstate().m_from_snapshot_blockhash->IsNull());
156 Assert(*chainman.ActiveChainstate().m_from_snapshot_blockhash ==
157 *chainman.SnapshotBlockhash());
158 const auto& coinscache{chainman.ActiveChainstate().CoinsTip()};
159 for (const auto& block : *g_chain) {
160 Assert(coinscache.HaveCoin(COutPoint{block->vtx.at(0)->GetHash(), 0}));
161 const auto* index{chainman.m_blockman.LookupBlockIndex(block->GetHash())};
162 Assert(index);
163 Assert(index->nTx == 0);
164 if (index->nHeight == chainman.GetSnapshotBaseHeight()) {
165 auto params{chainman.GetParams().AssumeutxoForHeight(index->nHeight)};
166 Assert(params.has_value());
167 Assert(params.value().m_chain_tx_count == index->m_chain_tx_count);
168 } else {
169 Assert(index->m_chain_tx_count == 0);
170 }
171 }
172 Assert(g_chain->size() == coinscache.GetCacheSize());
173 dirty_chainman = true;
174 } else {
175 Assert(!chainman.SnapshotBlockhash());
176 Assert(!chainman.ActiveChainstate().m_from_snapshot_blockhash);
177 }
178 // Snapshot should refuse to load a second time regardless of validity
179 Assert(!ActivateFuzzedSnapshot());
180 if constexpr (INVALID) {
181 // Activating the snapshot, or any other action that makes the chainman
182 // "dirty" can and must not happen for the INVALID fuzz target
183 Assert(!dirty_chainman);
184 }
185 if (dirty_chainman) {
186 setup.m_node.chainman.reset();
187 setup.m_make_chainman();
188 setup.LoadVerifyActivateChainstate();
189 }
190 }
191
192 // There are two fuzz targets:
193 //
194 // The target 'utxo_snapshot', which allows valid snapshots, but is slow,
195 // because it has to reset the chainstate manager on almost all fuzz inputs.
196 // Otherwise, a dirty header tree or dirty chainstate could leak from one fuzz
197 // input execution into the next, which makes execution non-deterministic.
198 //
199 // The target 'utxo_snapshot_invalid', which is fast and does not require any
200 // expensive state to be reset.
201 FUZZ_TARGET(utxo_snapshot /*valid*/, .init = initialize_chain<false>) { utxo_snapshot_fuzz<false>(buffer); }
202 FUZZ_TARGET(utxo_snapshot_invalid, .init = initialize_chain<true>) { utxo_snapshot_fuzz<true>(buffer); }
203
204 } // namespace
205