/ src / test / fuzz / coins_view.cpp
coins_view.cpp
  1  // Copyright (c) 2020-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 <coins.h>
  6  #include <consensus/amount.h>
  7  #include <consensus/tx_check.h>
  8  #include <consensus/tx_verify.h>
  9  #include <consensus/validation.h>
 10  #include <policy/policy.h>
 11  #include <primitives/transaction.h>
 12  #include <script/interpreter.h>
 13  #include <test/fuzz/FuzzedDataProvider.h>
 14  #include <test/fuzz/fuzz.h>
 15  #include <test/fuzz/util.h>
 16  #include <test/util/setup_common.h>
 17  #include <txdb.h>
 18  #include <util/hasher.h>
 19  
 20  #include <cassert>
 21  #include <algorithm>
 22  #include <cstdint>
 23  #include <functional>
 24  #include <limits>
 25  #include <memory>
 26  #include <optional>
 27  #include <ranges>
 28  #include <stdexcept>
 29  #include <string>
 30  #include <utility>
 31  #include <vector>
 32  
 33  namespace {
 34  const Coin EMPTY_COIN{};
 35  
 36  bool operator==(const Coin& a, const Coin& b)
 37  {
 38      if (a.IsSpent() && b.IsSpent()) return true;
 39      return a.fCoinBase == b.fCoinBase && a.nHeight == b.nHeight && a.out == b.out;
 40  }
 41  
 42  /**
 43   * MutationGuardCoinsViewCache asserts that nothing mutates cacheCoins until
 44   * BatchWrite is called. It keeps a snapshot of the cacheCoins state, which it
 45   * uses for the assertion in BatchWrite. After the call to the superclass
 46   * CCoinsViewCache::BatchWrite returns, it recomputes the snapshot at that
 47   * moment.
 48   */
 49  class MutationGuardCoinsViewCache final : public CCoinsViewCache
 50  {
 51  private:
 52      struct CacheCoinSnapshot {
 53          COutPoint outpoint;
 54          bool dirty{false};
 55          bool fresh{false};
 56          Coin coin;
 57          bool operator==(const CacheCoinSnapshot&) const = default;
 58      };
 59  
 60      std::vector<CacheCoinSnapshot> ComputeCacheCoinsSnapshot() const
 61      {
 62          std::vector<CacheCoinSnapshot> snapshot;
 63          snapshot.reserve(cacheCoins.size());
 64  
 65          for (const auto& [outpoint, entry] : cacheCoins) {
 66              snapshot.emplace_back(outpoint, entry.IsDirty(), entry.IsFresh(), entry.coin);
 67          }
 68  
 69          std::ranges::sort(snapshot, std::less<>{}, &CacheCoinSnapshot::outpoint);
 70          return snapshot;
 71      }
 72  
 73      mutable std::vector<CacheCoinSnapshot> m_expected_snapshot{ComputeCacheCoinsSnapshot()};
 74  
 75  public:
 76      void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override
 77      {
 78          // Nothing must modify cacheCoins other than BatchWrite.
 79          assert(ComputeCacheCoinsSnapshot() == m_expected_snapshot);
 80          CCoinsViewCache::BatchWrite(cursor, block_hash);
 81          m_expected_snapshot = ComputeCacheCoinsSnapshot();
 82      }
 83  
 84      using CCoinsViewCache::CCoinsViewCache;
 85  };
 86  } // namespace
 87  
 88  void initialize_coins_view()
 89  {
 90      static const auto testing_setup = MakeNoLogFileContext<>();
 91  }
 92  
 93  void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& coins_view_cache, CCoinsView& backend_coins_view, bool is_db)
 94  {
 95      bool good_data{true};
 96  
 97      if (is_db) coins_view_cache.SetBestBlock(uint256::ONE);
 98      COutPoint random_out_point;
 99      Coin random_coin;
100      CMutableTransaction random_mutable_transaction;
101      LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000)
102      {
103          CallOneOf(
104              fuzzed_data_provider,
105              [&] {
106                  if (random_coin.IsSpent()) {
107                      return;
108                  }
109                  COutPoint outpoint{random_out_point};
110                  Coin coin{random_coin};
111                  if (fuzzed_data_provider.ConsumeBool()) {
112                      // We can only skip the check if no unspent coin exists for this outpoint.
113                      const bool possible_overwrite{coins_view_cache.PeekCoin(outpoint) || fuzzed_data_provider.ConsumeBool()};
114                      coins_view_cache.AddCoin(outpoint, std::move(coin), possible_overwrite);
115                  } else {
116                      coins_view_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
117                  }
118              },
119              [&] {
120                  coins_view_cache.Flush(/*reallocate_cache=*/fuzzed_data_provider.ConsumeBool());
121              },
122              [&] {
123                  coins_view_cache.Sync();
124              },
125              [&] {
126                  uint256 best_block{ConsumeUInt256(fuzzed_data_provider)};
127                  // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().
128                  if (is_db && best_block.IsNull()) best_block = uint256::ONE;
129                  coins_view_cache.SetBestBlock(best_block);
130              },
131              [&] {
132                  {
133                      const auto reset_guard{coins_view_cache.CreateResetGuard()};
134                  }
135                  // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().
136                  if (is_db) {
137                      const uint256 best_block{ConsumeUInt256(fuzzed_data_provider)};
138                      if (best_block.IsNull()) {
139                          good_data = false;
140                          return;
141                      }
142                      coins_view_cache.SetBestBlock(best_block);
143                  }
144              },
145              [&] {
146                  Coin move_to;
147                  (void)coins_view_cache.SpendCoin(random_out_point, fuzzed_data_provider.ConsumeBool() ? &move_to : nullptr);
148              },
149              [&] {
150                  coins_view_cache.Uncache(random_out_point);
151              },
152              [&] {
153                  if (fuzzed_data_provider.ConsumeBool()) {
154                      backend_coins_view = CCoinsView{};
155                  }
156                  coins_view_cache.SetBackend(backend_coins_view);
157              },
158              [&] {
159                  const std::optional<COutPoint> opt_out_point = ConsumeDeserializable<COutPoint>(fuzzed_data_provider);
160                  if (!opt_out_point) {
161                      good_data = false;
162                      return;
163                  }
164                  random_out_point = *opt_out_point;
165              },
166              [&] {
167                  const std::optional<Coin> opt_coin = ConsumeDeserializable<Coin>(fuzzed_data_provider);
168                  if (!opt_coin) {
169                      good_data = false;
170                      return;
171                  }
172                  random_coin = *opt_coin;
173              },
174              [&] {
175                  const std::optional<CMutableTransaction> opt_mutable_transaction = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS);
176                  if (!opt_mutable_transaction) {
177                      good_data = false;
178                      return;
179                  }
180                  random_mutable_transaction = *opt_mutable_transaction;
181              },
182              [&] {
183                  CoinsCachePair sentinel{};
184                  sentinel.second.SelfRef(sentinel);
185                  size_t dirty_count{0};
186                  CCoinsMapMemoryResource resource;
187                  CCoinsMap coins_map{0, SaltedOutpointHasher{/*deterministic=*/true}, CCoinsMap::key_equal{}, &resource};
188                  LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000)
189                  {
190                      CCoinsCacheEntry coins_cache_entry;
191                      if (fuzzed_data_provider.ConsumeBool()) {
192                          coins_cache_entry.coin = random_coin;
193                      } else {
194                          const std::optional<Coin> opt_coin = ConsumeDeserializable<Coin>(fuzzed_data_provider);
195                          if (!opt_coin) {
196                              good_data = false;
197                              return;
198                          }
199                          coins_cache_entry.coin = *opt_coin;
200                      }
201                      // Avoid setting FRESH for an outpoint that already exists unspent in the parent view.
202                      bool fresh{!coins_view_cache.PeekCoin(random_out_point) && fuzzed_data_provider.ConsumeBool()};
203                      bool dirty{fresh || fuzzed_data_provider.ConsumeBool()};
204                      auto it{coins_map.emplace(random_out_point, std::move(coins_cache_entry)).first};
205                      if (dirty) CCoinsCacheEntry::SetDirty(*it, sentinel);
206                      if (fresh) CCoinsCacheEntry::SetFresh(*it, sentinel);
207                      dirty_count += dirty;
208                  }
209                  auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, coins_map, /*will_erase=*/true)};
210                  uint256 best_block{coins_view_cache.GetBestBlock()};
211                  if (fuzzed_data_provider.ConsumeBool()) best_block = ConsumeUInt256(fuzzed_data_provider);
212                  // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().
213                  if (is_db && best_block.IsNull()) best_block = uint256::ONE;
214                  coins_view_cache.BatchWrite(cursor, best_block);
215              });
216      }
217  
218      {
219          bool expected_code_path = false;
220          try {
221              (void)coins_view_cache.Cursor();
222          } catch (const std::logic_error&) {
223              expected_code_path = true;
224          }
225          assert(expected_code_path);
226          (void)coins_view_cache.DynamicMemoryUsage();
227          (void)coins_view_cache.EstimateSize();
228          (void)coins_view_cache.GetBestBlock();
229          (void)coins_view_cache.GetCacheSize();
230          (void)coins_view_cache.GetHeadBlocks();
231          (void)coins_view_cache.HaveInputs(CTransaction{random_mutable_transaction});
232      }
233  
234      {
235          if (is_db) {
236              std::unique_ptr<CCoinsViewCursor> coins_view_cursor = backend_coins_view.Cursor();
237              assert(!!coins_view_cursor);
238          }
239          (void)backend_coins_view.EstimateSize();
240          (void)backend_coins_view.GetBestBlock();
241          (void)backend_coins_view.GetHeadBlocks();
242      }
243  
244      if (fuzzed_data_provider.ConsumeBool()) {
245          CallOneOf(
246              fuzzed_data_provider,
247              [&] {
248                  const CTransaction transaction{random_mutable_transaction};
249                  bool is_spent = false;
250                  for (const CTxOut& tx_out : transaction.vout) {
251                      if (Coin{tx_out, 0, transaction.IsCoinBase()}.IsSpent()) {
252                          is_spent = true;
253                      }
254                  }
255                  if (is_spent) {
256                      // Avoid:
257                      // coins.cpp:69: void CCoinsViewCache::AddCoin(const COutPoint &, Coin &&, bool): Assertion `!coin.IsSpent()' failed.
258                      return;
259                  }
260                  const int height{int(fuzzed_data_provider.ConsumeIntegral<uint32_t>() >> 1)};
261                  const bool check_for_overwrite{transaction.IsCoinBase() || [&] {
262                      for (uint32_t i{0}; i < transaction.vout.size(); ++i) {
263                          if (coins_view_cache.PeekCoin(COutPoint{transaction.GetHash(), i})) return true;
264                      }
265                      return fuzzed_data_provider.ConsumeBool();
266                  }()}; // We can only skip the check if the current txid has no unspent outputs
267                  AddCoins(coins_view_cache, transaction, height, check_for_overwrite);
268              },
269              [&] {
270                  (void)AreInputsStandard(CTransaction{random_mutable_transaction}, coins_view_cache);
271              },
272              [&] {
273                  TxValidationState state;
274                  CAmount tx_fee_out;
275                  const CTransaction transaction{random_mutable_transaction};
276                  if (ContainsSpentInput(transaction, coins_view_cache)) {
277                      // Avoid:
278                      // consensus/tx_verify.cpp:171: bool Consensus::CheckTxInputs(const CTransaction &, TxValidationState &, const CCoinsViewCache &, int, CAmount &): Assertion `!coin.IsSpent()' failed.
279                      return;
280                  }
281                  TxValidationState dummy;
282                  if (!CheckTransaction(transaction, dummy)) {
283                      // It is not allowed to call CheckTxInputs if CheckTransaction failed
284                      return;
285                  }
286                  if (Consensus::CheckTxInputs(transaction, state, coins_view_cache, fuzzed_data_provider.ConsumeIntegralInRange<int>(0, std::numeric_limits<int>::max()), tx_fee_out)) {
287                      assert(MoneyRange(tx_fee_out));
288                  }
289              },
290              [&] {
291                  const CTransaction transaction{random_mutable_transaction};
292                  if (ContainsSpentInput(transaction, coins_view_cache)) {
293                      // Avoid:
294                      // consensus/tx_verify.cpp:130: unsigned int GetP2SHSigOpCount(const CTransaction &, const CCoinsViewCache &): Assertion `!coin.IsSpent()' failed.
295                      return;
296                  }
297                  (void)GetP2SHSigOpCount(transaction, coins_view_cache);
298              },
299              [&] {
300                  const CTransaction transaction{random_mutable_transaction};
301                  if (ContainsSpentInput(transaction, coins_view_cache)) {
302                      // Avoid:
303                      // consensus/tx_verify.cpp:130: unsigned int GetP2SHSigOpCount(const CTransaction &, const CCoinsViewCache &): Assertion `!coin.IsSpent()' failed.
304                      return;
305                  }
306                  const auto flags = script_verify_flags::from_int(fuzzed_data_provider.ConsumeIntegral<script_verify_flags::value_type>());
307                  if (!transaction.vin.empty() && (flags & SCRIPT_VERIFY_WITNESS) != 0 && (flags & SCRIPT_VERIFY_P2SH) == 0) {
308                      // Avoid:
309                      // script/interpreter.cpp:1705: size_t CountWitnessSigOps(const CScript &, const CScript &, const CScriptWitness &, unsigned int): Assertion `(flags & SCRIPT_VERIFY_P2SH) != 0' failed.
310                      return;
311                  }
312                  (void)GetTransactionSigOpCost(transaction, coins_view_cache, flags);
313              },
314              [&] {
315                  (void)IsWitnessStandard(CTransaction{random_mutable_transaction}, coins_view_cache);
316              });
317      }
318  
319      {
320          const Coin& coin_using_access_coin = coins_view_cache.AccessCoin(random_out_point);
321          const bool exists_using_access_coin = !(coin_using_access_coin == EMPTY_COIN);
322          const bool exists_using_have_coin = coins_view_cache.HaveCoin(random_out_point);
323          const bool exists_using_have_coin_in_cache = coins_view_cache.HaveCoinInCache(random_out_point);
324          if (auto coin{coins_view_cache.GetCoin(random_out_point)}) {
325              assert(*coin == coin_using_access_coin);
326              assert(exists_using_access_coin && exists_using_have_coin_in_cache && exists_using_have_coin);
327          } else {
328              assert(!exists_using_access_coin && !exists_using_have_coin_in_cache && !exists_using_have_coin);
329          }
330          // If HaveCoin on the backend is true, it must also be on the cache if the coin wasn't spent.
331          const bool exists_using_have_coin_in_backend = backend_coins_view.HaveCoin(random_out_point);
332          if (!coin_using_access_coin.IsSpent() && exists_using_have_coin_in_backend) {
333              assert(exists_using_have_coin);
334          }
335          if (auto coin{backend_coins_view.GetCoin(random_out_point)}) {
336              assert(exists_using_have_coin_in_backend);
337              // Note we can't assert that `coin_using_get_coin == *coin` because the coin in
338              // the cache may have been modified but not yet flushed.
339          } else {
340              assert(!exists_using_have_coin_in_backend);
341          }
342      }
343  }
344  
345  FUZZ_TARGET(coins_view, .init = initialize_coins_view)
346  {
347      FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
348      CCoinsView backend_coins_view;
349      CCoinsViewCache coins_view_cache{&backend_coins_view, /*deterministic=*/true};
350      TestCoinsView(fuzzed_data_provider, coins_view_cache, backend_coins_view, /*is_db=*/false);
351  }
352  
353  FUZZ_TARGET(coins_view_db, .init = initialize_coins_view)
354  {
355      FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
356      auto db_params = DBParams{
357          .path = "",
358          .cache_bytes = 1_MiB,
359          .memory_only = true,
360      };
361      CCoinsViewDB backend_coins_view{std::move(db_params), CoinsViewOptions{}};
362      CCoinsViewCache coins_view_cache{&backend_coins_view, /*deterministic=*/true};
363      TestCoinsView(fuzzed_data_provider, coins_view_cache, backend_coins_view, /*is_db=*/true);
364  }
365  
366  // Creates a CoinsViewOverlay and a MutationGuardCoinsViewCache as the base.
367  // This allows us to exercise all methods on a CoinsViewOverlay, while also
368  // ensuring that nothing can mutate the underlying cache until Flush or Sync is
369  // called.
370  FUZZ_TARGET(coins_view_overlay, .init = initialize_coins_view)
371  {
372      FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
373      CCoinsView backend_base_coins_view;
374      MutationGuardCoinsViewCache backend_cache{&backend_base_coins_view, /*deterministic=*/true};
375      CoinsViewOverlay coins_view_cache{&backend_cache, /*deterministic=*/true};
376      TestCoinsView(fuzzed_data_provider, coins_view_cache, backend_cache, /*is_db=*/false);
377  }