/ src / test / fuzz / txdownloadman.cpp
txdownloadman.cpp
  1  // Copyright (c) 2023-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 <consensus/validation.h>
  6  #include <node/context.h>
  7  #include <node/mempool_args.h>
  8  #include <node/miner.h>
  9  #include <node/txdownloadman.h>
 10  #include <node/txdownloadman_impl.h>
 11  #include <test/fuzz/FuzzedDataProvider.h>
 12  #include <test/fuzz/fuzz.h>
 13  #include <test/fuzz/util.h>
 14  #include <test/fuzz/util/mempool.h>
 15  #include <test/util/mining.h>
 16  #include <test/util/script.h>
 17  #include <test/util/setup_common.h>
 18  #include <test/util/time.h>
 19  #include <test/util/txmempool.h>
 20  #include <txmempool.h>
 21  #include <util/hasher.h>
 22  #include <util/rbf.h>
 23  #include <util/time.h>
 24  #include <validation.h>
 25  #include <validationinterface.h>
 26  
 27  namespace {
 28  
 29  const TestingSetup* g_setup;
 30  
 31  constexpr size_t NUM_COINS{50};
 32  COutPoint COINS[NUM_COINS];
 33  
 34  static TxValidationResult TESTED_TX_RESULTS[] = {
 35      // Skip TX_RESULT_UNSET
 36      TxValidationResult::TX_CONSENSUS,
 37      TxValidationResult::TX_INPUTS_NOT_STANDARD,
 38      TxValidationResult::TX_NOT_STANDARD,
 39      TxValidationResult::TX_MISSING_INPUTS,
 40      TxValidationResult::TX_PREMATURE_SPEND,
 41      TxValidationResult::TX_WITNESS_MUTATED,
 42      TxValidationResult::TX_WITNESS_STRIPPED,
 43      TxValidationResult::TX_CONFLICT,
 44      TxValidationResult::TX_MEMPOOL_POLICY,
 45      // Skip TX_NO_MEMPOOL
 46      TxValidationResult::TX_RECONSIDERABLE,
 47      TxValidationResult::TX_UNKNOWN,
 48  };
 49  
 50  // Precomputed transactions. Some may conflict with each other.
 51  std::vector<CTransactionRef> TRANSACTIONS;
 52  
 53  // Limit the total number of peers because we don't expect coverage to change much with lots more peers.
 54  constexpr int NUM_PEERS = 16;
 55  
 56  // Precomputed random durations (positive and negative, each ~exponentially distributed).
 57  std::chrono::microseconds TIME_SKIPS[128];
 58  
 59  static CTransactionRef MakeTransactionSpending(const std::vector<COutPoint>& outpoints, size_t num_outputs, bool add_witness)
 60  {
 61      CMutableTransaction tx;
 62      // If no outpoints are given, create a random one.
 63      for (const auto& outpoint : outpoints) {
 64          tx.vin.emplace_back(outpoint);
 65      }
 66      if (add_witness) {
 67          tx.vin[0].scriptWitness.stack.push_back({1});
 68      }
 69      for (size_t o = 0; o < num_outputs; ++o) tx.vout.emplace_back(CENT, P2WSH_OP_TRUE);
 70      return MakeTransactionRef(tx);
 71  }
 72  static std::vector<COutPoint> PickCoins(FuzzedDataProvider& fuzzed_data_provider)
 73  {
 74      std::vector<COutPoint> ret;
 75      ret.push_back(fuzzed_data_provider.PickValueInArray(COINS));
 76      LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10) {
 77          ret.push_back(fuzzed_data_provider.PickValueInArray(COINS));
 78      }
 79      return ret;
 80  }
 81  
 82  void initialize()
 83  {
 84      static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
 85      g_setup = testing_setup.get();
 86      for (uint32_t i = 0; i < uint32_t{NUM_COINS}; ++i) {
 87          COINS[i] = COutPoint{Txid::FromUint256((HashWriter() << i).GetHash()), i};
 88      }
 89      size_t outpoints_index = 0;
 90      // 2 transactions same txid different witness
 91      {
 92          auto tx1{MakeTransactionSpending({COINS[outpoints_index]}, /*num_outputs=*/5, /*add_witness=*/false)};
 93          auto tx2{MakeTransactionSpending({COINS[outpoints_index]}, /*num_outputs=*/5, /*add_witness=*/true)};
 94          Assert(tx1->GetHash() == tx2->GetHash());
 95          TRANSACTIONS.emplace_back(tx1);
 96          TRANSACTIONS.emplace_back(tx2);
 97          outpoints_index += 1;
 98      }
 99      // 2 parents 1 child
100      {
101          auto tx_parent_1{MakeTransactionSpending({COINS[outpoints_index++]}, /*num_outputs=*/1, /*add_witness=*/true)};
102          TRANSACTIONS.emplace_back(tx_parent_1);
103          auto tx_parent_2{MakeTransactionSpending({COINS[outpoints_index++]}, /*num_outputs=*/1, /*add_witness=*/false)};
104          TRANSACTIONS.emplace_back(tx_parent_2);
105          TRANSACTIONS.emplace_back(MakeTransactionSpending({COutPoint{tx_parent_1->GetHash(), 0}, COutPoint{tx_parent_2->GetHash(), 0}},
106                                                              /*num_outputs=*/1, /*add_witness=*/true));
107      }
108      // 1 parent 2 children
109      {
110          auto tx_parent{MakeTransactionSpending({COINS[outpoints_index++]}, /*num_outputs=*/2, /*add_witness=*/true)};
111          TRANSACTIONS.emplace_back(tx_parent);
112          TRANSACTIONS.emplace_back(MakeTransactionSpending({COutPoint{tx_parent->GetHash(), 0}},
113                                                              /*num_outputs=*/1, /*add_witness=*/true));
114          TRANSACTIONS.emplace_back(MakeTransactionSpending({COutPoint{tx_parent->GetHash(), 1}},
115                                                              /*num_outputs=*/1, /*add_witness=*/true));
116      }
117      // chain of 5 segwit
118      {
119          COutPoint& last_outpoint = COINS[outpoints_index++];
120          for (auto i{0}; i < 5; ++i) {
121              auto tx{MakeTransactionSpending({last_outpoint}, /*num_outputs=*/1, /*add_witness=*/true)};
122              TRANSACTIONS.emplace_back(tx);
123              last_outpoint = COutPoint{tx->GetHash(), 0};
124          }
125      }
126      // chain of 5 non-segwit
127      {
128          COutPoint& last_outpoint = COINS[outpoints_index++];
129          for (auto i{0}; i < 5; ++i) {
130              auto tx{MakeTransactionSpending({last_outpoint}, /*num_outputs=*/1, /*add_witness=*/false)};
131              TRANSACTIONS.emplace_back(tx);
132              last_outpoint = COutPoint{tx->GetHash(), 0};
133          }
134      }
135      // Also create a loose tx for each outpoint. Some of these transactions conflict with the above
136      // or have the same txid.
137      for (const auto& outpoint : COINS) {
138          TRANSACTIONS.emplace_back(MakeTransactionSpending({outpoint}, /*num_outputs=*/1, /*add_witness=*/true));
139      }
140  
141      // Create random-looking time jumps
142      int i = 0;
143      // TIME_SKIPS[N] for N=0..15 is just N microseconds.
144      for (; i < 16; ++i) {
145          TIME_SKIPS[i] = std::chrono::microseconds{i};
146      }
147      // TIME_SKIPS[N] for N=16..127 has randomly-looking but roughly exponentially increasing values up to
148      // 198.416453 seconds.
149      for (; i < 128; ++i) {
150          int diff_bits = ((i - 10) * 2) / 9;
151          uint64_t diff = 1 + (CSipHasher(0, 0).Write(i).Finalize() >> (64 - diff_bits));
152          TIME_SKIPS[i] = TIME_SKIPS[i - 1] + std::chrono::microseconds{diff};
153      }
154  }
155  
156  void CheckPackageToValidate(const node::PackageToValidate& package_to_validate, NodeId peer)
157  {
158      Assert(package_to_validate.m_senders.size() == 2);
159      Assert(package_to_validate.m_senders.front() == peer);
160      Assert(package_to_validate.m_senders.back() < NUM_PEERS);
161  
162      // Package is a 1p1c
163      const auto& package = package_to_validate.m_txns;
164      Assert(IsChildWithParents(package));
165      Assert(package.size() == 2);
166  }
167  
168  FUZZ_TARGET(txdownloadman, .init = initialize)
169  {
170      SeedRandomStateForTest(SeedRand::ZEROS);
171      FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
172      NodeClockContext clock_ctx{ConsumeTime(fuzzed_data_provider)};
173  
174      // Initialize txdownloadman
175      bilingual_str error;
176      CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node), error};
177      FastRandomContext det_rand{true};
178      node::TxDownloadManager txdownloadman{node::TxDownloadOptions{pool, det_rand, true}};
179  
180      std::chrono::microseconds time{244466666};
181  
182      LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 500)
183      {
184          NodeId rand_peer = fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(0, NUM_PEERS - 1);
185  
186          // Transaction can be one of the premade ones or a randomly generated one
187          auto rand_tx = fuzzed_data_provider.ConsumeBool() ?
188              MakeTransactionSpending(PickCoins(fuzzed_data_provider),
189                                      /*num_outputs=*/fuzzed_data_provider.ConsumeIntegralInRange(1, 500),
190                                      /*add_witness=*/fuzzed_data_provider.ConsumeBool()) :
191              TRANSACTIONS.at(fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, TRANSACTIONS.size() - 1));
192  
193          CallOneOf(
194              fuzzed_data_provider,
195              [&] {
196                  node::TxDownloadConnectionInfo info{
197                      .m_preferred = fuzzed_data_provider.ConsumeBool(),
198                      .m_relay_permissions = fuzzed_data_provider.ConsumeBool(),
199                      .m_wtxid_relay = fuzzed_data_provider.ConsumeBool()
200                  };
201                  txdownloadman.ConnectedPeer(rand_peer, info);
202              },
203              [&] {
204                  txdownloadman.DisconnectedPeer(rand_peer);
205                  txdownloadman.CheckIsEmpty(rand_peer);
206              },
207              [&] {
208                  txdownloadman.ActiveTipChange();
209              },
210              [&] {
211                  CBlock block;
212                  block.vtx.push_back(rand_tx);
213                  txdownloadman.BlockConnected(std::make_shared<CBlock>(block));
214              },
215              [&] {
216                  txdownloadman.BlockDisconnected();
217              },
218              [&] {
219                  txdownloadman.MempoolAcceptedTx(rand_tx);
220              },
221              [&] {
222                  TxValidationState state;
223                  state.Invalid(fuzzed_data_provider.PickValueInArray(TESTED_TX_RESULTS), "");
224                  bool first_time_failure{fuzzed_data_provider.ConsumeBool()};
225  
226                  node::RejectedTxTodo todo = txdownloadman.MempoolRejectedTx(rand_tx, state, rand_peer, first_time_failure);
227                  Assert(first_time_failure || !todo.m_should_add_extra_compact_tx);
228              },
229              [&] {
230                  auto gtxid = fuzzed_data_provider.ConsumeBool() ?
231                               GenTxid{rand_tx->GetHash()} :
232                               GenTxid{rand_tx->GetWitnessHash()};
233                  txdownloadman.AddTxAnnouncement(rand_peer, gtxid, time);
234              },
235              [&] {
236                  txdownloadman.GetRequestsToSend(rand_peer, time);
237              },
238              [&] {
239                  txdownloadman.ReceivedTx(rand_peer, rand_tx);
240                  const auto& [should_validate, maybe_package] = txdownloadman.ReceivedTx(rand_peer, rand_tx);
241                  // The only possible results should be:
242                  // - Don't validate the tx, no package.
243                  // - Don't validate the tx, package.
244                  // - Validate the tx, no package.
245                  // The only combination that doesn't make sense is validate both tx and package.
246                  Assert(!(should_validate && maybe_package.has_value()));
247                  if (maybe_package.has_value()) CheckPackageToValidate(*maybe_package, rand_peer);
248              },
249              [&] {
250                  txdownloadman.ReceivedNotFound(rand_peer, {rand_tx->GetWitnessHash()});
251              },
252              [&] {
253                  const bool expect_work{txdownloadman.HaveMoreWork(rand_peer)};
254                  const auto ptx = txdownloadman.GetTxToReconsider(rand_peer);
255                  // expect_work=true doesn't necessarily mean the next item from the workset isn't a
256                  // nullptr, as the transaction could have been removed from orphanage without being
257                  // removed from the peer's workset.
258                  if (ptx) {
259                      // However, if there was a non-null tx in the workset, HaveMoreWork should have
260                      // returned true.
261                      Assert(expect_work);
262                  }
263              });
264          // Jump forwards or backwards
265          auto time_skip = fuzzed_data_provider.PickValueInArray(TIME_SKIPS);
266          if (fuzzed_data_provider.ConsumeBool()) time_skip *= -1;
267          time += time_skip;
268      }
269      // Disconnect everybody, check that all data structures are empty.
270      for (NodeId nodeid = 0; nodeid < NUM_PEERS; ++nodeid) {
271          txdownloadman.DisconnectedPeer(nodeid);
272          txdownloadman.CheckIsEmpty(nodeid);
273      }
274      txdownloadman.CheckIsEmpty();
275  }
276  
277  // Give node 0 relay permissions, and nobody else. This helps us remember who is a RelayPermissions
278  // peer without tracking anything (this is only for the txdownload_impl target).
279  static bool HasRelayPermissions(NodeId peer) { return peer == 0; }
280  
281  static void CheckInvariants(const node::TxDownloadManagerImpl& txdownload_impl)
282  {
283      txdownload_impl.m_orphanage->SanityCheck();
284      // We should never have more than the maximum in-flight requests out for a peer.
285      for (NodeId peer = 0; peer < NUM_PEERS; ++peer) {
286          if (!HasRelayPermissions(peer)) {
287              Assert(txdownload_impl.m_txrequest.Count(peer) <= node::MAX_PEER_TX_ANNOUNCEMENTS);
288          }
289      }
290      txdownload_impl.m_txrequest.SanityCheck();
291  }
292  
293  FUZZ_TARGET(txdownloadman_impl, .init = initialize)
294  {
295      SeedRandomStateForTest(SeedRand::ZEROS);
296      FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
297      NodeClockContext clock_ctx{ConsumeTime(fuzzed_data_provider)};
298  
299      // Initialize a TxDownloadManagerImpl
300      bilingual_str error;
301      CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node), error};
302      FastRandomContext det_rand{true};
303      node::TxDownloadManagerImpl txdownload_impl{node::TxDownloadOptions{pool, det_rand, true}};
304  
305      std::chrono::microseconds time{244466666};
306  
307      LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 500)
308      {
309          NodeId rand_peer = fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(0, NUM_PEERS - 1);
310  
311          // Transaction can be one of the premade ones or a randomly generated one
312          auto rand_tx = fuzzed_data_provider.ConsumeBool() ?
313              MakeTransactionSpending(PickCoins(fuzzed_data_provider),
314                                      /*num_outputs=*/fuzzed_data_provider.ConsumeIntegralInRange(1, 500),
315                                      /*add_witness=*/fuzzed_data_provider.ConsumeBool()) :
316              TRANSACTIONS.at(fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, TRANSACTIONS.size() - 1));
317  
318          CallOneOf(
319              fuzzed_data_provider,
320              [&] {
321                  node::TxDownloadConnectionInfo info{
322                      .m_preferred = fuzzed_data_provider.ConsumeBool(),
323                      .m_relay_permissions = HasRelayPermissions(rand_peer),
324                      .m_wtxid_relay = fuzzed_data_provider.ConsumeBool()
325                  };
326                  txdownload_impl.ConnectedPeer(rand_peer, info);
327              },
328              [&] {
329                  txdownload_impl.DisconnectedPeer(rand_peer);
330                  txdownload_impl.CheckIsEmpty(rand_peer);
331              },
332              [&] {
333                  txdownload_impl.ActiveTipChange();
334                  // After a block update, nothing should be in the rejection caches
335                  for (const auto& tx : TRANSACTIONS) {
336                      Assert(!txdownload_impl.RecentRejectsFilter().contains(tx->GetWitnessHash().ToUint256()));
337                      Assert(!txdownload_impl.RecentRejectsFilter().contains(tx->GetHash().ToUint256()));
338                      Assert(!txdownload_impl.RecentRejectsReconsiderableFilter().contains(tx->GetWitnessHash().ToUint256()));
339                      Assert(!txdownload_impl.RecentRejectsReconsiderableFilter().contains(tx->GetHash().ToUint256()));
340                  }
341              },
342              [&] {
343                  CBlock block;
344                  block.vtx.push_back(rand_tx);
345                  txdownload_impl.BlockConnected(std::make_shared<CBlock>(block));
346                  // Block transactions must be removed from orphanage
347                  Assert(!txdownload_impl.m_orphanage->HaveTx(rand_tx->GetWitnessHash()));
348              },
349              [&] {
350                  txdownload_impl.BlockDisconnected();
351                  Assert(!txdownload_impl.RecentConfirmedTransactionsFilter().contains(rand_tx->GetWitnessHash().ToUint256()));
352                  Assert(!txdownload_impl.RecentConfirmedTransactionsFilter().contains(rand_tx->GetHash().ToUint256()));
353              },
354              [&] {
355                  txdownload_impl.MempoolAcceptedTx(rand_tx);
356              },
357              [&] {
358                  TxValidationState state;
359                  state.Invalid(fuzzed_data_provider.PickValueInArray(TESTED_TX_RESULTS), "");
360                  bool first_time_failure{fuzzed_data_provider.ConsumeBool()};
361  
362                  bool reject_contains_wtxid{txdownload_impl.RecentRejectsFilter().contains(rand_tx->GetWitnessHash().ToUint256())};
363  
364                  node::RejectedTxTodo todo = txdownload_impl.MempoolRejectedTx(rand_tx, state, rand_peer, first_time_failure);
365                  Assert(first_time_failure || !todo.m_should_add_extra_compact_tx);
366                  if (!reject_contains_wtxid) Assert(todo.m_unique_parents.size() <= rand_tx->vin.size());
367              },
368              [&] {
369                  auto gtxid = fuzzed_data_provider.ConsumeBool() ?
370                               GenTxid{rand_tx->GetHash()} :
371                               GenTxid{rand_tx->GetWitnessHash()};
372                  txdownload_impl.AddTxAnnouncement(rand_peer, gtxid, time);
373              },
374              [&] {
375                  const auto getdata_requests = txdownload_impl.GetRequestsToSend(rand_peer, time);
376                  // TxDownloadManager should not be telling us to request things we already have.
377                  // Exclude m_lazy_recent_rejects_reconsiderable because it may request low-feerate parent of orphan.
378                  for (const auto& gtxid : getdata_requests) {
379                      Assert(!txdownload_impl.AlreadyHaveTx(gtxid, /*include_reconsiderable=*/false));
380                  }
381              },
382              [&] {
383                  const auto& [should_validate, maybe_package] = txdownload_impl.ReceivedTx(rand_peer, rand_tx);
384                  // The only possible results should be:
385                  // - Don't validate the tx, no package.
386                  // - Don't validate the tx, package.
387                  // - Validate the tx, no package.
388                  // The only combination that doesn't make sense is validate both tx and package.
389                  Assert(!(should_validate && maybe_package.has_value()));
390                  if (should_validate) {
391                      Assert(!txdownload_impl.AlreadyHaveTx(rand_tx->GetWitnessHash(), /*include_reconsiderable=*/true));
392                  }
393                  if (maybe_package.has_value()) {
394                      CheckPackageToValidate(*maybe_package, rand_peer);
395  
396                      const auto& package = maybe_package->m_txns;
397                      // Parent is in m_lazy_recent_rejects_reconsiderable and child is in m_orphanage
398                      Assert(txdownload_impl.RecentRejectsReconsiderableFilter().contains(rand_tx->GetWitnessHash().ToUint256()));
399                      Assert(txdownload_impl.m_orphanage->HaveTx(maybe_package->m_txns.back()->GetWitnessHash()));
400                      // Package has not been rejected
401                      Assert(!txdownload_impl.RecentRejectsReconsiderableFilter().contains(GetPackageHash(package)));
402                      // Neither is in m_lazy_recent_rejects
403                      Assert(!txdownload_impl.RecentRejectsFilter().contains(package.front()->GetWitnessHash().ToUint256()));
404                      Assert(!txdownload_impl.RecentRejectsFilter().contains(package.back()->GetWitnessHash().ToUint256()));
405                  }
406              },
407              [&] {
408                  txdownload_impl.ReceivedNotFound(rand_peer, {rand_tx->GetWitnessHash()});
409              },
410              [&] {
411                  const bool expect_work{txdownload_impl.HaveMoreWork(rand_peer)};
412                  const auto ptx{txdownload_impl.GetTxToReconsider(rand_peer)};
413                  // expect_work=true doesn't necessarily mean the next item from the workset isn't a
414                  // nullptr, as the transaction could have been removed from orphanage without being
415                  // removed from the peer's workset.
416                  if (ptx) {
417                      // However, if there was a non-null tx in the workset, HaveMoreWork should have
418                      // returned true.
419                      Assert(expect_work);
420                      Assert(txdownload_impl.AlreadyHaveTx(ptx->GetWitnessHash(), /*include_reconsiderable=*/false));
421                      // Presumably we have validated this tx. Use "missing inputs" to keep it in the
422                      // orphanage longer. Later iterations might call MempoolAcceptedTx or
423                      // MempoolRejectedTx with a different error.
424                      TxValidationState state_missing_inputs;
425                      state_missing_inputs.Invalid(TxValidationResult::TX_MISSING_INPUTS, "");
426                      txdownload_impl.MempoolRejectedTx(ptx, state_missing_inputs, rand_peer, fuzzed_data_provider.ConsumeBool());
427                  }
428              });
429  
430          auto time_skip = fuzzed_data_provider.PickValueInArray(TIME_SKIPS);
431          if (fuzzed_data_provider.ConsumeBool()) time_skip *= -1;
432          time += time_skip;
433      }
434      CheckInvariants(txdownload_impl);
435      // Disconnect everybody, check that all data structures are empty.
436      for (NodeId nodeid = 0; nodeid < NUM_PEERS; ++nodeid) {
437          txdownload_impl.DisconnectedPeer(nodeid);
438          txdownload_impl.CheckIsEmpty(nodeid);
439      }
440      txdownload_impl.CheckIsEmpty();
441  }
442  
443  } // namespace