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