/ src / test / fuzz / utxo_snapshot.cpp
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