/root/bitcoin/src/test/fuzz/txrequest.cpp
Line | Count | Source |
1 | | // Copyright (c) 2020-2021 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 | 0 | { |
32 | 0 | for (uint8_t txhash = 0; txhash < MAX_TXHASHES; txhash += 1) { |
33 | 0 | CSHA256().Write(&txhash, 1).Finalize(TXHASHES[txhash].begin()); |
34 | 0 | } |
35 | 0 | int i = 0; |
36 | | // DELAYS[N] for N=0..15 is just N microseconds. |
37 | 0 | for (; i < 16; ++i) { |
38 | 0 | DELAYS[i] = std::chrono::microseconds{i}; |
39 | 0 | } |
40 | | // DELAYS[N] for N=16..127 has randomly-looking but roughly exponentially increasing values up to |
41 | | // 198.416453 seconds. |
42 | 0 | for (; i < 128; ++i) { |
43 | 0 | int diff_bits = ((i - 10) * 2) / 9; |
44 | 0 | uint64_t diff = 1 + (CSipHasher(0, 0).Write(i).Finalize() >> (64 - diff_bits)); |
45 | 0 | DELAYS[i] = DELAYS[i - 1] + std::chrono::microseconds{diff}; |
46 | 0 | } |
47 | | // DELAYS[N] for N=128..255 are negative delays with the same magnitude as N=0..127. |
48 | 0 | for (; i < 256; ++i) { |
49 | 0 | DELAYS[i] = -DELAYS[255 - i]; |
50 | 0 | } |
51 | 0 | } |
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 | 0 | { |
110 | 0 | bool all_nothing = true; |
111 | 0 | for (int peer = 0; peer < MAX_PEERS; ++peer) { |
112 | 0 | const Announcement& ann = m_announcements[txhash][peer]; |
113 | 0 | if (ann.m_state != State::NOTHING) { |
114 | 0 | if (ann.m_state != State::COMPLETED) return; |
115 | 0 | all_nothing = false; |
116 | 0 | } |
117 | 0 | } |
118 | 0 | if (all_nothing) return; |
119 | 0 | for (int peer = 0; peer < MAX_PEERS; ++peer) { |
120 | 0 | m_announcements[txhash][peer].m_state = State::NOTHING; |
121 | 0 | } |
122 | 0 | } |
123 | | |
124 | | //! Find the current best peer to request from for a txhash (or -1 if none). |
125 | | int GetSelected(int txhash) const |
126 | 0 | { |
127 | 0 | int ret = -1; |
128 | 0 | uint64_t ret_priority = 0; |
129 | 0 | for (int peer = 0; peer < MAX_PEERS; ++peer) { |
130 | 0 | const Announcement& ann = m_announcements[txhash][peer]; |
131 | | // Return -1 if there already is a (non-expired) in-flight request. |
132 | 0 | 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 | 0 | if (ann.m_state == State::CANDIDATE && ann.m_time <= m_now) { |
135 | 0 | if (ret == -1 || ann.m_priority > ret_priority) { |
136 | 0 | std::tie(ret, ret_priority) = std::tie(peer, ann.m_priority); |
137 | 0 | } |
138 | 0 | } |
139 | 0 | } |
140 | 0 | return ret; |
141 | 0 | } |
142 | | |
143 | | public: |
144 | 0 | Tester() : m_tracker(true) {} |
145 | | |
146 | 0 | std::chrono::microseconds Now() const { return m_now; } |
147 | | |
148 | | void AdvanceTime(std::chrono::microseconds offset) |
149 | 0 | { |
150 | 0 | m_now += offset; |
151 | 0 | while (!m_events.empty() && m_events.top() <= m_now) m_events.pop(); |
152 | 0 | } |
153 | | |
154 | | void AdvanceToEvent() |
155 | 0 | { |
156 | 0 | while (!m_events.empty() && m_events.top() <= m_now) m_events.pop(); |
157 | 0 | if (!m_events.empty()) { |
158 | 0 | m_now = m_events.top(); |
159 | 0 | m_events.pop(); |
160 | 0 | } |
161 | 0 | } |
162 | | |
163 | | void DisconnectedPeer(int peer) |
164 | 0 | { |
165 | | // Apply to naive structure: all announcements for that peer are wiped. |
166 | 0 | for (int txhash = 0; txhash < MAX_TXHASHES; ++txhash) { |
167 | 0 | if (m_announcements[txhash][peer].m_state != State::NOTHING) { |
168 | 0 | m_announcements[txhash][peer].m_state = State::NOTHING; |
169 | 0 | Cleanup(txhash); |
170 | 0 | } |
171 | 0 | } |
172 | | |
173 | | // Call TxRequestTracker's implementation. |
174 | 0 | m_tracker.DisconnectedPeer(peer); |
175 | 0 | } |
176 | | |
177 | | void ForgetTxHash(int txhash) |
178 | 0 | { |
179 | | // Apply to naive structure: all announcements for that txhash are wiped. |
180 | 0 | for (int peer = 0; peer < MAX_PEERS; ++peer) { |
181 | 0 | m_announcements[txhash][peer].m_state = State::NOTHING; |
182 | 0 | } |
183 | 0 | Cleanup(txhash); |
184 | | |
185 | | // Call TxRequestTracker's implementation. |
186 | 0 | m_tracker.ForgetTxHash(TXHASHES[txhash]); |
187 | 0 | } |
188 | | |
189 | | void ReceivedInv(int peer, int txhash, bool is_wtxid, bool preferred, std::chrono::microseconds reqtime) |
190 | 0 | { |
191 | | // Apply to naive structure: if no announcement for txidnum/peer combination |
192 | | // already, create a new CANDIDATE; otherwise do nothing. |
193 | 0 | Announcement& ann = m_announcements[txhash][peer]; |
194 | 0 | if (ann.m_state == State::NOTHING) { |
195 | 0 | ann.m_preferred = preferred; |
196 | 0 | ann.m_state = State::CANDIDATE; |
197 | 0 | ann.m_time = reqtime; |
198 | 0 | ann.m_is_wtxid = is_wtxid; |
199 | 0 | ann.m_sequence = m_current_sequence++; |
200 | 0 | 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 | 0 | if (reqtime > m_now) m_events.push(reqtime); |
204 | 0 | } |
205 | | |
206 | | // Call TxRequestTracker's implementation. |
207 | 0 | m_tracker.ReceivedInv(peer, is_wtxid ? GenTxid::Wtxid(TXHASHES[txhash]) : GenTxid::Txid(TXHASHES[txhash]), preferred, reqtime); |
208 | 0 | } |
209 | | |
210 | | void RequestedTx(int peer, int txhash, std::chrono::microseconds exptime) |
211 | 0 | { |
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 | 0 | if (m_announcements[txhash][peer].m_state == State::CANDIDATE) { |
215 | 0 | for (int peer2 = 0; peer2 < MAX_PEERS; ++peer2) { |
216 | 0 | if (m_announcements[txhash][peer2].m_state == State::REQUESTED) { |
217 | 0 | m_announcements[txhash][peer2].m_state = State::COMPLETED; |
218 | 0 | } |
219 | 0 | } |
220 | 0 | m_announcements[txhash][peer].m_state = State::REQUESTED; |
221 | 0 | m_announcements[txhash][peer].m_time = exptime; |
222 | 0 | } |
223 | | |
224 | | // Add event so that AdvanceToEvent can quickly jump to the point where its exptime passes. |
225 | 0 | if (exptime > m_now) m_events.push(exptime); |
226 | | |
227 | | // Call TxRequestTracker's implementation. |
228 | 0 | m_tracker.RequestedTx(peer, TXHASHES[txhash], exptime); |
229 | 0 | } |
230 | | |
231 | | void ReceivedResponse(int peer, int txhash) |
232 | 0 | { |
233 | | // Apply to naive structure: convert anything to COMPLETED. |
234 | 0 | if (m_announcements[txhash][peer].m_state != State::NOTHING) { |
235 | 0 | m_announcements[txhash][peer].m_state = State::COMPLETED; |
236 | 0 | Cleanup(txhash); |
237 | 0 | } |
238 | | |
239 | | // Call TxRequestTracker's implementation. |
240 | 0 | m_tracker.ReceivedResponse(peer, TXHASHES[txhash]); |
241 | 0 | } |
242 | | |
243 | | void GetRequestable(int peer) |
244 | 0 | { |
245 | | // Implement using naive structure: |
246 | | |
247 | | //! list of (sequence number, txhash, is_wtxid) tuples. |
248 | 0 | std::vector<std::tuple<uint64_t, int, bool>> result; |
249 | 0 | std::vector<std::pair<NodeId, GenTxid>> expected_expired; |
250 | 0 | for (int txhash = 0; txhash < MAX_TXHASHES; ++txhash) { |
251 | | // Mark any expired REQUESTED announcements as COMPLETED. |
252 | 0 | for (int peer2 = 0; peer2 < MAX_PEERS; ++peer2) { |
253 | 0 | Announcement& ann2 = m_announcements[txhash][peer2]; |
254 | 0 | if (ann2.m_state == State::REQUESTED && ann2.m_time <= m_now) { |
255 | 0 | expected_expired.emplace_back(peer2, ann2.m_is_wtxid ? GenTxid::Wtxid(TXHASHES[txhash]) : GenTxid::Txid(TXHASHES[txhash])); |
256 | 0 | ann2.m_state = State::COMPLETED; |
257 | 0 | break; |
258 | 0 | } |
259 | 0 | } |
260 | | // And delete txids with only COMPLETED announcements left. |
261 | 0 | Cleanup(txhash); |
262 | | // CANDIDATEs for which this announcement has the highest priority get returned. |
263 | 0 | const Announcement& ann = m_announcements[txhash][peer]; |
264 | 0 | if (ann.m_state == State::CANDIDATE && GetSelected(txhash) == peer) { |
265 | 0 | result.emplace_back(ann.m_sequence, txhash, ann.m_is_wtxid); |
266 | 0 | } |
267 | 0 | } |
268 | | // Sort the results by sequence number. |
269 | 0 | std::sort(result.begin(), result.end()); |
270 | 0 | std::sort(expected_expired.begin(), expected_expired.end()); |
271 | | |
272 | | // Compare with TxRequestTracker's implementation. |
273 | 0 | std::vector<std::pair<NodeId, GenTxid>> expired; |
274 | 0 | const auto actual = m_tracker.GetRequestable(peer, m_now, &expired); |
275 | 0 | std::sort(expired.begin(), expired.end()); |
276 | 0 | assert(expired == expected_expired); |
277 | | |
278 | 0 | m_tracker.PostGetRequestableSanityCheck(m_now); |
279 | 0 | assert(result.size() == actual.size()); |
280 | 0 | for (size_t pos = 0; pos < actual.size(); ++pos) { |
281 | 0 | assert(TXHASHES[std::get<1>(result[pos])] == actual[pos].GetHash()); |
282 | 0 | assert(std::get<2>(result[pos]) == actual[pos].IsWtxid()); |
283 | 0 | } |
284 | 0 | } |
285 | | |
286 | | void Check() |
287 | 0 | { |
288 | | // Compare CountTracked and CountLoad with naive structure. |
289 | 0 | size_t total = 0; |
290 | 0 | for (int peer = 0; peer < MAX_PEERS; ++peer) { |
291 | 0 | size_t tracked = 0; |
292 | 0 | size_t inflight = 0; |
293 | 0 | size_t candidates = 0; |
294 | 0 | for (int txhash = 0; txhash < MAX_TXHASHES; ++txhash) { |
295 | 0 | tracked += m_announcements[txhash][peer].m_state != State::NOTHING; |
296 | 0 | inflight += m_announcements[txhash][peer].m_state == State::REQUESTED; |
297 | 0 | candidates += m_announcements[txhash][peer].m_state == State::CANDIDATE; |
298 | |
|
299 | 0 | std::bitset<MAX_PEERS> expected_announcers; |
300 | 0 | for (int peer = 0; peer < MAX_PEERS; ++peer) { |
301 | 0 | if (m_announcements[txhash][peer].m_state == State::CANDIDATE || m_announcements[txhash][peer].m_state == State::REQUESTED) { |
302 | 0 | expected_announcers[peer] = true; |
303 | 0 | } |
304 | 0 | } |
305 | 0 | std::vector<NodeId> candidate_peers; |
306 | 0 | m_tracker.GetCandidatePeers(TXHASHES[txhash], candidate_peers); |
307 | 0 | assert(expected_announcers.count() == candidate_peers.size()); |
308 | 0 | for (const auto& peer : candidate_peers) { |
309 | 0 | assert(expected_announcers[peer]); |
310 | 0 | } |
311 | 0 | } |
312 | 0 | assert(m_tracker.Count(peer) == tracked); |
313 | 0 | assert(m_tracker.CountInFlight(peer) == inflight); |
314 | 0 | assert(m_tracker.CountCandidates(peer) == candidates); |
315 | 0 | total += tracked; |
316 | 0 | } |
317 | | // Compare Size. |
318 | 0 | assert(m_tracker.Size() == total); |
319 | | |
320 | | // Invoke internal consistency check of TxRequestTracker object. |
321 | 0 | m_tracker.SanityCheck(); |
322 | 0 | } |
323 | | }; |
324 | | } // namespace |
325 | | |
326 | | FUZZ_TARGET(txrequest) |
327 | 0 | { |
328 | | // Tester object (which encapsulates a TxRequestTracker). |
329 | 0 | Tester tester; |
330 | | |
331 | | // Decode the input as a sequence of instructions with parameters |
332 | 0 | auto it = buffer.begin(); |
333 | 0 | while (it != buffer.end()) { |
334 | 0 | int cmd = *(it++) % 11; |
335 | 0 | int peer, txidnum, delaynum; |
336 | 0 | switch (cmd) { |
337 | 0 | case 0: // Make time jump to the next event (m_time of CANDIDATE or REQUESTED) |
338 | 0 | tester.AdvanceToEvent(); |
339 | 0 | break; |
340 | 0 | case 1: // Change time |
341 | 0 | delaynum = it == buffer.end() ? 0 : *(it++); |
342 | 0 | tester.AdvanceTime(DELAYS[delaynum]); |
343 | 0 | break; |
344 | 0 | case 2: // Query for requestable txs |
345 | 0 | peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
346 | 0 | tester.GetRequestable(peer); |
347 | 0 | break; |
348 | 0 | case 3: // Peer went offline |
349 | 0 | peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
350 | 0 | tester.DisconnectedPeer(peer); |
351 | 0 | break; |
352 | 0 | case 4: // No longer need tx |
353 | 0 | txidnum = it == buffer.end() ? 0 : *(it++); |
354 | 0 | tester.ForgetTxHash(txidnum % MAX_TXHASHES); |
355 | 0 | break; |
356 | 0 | case 5: // Received immediate preferred inv |
357 | 0 | case 6: // Same, but non-preferred. |
358 | 0 | peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
359 | 0 | txidnum = it == buffer.end() ? 0 : *(it++); |
360 | 0 | tester.ReceivedInv(peer, txidnum % MAX_TXHASHES, (txidnum / MAX_TXHASHES) & 1, cmd & 1, |
361 | 0 | std::chrono::microseconds::min()); |
362 | 0 | break; |
363 | 0 | case 7: // Received delayed preferred inv |
364 | 0 | case 8: // Same, but non-preferred. |
365 | 0 | peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
366 | 0 | txidnum = it == buffer.end() ? 0 : *(it++); |
367 | 0 | delaynum = it == buffer.end() ? 0 : *(it++); |
368 | 0 | tester.ReceivedInv(peer, txidnum % MAX_TXHASHES, (txidnum / MAX_TXHASHES) & 1, cmd & 1, |
369 | 0 | tester.Now() + DELAYS[delaynum]); |
370 | 0 | break; |
371 | 0 | case 9: // Requested tx from peer |
372 | 0 | peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
373 | 0 | txidnum = it == buffer.end() ? 0 : *(it++); |
374 | 0 | delaynum = it == buffer.end() ? 0 : *(it++); |
375 | 0 | tester.RequestedTx(peer, txidnum % MAX_TXHASHES, tester.Now() + DELAYS[delaynum]); |
376 | 0 | break; |
377 | 0 | case 10: // Received response |
378 | 0 | peer = it == buffer.end() ? 0 : *(it++) % MAX_PEERS; |
379 | 0 | txidnum = it == buffer.end() ? 0 : *(it++); |
380 | 0 | tester.ReceivedResponse(peer, txidnum % MAX_TXHASHES); |
381 | 0 | break; |
382 | 0 | default: |
383 | 0 | assert(false); |
384 | 0 | } |
385 | 0 | } |
386 | 0 | tester.Check(); |
387 | 0 | } |