Skip to content

Commit 5b03121

Browse files
committed
Add txrequest fuzz tests
This adds a fuzz test that reimplements a naive reimplementation of TxRequestTracker (with up to 16 fixed peers and 16 fixed txhashes), and compares the real implementation against it.
1 parent 3c7fe0e commit 5b03121

File tree

2 files changed

+375
-0
lines changed

2 files changed

+375
-0
lines changed

src/Makefile.test.include

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ FUZZ_TARGETS = \
151151
test/fuzz/tx_in_deserialize \
152152
test/fuzz/tx_out \
153153
test/fuzz/txoutcompressor_deserialize \
154+
test/fuzz/txrequest \
154155
test/fuzz/txundo_deserialize \
155156
test/fuzz/uint160_deserialize \
156157
test/fuzz/uint256_deserialize
@@ -1215,6 +1216,12 @@ test_fuzz_txoutcompressor_deserialize_LDADD = $(FUZZ_SUITE_LD_COMMON)
12151216
test_fuzz_txoutcompressor_deserialize_LDFLAGS = $(FUZZ_SUITE_LDFLAGS_COMMON)
12161217
test_fuzz_txoutcompressor_deserialize_SOURCES = test/fuzz/deserialize.cpp
12171218

1219+
test_fuzz_txrequest_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
1220+
test_fuzz_txrequest_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
1221+
test_fuzz_txrequest_LDADD = $(FUZZ_SUITE_LD_COMMON)
1222+
test_fuzz_txrequest_LDFLAGS = $(FUZZ_SUITE_LDFLAGS_COMMON)
1223+
test_fuzz_txrequest_SOURCES = test/fuzz/txrequest.cpp
1224+
12181225
test_fuzz_txundo_deserialize_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) -DTXUNDO_DESERIALIZE=1
12191226
test_fuzz_txundo_deserialize_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
12201227
test_fuzz_txundo_deserialize_LDADD = $(FUZZ_SUITE_LD_COMMON)

src/test/fuzz/txrequest.cpp

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
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

Comments
 (0)