Coverage Report

Created: 2025-05-14 12:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}