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