|
| 1 | +// Copyright (c) 2020 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 GenTxids 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 | + m_tracker.ReceivedInv(peer, GenTxid{is_wtxid, TXHASHES[txhash]}, preferred, reqtime); |
| 208 | + } |
| 209 | + |
| 210 | + void RequestedTx(int peer, int txhash, std::chrono::microseconds exptime) |
| 211 | + { |
| 212 | + // Apply to naive structure: if a CANDIDATE announcement exists for peer/txhash, |
| 213 | + // convert it to REQUESTED, and change any existing REQUESTED announcement for the same txhash to COMPLETED. |
| 214 | + if (m_announcements[txhash][peer].m_state == State::CANDIDATE) { |
| 215 | + for (int peer2 = 0; peer2 < MAX_PEERS; ++peer2) { |
| 216 | + if (m_announcements[txhash][peer2].m_state == State::REQUESTED) { |
| 217 | + m_announcements[txhash][peer2].m_state = State::COMPLETED; |
| 218 | + } |
| 219 | + } |
| 220 | + m_announcements[txhash][peer].m_state = State::REQUESTED; |
| 221 | + m_announcements[txhash][peer].m_time = exptime; |
| 222 | + } |
| 223 | + |
| 224 | + // Add event so that AdvanceToEvent can quickly jump to the point where its exptime passes. |
| 225 | + if (exptime > m_now) m_events.push(exptime); |
| 226 | + |
| 227 | + // Call TxRequestTracker's implementation. |
| 228 | + m_tracker.RequestedTx(peer, TXHASHES[txhash], exptime); |
| 229 | + } |
| 230 | + |
| 231 | + void ReceivedResponse(int peer, int txhash) |
| 232 | + { |
| 233 | + // Apply to naive structure: convert anything to COMPLETED. |
| 234 | + if (m_announcements[txhash][peer].m_state != State::NOTHING) { |
| 235 | + m_announcements[txhash][peer].m_state = State::COMPLETED; |
| 236 | + Cleanup(txhash); |
| 237 | + } |
| 238 | + |
| 239 | + // Call TxRequestTracker's implementation. |
| 240 | + m_tracker.ReceivedResponse(peer, TXHASHES[txhash]); |
| 241 | + } |
| 242 | + |
| 243 | + void GetRequestable(int peer) |
| 244 | + { |
| 245 | + // Implement using naive structure: |
| 246 | + |
| 247 | + //! list of (sequence number, txhash, is_wtxid) tuples. |
| 248 | + std::vector<std::tuple<uint64_t, int, bool>> result; |
| 249 | + for (int txhash = 0; txhash < MAX_TXHASHES; ++txhash) { |
| 250 | + // Mark any expired REQUESTED announcements as COMPLETED. |
| 251 | + for (int peer2 = 0; peer2 < MAX_PEERS; ++peer2) { |
| 252 | + Announcement& ann2 = m_announcements[txhash][peer2]; |
| 253 | + if (ann2.m_state == State::REQUESTED && ann2.m_time <= m_now) { |
| 254 | + ann2.m_state = State::COMPLETED; |
| 255 | + break; |
| 256 | + } |
| 257 | + } |
| 258 | + // And delete txids with only COMPLETED announcements left. |
| 259 | + Cleanup(txhash); |
| 260 | + // CANDIDATEs for which this announcement has the highest priority get returned. |
| 261 | + const Announcement& ann = m_announcements[txhash][peer]; |
| 262 | + if (ann.m_state == State::CANDIDATE && GetSelected(txhash) == peer) { |
| 263 | + result.emplace_back(ann.m_sequence, txhash, ann.m_is_wtxid); |
| 264 | + } |
| 265 | + } |
| 266 | + // Sort the results by sequence number. |
| 267 | + std::sort(result.begin(), result.end()); |
| 268 | + |
| 269 | + // Compare with TxRequestTracker's implementation. |
| 270 | + const auto actual = m_tracker.GetRequestable(peer, m_now); |
| 271 | + |
| 272 | + m_tracker.PostGetRequestableSanityCheck(m_now); |
| 273 | + assert(result.size() == actual.size()); |
| 274 | + for (size_t pos = 0; pos < actual.size(); ++pos) { |
| 275 | + assert(TXHASHES[std::get<1>(result[pos])] == actual[pos].GetHash()); |
| 276 | + assert(std::get<2>(result[pos]) == actual[pos].IsWtxid()); |
| 277 | + } |
| 278 | + } |
| 279 | + |
| 280 | + void Check() |
| 281 | + { |
| 282 | + // Compare CountTracked and CountLoad with naive structure. |
| 283 | + size_t total = 0; |
| 284 | + for (int peer = 0; peer < MAX_PEERS; ++peer) { |
| 285 | + size_t tracked = 0; |
| 286 | + size_t inflight = 0; |
| 287 | + size_t candidates = 0; |
| 288 | + for (int txhash = 0; txhash < MAX_TXHASHES; ++txhash) { |
| 289 | + tracked += m_announcements[txhash][peer].m_state != State::NOTHING; |
| 290 | + inflight += m_announcements[txhash][peer].m_state == State::REQUESTED; |
| 291 | + candidates += m_announcements[txhash][peer].m_state == State::CANDIDATE; |
| 292 | + } |
| 293 | + assert(m_tracker.Count(peer) == tracked); |
| 294 | + assert(m_tracker.CountInFlight(peer) == inflight); |
| 295 | + assert(m_tracker.CountCandidates(peer) == candidates); |
| 296 | + total += tracked; |
| 297 | + } |
| 298 | + // Compare Size. |
| 299 | + assert(m_tracker.Size() == total); |
| 300 | + |
| 301 | + // Invoke internal consistency check of TxRequestTracker object. |
| 302 | + m_tracker.SanityCheck(); |
| 303 | + } |
| 304 | +}; |
| 305 | +} // namespace |
| 306 | + |
| 307 | +void test_one_input(const std::vector<uint8_t>& buffer) |
| 308 | +{ |
| 309 | + // Tester object (which encapsulates a TxRequestTracker). |
| 310 | + Tester tester; |
| 311 | + |
| 312 | + // Decode the input as a sequence of instructions with parameters |
| 313 | + auto it = buffer.begin(); |
| 314 | + while (it != buffer.end()) { |
| 315 | + int cmd = *(it++) % 11; |
| 316 | + int peer, txidnum, delaynum; |
| 317 | + switch (cmd) { |
| 318 | + case 0: // Make time jump to the next event (m_time of CANDIDATE or REQUESTED) |
| 319 | + tester.AdvanceToEvent(); |
| 320 | + break; |
| 321 | + case 1: // Change time |
| 322 | + delaynum = it == buffer.end() ? 0 : *(it++); |
| 323 | + tester.AdvanceTime(DELAYS[delaynum]); |
| 324 | + break; |
| 325 | + case 2: // Query for requestable txs |
| 326 | + peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
| 327 | + tester.GetRequestable(peer); |
| 328 | + break; |
| 329 | + case 3: // Peer went offline |
| 330 | + peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
| 331 | + tester.DisconnectedPeer(peer); |
| 332 | + break; |
| 333 | + case 4: // No longer need tx |
| 334 | + txidnum = it == buffer.end() ? 0 : *(it++); |
| 335 | + tester.ForgetTxHash(txidnum % MAX_TXHASHES); |
| 336 | + break; |
| 337 | + case 5: // Received immediate preferred inv |
| 338 | + case 6: // Same, but non-preferred. |
| 339 | + peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
| 340 | + txidnum = it == buffer.end() ? 0 : *(it++); |
| 341 | + tester.ReceivedInv(peer, txidnum % MAX_TXHASHES, (txidnum / MAX_TXHASHES) & 1, cmd & 1, |
| 342 | + std::chrono::microseconds::min()); |
| 343 | + break; |
| 344 | + case 7: // Received delayed preferred inv |
| 345 | + case 8: // Same, but non-preferred. |
| 346 | + peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
| 347 | + txidnum = it == buffer.end() ? 0 : *(it++); |
| 348 | + delaynum = it == buffer.end() ? 0 : *(it++); |
| 349 | + tester.ReceivedInv(peer, txidnum % MAX_TXHASHES, (txidnum / MAX_TXHASHES) & 1, cmd & 1, |
| 350 | + tester.Now() + DELAYS[delaynum]); |
| 351 | + break; |
| 352 | + case 9: // Requested tx from peer |
| 353 | + peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
| 354 | + txidnum = it == buffer.end() ? 0 : *(it++); |
| 355 | + delaynum = it == buffer.end() ? 0 : *(it++); |
| 356 | + tester.RequestedTx(peer, txidnum % MAX_TXHASHES, tester.Now() + DELAYS[delaynum]); |
| 357 | + break; |
| 358 | + case 10: // Received response |
| 359 | + peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
| 360 | + txidnum = it == buffer.end() ? 0 : *(it++); |
| 361 | + tester.ReceivedResponse(peer, txidnum % MAX_TXHASHES); |
| 362 | + break; |
| 363 | + default: |
| 364 | + assert(false); |
| 365 | + } |
| 366 | + } |
| 367 | + tester.Check(); |
| 368 | +} |
0 commit comments