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