utxo_snapshot.cpp raw

   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      FakeNodeClock clock{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
 233