/ src / test / fuzz / txrequest.cpp
txrequest.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 <crypto/common.h>
  6  #include <crypto/sha256.h>
  7  #include <crypto/siphash.h>
  8  #include <primitives/transaction.h>
  9  #include <test/fuzz/fuzz.h>
 10  #include <txrequest.h>
 11  
 12  #include <bitset>
 13  #include <cstdint>
 14  #include <queue>
 15  #include <vector>
 16  
 17  namespace {
 18  
 19  constexpr int MAX_TXHASHES = 16;
 20  constexpr int MAX_PEERS = 16;
 21  
 22  //! Randomly generated txhashes used in this test (length is MAX_TXHASHES).
 23  uint256 TXHASHES[MAX_TXHASHES];
 24  
 25  //! Precomputed random durations (positive and negative, each ~exponentially distributed).
 26  std::chrono::microseconds DELAYS[256];
 27  
 28  struct Initializer
 29  {
 30      Initializer()
 31      {
 32          for (uint8_t txhash = 0; txhash < MAX_TXHASHES; txhash += 1) {
 33              CSHA256().Write(&txhash, 1).Finalize(TXHASHES[txhash].begin());
 34          }
 35          int i = 0;
 36          // DELAYS[N] for N=0..15 is just N microseconds.
 37          for (; i < 16; ++i) {
 38              DELAYS[i] = std::chrono::microseconds{i};
 39          }
 40          // DELAYS[N] for N=16..127 has randomly-looking but roughly exponentially increasing values up to
 41          // 198.416453 seconds.
 42          for (; i < 128; ++i) {
 43              int diff_bits = ((i - 10) * 2) / 9;
 44              uint64_t diff = 1 + (CSipHasher(0, 0).Write(i).Finalize() >> (64 - diff_bits));
 45              DELAYS[i] = DELAYS[i - 1] + std::chrono::microseconds{diff};
 46          }
 47          // DELAYS[N] for N=128..255 are negative delays with the same magnitude as N=0..127.
 48          for (; i < 256; ++i) {
 49              DELAYS[i] = -DELAYS[255 - i];
 50          }
 51      }
 52  } g_initializer;
 53  
 54  /** Tester class for TxRequestTracker
 55   *
 56   * It includes a naive reimplementation of its behavior, for a limited set
 57   * of MAX_TXHASHES distinct txids, and MAX_PEERS peer identifiers.
 58   *
 59   * All of the public member functions perform the same operation on
 60   * an actual TxRequestTracker and on the state of the reimplementation.
 61   * The output of GetRequestable is compared with the expected value
 62   * as well.
 63   *
 64   * Check() calls the TxRequestTracker's sanity check, plus compares the
 65   * output of the constant accessors (Size(), CountLoad(), CountTracked())
 66   * with expected values.
 67   */
 68  class Tester
 69  {
 70      //! TxRequestTracker object being tested.
 71      TxRequestTracker m_tracker;
 72  
 73      //! States for txid/peer combinations in the naive data structure.
 74      enum class State {
 75          NOTHING, //!< Absence of this txid/peer combination
 76  
 77          // Note that this implementation does not distinguish between DELAYED/READY/BEST variants of CANDIDATE.
 78          CANDIDATE,
 79          REQUESTED,
 80          COMPLETED,
 81      };
 82  
 83      //! Sequence numbers, incremented whenever a new CANDIDATE is added.
 84      uint64_t m_current_sequence{0};
 85  
 86      //! List of future 'events' (all inserted reqtimes/exptimes). This is used to implement AdvanceToEvent.
 87      std::priority_queue<std::chrono::microseconds, std::vector<std::chrono::microseconds>,
 88          std::greater<std::chrono::microseconds>> m_events;
 89  
 90      //! Information about a txhash/peer combination.
 91      struct Announcement
 92      {
 93          std::chrono::microseconds m_time;
 94          uint64_t m_sequence;
 95          State m_state{State::NOTHING};
 96          bool m_preferred;
 97          bool m_is_wtxid;
 98          uint64_t m_priority; //!< Precomputed priority.
 99      };
100  
101      //! Information about all txhash/peer combination.
102      Announcement m_announcements[MAX_TXHASHES][MAX_PEERS];
103  
104      //! The current time; can move forward and backward.
105      std::chrono::microseconds m_now{244466666};
106  
107      //! Delete txhashes whose only announcements are COMPLETED.
108      void Cleanup(int txhash)
109      {
110          bool all_nothing = true;
111          for (int peer = 0; peer < MAX_PEERS; ++peer) {
112              const Announcement& ann = m_announcements[txhash][peer];
113              if (ann.m_state != State::NOTHING) {
114                  if (ann.m_state != State::COMPLETED) return;
115                  all_nothing = false;
116              }
117          }
118          if (all_nothing) return;
119          for (int peer = 0; peer < MAX_PEERS; ++peer) {
120              m_announcements[txhash][peer].m_state = State::NOTHING;
121          }
122      }
123  
124      //! Find the current best peer to request from for a txhash (or -1 if none).
125      int GetSelected(int txhash) const
126      {
127          int ret = -1;
128          uint64_t ret_priority = 0;
129          for (int peer = 0; peer < MAX_PEERS; ++peer) {
130              const Announcement& ann = m_announcements[txhash][peer];
131              // Return -1 if there already is a (non-expired) in-flight request.
132              if (ann.m_state == State::REQUESTED) return -1;
133              // If it's a viable candidate, see if it has lower priority than the best one so far.
134              if (ann.m_state == State::CANDIDATE && ann.m_time <= m_now) {
135                  if (ret == -1 || ann.m_priority > ret_priority) {
136                      std::tie(ret, ret_priority) = std::tie(peer, ann.m_priority);
137                  }
138              }
139          }
140          return ret;
141      }
142  
143  public:
144      Tester() : m_tracker(true) {}
145  
146      std::chrono::microseconds Now() const { return m_now; }
147  
148      void AdvanceTime(std::chrono::microseconds offset)
149      {
150          m_now += offset;
151          while (!m_events.empty() && m_events.top() <= m_now) m_events.pop();
152      }
153  
154      void AdvanceToEvent()
155      {
156          while (!m_events.empty() && m_events.top() <= m_now) m_events.pop();
157          if (!m_events.empty()) {
158              m_now = m_events.top();
159              m_events.pop();
160          }
161      }
162  
163      void DisconnectedPeer(int peer)
164      {
165          // Apply to naive structure: all announcements for that peer are wiped.
166          for (int txhash = 0; txhash < MAX_TXHASHES; ++txhash) {
167              if (m_announcements[txhash][peer].m_state != State::NOTHING) {
168                  m_announcements[txhash][peer].m_state = State::NOTHING;
169                  Cleanup(txhash);
170              }
171          }
172  
173          // Call TxRequestTracker's implementation.
174          m_tracker.DisconnectedPeer(peer);
175      }
176  
177      void ForgetTxHash(int txhash)
178      {
179          // Apply to naive structure: all announcements for that txhash are wiped.
180          for (int peer = 0; peer < MAX_PEERS; ++peer) {
181              m_announcements[txhash][peer].m_state = State::NOTHING;
182          }
183          Cleanup(txhash);
184  
185          // Call TxRequestTracker's implementation.
186          m_tracker.ForgetTxHash(TXHASHES[txhash]);
187      }
188  
189      void ReceivedInv(int peer, int txhash, bool is_wtxid, bool preferred, std::chrono::microseconds reqtime)
190      {
191          // Apply to naive structure: if no announcement for txidnum/peer combination
192          // already, create a new CANDIDATE; otherwise do nothing.
193          Announcement& ann = m_announcements[txhash][peer];
194          if (ann.m_state == State::NOTHING) {
195              ann.m_preferred = preferred;
196              ann.m_state = State::CANDIDATE;
197              ann.m_time = reqtime;
198              ann.m_is_wtxid = is_wtxid;
199              ann.m_sequence = m_current_sequence++;
200              ann.m_priority = m_tracker.ComputePriority(TXHASHES[txhash], peer, ann.m_preferred);
201  
202              // Add event so that AdvanceToEvent can quickly jump to the point where its reqtime passes.
203              if (reqtime > m_now) m_events.push(reqtime);
204          }
205  
206          // Call TxRequestTracker's implementation.
207          auto gtxid = is_wtxid ? GenTxid{Wtxid::FromUint256(TXHASHES[txhash])} : GenTxid{Txid::FromUint256(TXHASHES[txhash])};
208          m_tracker.ReceivedInv(peer, gtxid, preferred, reqtime);
209      }
210  
211      void RequestedTx(int peer, int txhash, std::chrono::microseconds exptime)
212      {
213          // Apply to naive structure: if a CANDIDATE announcement exists for peer/txhash,
214          // convert it to REQUESTED, and change any existing REQUESTED announcement for the same txhash to COMPLETED.
215          if (m_announcements[txhash][peer].m_state == State::CANDIDATE) {
216              for (int peer2 = 0; peer2 < MAX_PEERS; ++peer2) {
217                  if (m_announcements[txhash][peer2].m_state == State::REQUESTED) {
218                      m_announcements[txhash][peer2].m_state = State::COMPLETED;
219                  }
220              }
221              m_announcements[txhash][peer].m_state = State::REQUESTED;
222              m_announcements[txhash][peer].m_time = exptime;
223          }
224  
225          // Add event so that AdvanceToEvent can quickly jump to the point where its exptime passes.
226          if (exptime > m_now) m_events.push(exptime);
227  
228          // Call TxRequestTracker's implementation.
229          m_tracker.RequestedTx(peer, TXHASHES[txhash], exptime);
230      }
231  
232      void ReceivedResponse(int peer, int txhash)
233      {
234          // Apply to naive structure: convert anything to COMPLETED.
235          if (m_announcements[txhash][peer].m_state != State::NOTHING) {
236              m_announcements[txhash][peer].m_state = State::COMPLETED;
237              Cleanup(txhash);
238          }
239  
240          // Call TxRequestTracker's implementation.
241          m_tracker.ReceivedResponse(peer, TXHASHES[txhash]);
242      }
243  
244      void GetRequestable(int peer)
245      {
246          // Implement using naive structure:
247  
248          //! list of (sequence number, txhash, is_wtxid) tuples.
249          std::vector<std::tuple<uint64_t, int, bool>> result;
250          std::vector<std::pair<NodeId, GenTxid>> expected_expired;
251          for (int txhash = 0; txhash < MAX_TXHASHES; ++txhash) {
252              // Mark any expired REQUESTED announcements as COMPLETED.
253              for (int peer2 = 0; peer2 < MAX_PEERS; ++peer2) {
254                  Announcement& ann2 = m_announcements[txhash][peer2];
255                  if (ann2.m_state == State::REQUESTED && ann2.m_time <= m_now) {
256                      auto gtxid = ann2.m_is_wtxid ? GenTxid{Wtxid::FromUint256(TXHASHES[txhash])} : GenTxid{Txid::FromUint256(TXHASHES[txhash])};
257                      expected_expired.emplace_back(peer2, gtxid);
258                      ann2.m_state = State::COMPLETED;
259                      break;
260                  }
261              }
262              // And delete txids with only COMPLETED announcements left.
263              Cleanup(txhash);
264              // CANDIDATEs for which this announcement has the highest priority get returned.
265              const Announcement& ann = m_announcements[txhash][peer];
266              if (ann.m_state == State::CANDIDATE && GetSelected(txhash) == peer) {
267                  result.emplace_back(ann.m_sequence, txhash, ann.m_is_wtxid);
268              }
269          }
270          // Sort the results by sequence number.
271          std::sort(result.begin(), result.end());
272          std::sort(expected_expired.begin(), expected_expired.end());
273  
274          // Compare with TxRequestTracker's implementation.
275          std::vector<std::pair<NodeId, GenTxid>> expired;
276          const auto actual = m_tracker.GetRequestable(peer, m_now, &expired);
277          std::sort(expired.begin(), expired.end());
278          assert(expired == expected_expired);
279  
280          m_tracker.PostGetRequestableSanityCheck(m_now);
281          assert(result.size() == actual.size());
282          for (size_t pos = 0; pos < actual.size(); ++pos) {
283              assert(TXHASHES[std::get<1>(result[pos])] == actual[pos].ToUint256());
284              assert(std::get<2>(result[pos]) == actual[pos].IsWtxid());
285          }
286      }
287  
288      void Check()
289      {
290          // Compare CountTracked and CountLoad with naive structure.
291          size_t total = 0;
292          for (int peer = 0; peer < MAX_PEERS; ++peer) {
293              size_t tracked = 0;
294              size_t inflight = 0;
295              size_t candidates = 0;
296              for (int txhash = 0; txhash < MAX_TXHASHES; ++txhash) {
297                  tracked += m_announcements[txhash][peer].m_state != State::NOTHING;
298                  inflight += m_announcements[txhash][peer].m_state == State::REQUESTED;
299                  candidates += m_announcements[txhash][peer].m_state == State::CANDIDATE;
300  
301                  std::bitset<MAX_PEERS> expected_announcers;
302                  for (int peer = 0; peer < MAX_PEERS; ++peer) {
303                      if (m_announcements[txhash][peer].m_state == State::CANDIDATE || m_announcements[txhash][peer].m_state == State::REQUESTED) {
304                          expected_announcers[peer] = true;
305                      }
306                  }
307                  std::vector<NodeId> candidate_peers;
308                  m_tracker.GetCandidatePeers(TXHASHES[txhash], candidate_peers);
309                  assert(expected_announcers.count() == candidate_peers.size());
310                  for (const auto& peer : candidate_peers) {
311                      assert(expected_announcers[peer]);
312                  }
313              }
314              assert(m_tracker.Count(peer) == tracked);
315              assert(m_tracker.CountInFlight(peer) == inflight);
316              assert(m_tracker.CountCandidates(peer) == candidates);
317              total += tracked;
318          }
319          // Compare Size.
320          assert(m_tracker.Size() == total);
321  
322          // Invoke internal consistency check of TxRequestTracker object.
323          m_tracker.SanityCheck();
324      }
325  };
326  } // namespace
327  
328  FUZZ_TARGET(txrequest)
329  {
330      // Tester object (which encapsulates a TxRequestTracker).
331      Tester tester;
332  
333      // Decode the input as a sequence of instructions with parameters
334      auto it = buffer.begin();
335      while (it != buffer.end()) {
336          int cmd = *(it++) % 11;
337          int peer, txidnum, delaynum;
338          switch (cmd) {
339          case 0: // Make time jump to the next event (m_time of CANDIDATE or REQUESTED)
340              tester.AdvanceToEvent();
341              break;
342          case 1: // Change time
343              delaynum = it == buffer.end() ? 0 : *(it++);
344              tester.AdvanceTime(DELAYS[delaynum]);
345              break;
346          case 2: // Query for requestable txs
347              peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS;
348              tester.GetRequestable(peer);
349              break;
350          case 3: // Peer went offline
351              peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS;
352              tester.DisconnectedPeer(peer);
353              break;
354          case 4: // No longer need tx
355              txidnum = it == buffer.end() ? 0 : *(it++);
356              tester.ForgetTxHash(txidnum % MAX_TXHASHES);
357              break;
358          case 5: // Received immediate preferred inv
359          case 6: // Same, but non-preferred.
360              peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS;
361              txidnum = it == buffer.end() ? 0 : *(it++);
362              tester.ReceivedInv(peer, txidnum % MAX_TXHASHES, (txidnum / MAX_TXHASHES) & 1, cmd & 1,
363                  std::chrono::microseconds::min());
364              break;
365          case 7: // Received delayed preferred inv
366          case 8: // Same, but non-preferred.
367              peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS;
368              txidnum = it == buffer.end() ? 0 : *(it++);
369              delaynum = it == buffer.end() ? 0 : *(it++);
370              tester.ReceivedInv(peer, txidnum % MAX_TXHASHES, (txidnum / MAX_TXHASHES) & 1, cmd & 1,
371                  tester.Now() + DELAYS[delaynum]);
372              break;
373          case 9: // Requested tx from peer
374              peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS;
375              txidnum = it == buffer.end() ? 0 : *(it++);
376              delaynum = it == buffer.end() ? 0 : *(it++);
377              tester.RequestedTx(peer, txidnum % MAX_TXHASHES, tester.Now() + DELAYS[delaynum]);
378              break;
379          case 10: // Received response
380              peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS;
381              txidnum = it == buffer.end() ? 0 : *(it++);
382              tester.ReceivedResponse(peer, txidnum % MAX_TXHASHES);
383              break;
384          default:
385              assert(false);
386          }
387      }
388      tester.Check();
389  }