Skip to content

Commit 05e6241

Browse files
glozowinstagibbs
andcommitted
[fuzz] txorphanage_impl protection harness
This fuzzer specifically tests protection of an honest peer's orphans. Co-authored-by: Greg Sanders <gsanders87@gmail.com>
1 parent 3b4e2ea commit 05e6241

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

src/test/fuzz/txorphan.cpp

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <util/check.h>
2020
#include <util/time.h>
2121

22+
#include <bitset>
2223
#include <cstdint>
2324
#include <memory>
2425
#include <set>
@@ -228,3 +229,168 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage)
228229
}
229230
orphanage->SanityCheck();
230231
}
232+
233+
FUZZ_TARGET(txorphan_protected, .init = initialize_orphanage)
234+
{
235+
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
236+
FastRandomContext orphanage_rng{/*fDeterministic=*/true};
237+
SetMockTime(ConsumeTime(fuzzed_data_provider));
238+
239+
// We have NUM_PEERS, of which Peer==0 is the "honest" one
240+
// who will never exceed their reserved weight or announcement
241+
// count, and should therefore never be evicted.
242+
const unsigned int MAX_PEERS = 125;
243+
const unsigned int NUM_PEERS = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(1, MAX_PEERS);
244+
// Generate a vector of bools for whether each peer is protected from eviction
245+
std::bitset<MAX_PEERS> protected_peers;
246+
for (unsigned int i = 0; i < NUM_PEERS; i++) {
247+
protected_peers.set(i, fuzzed_data_provider.ConsumeBool());
248+
}
249+
250+
// Params for orphanage.
251+
const unsigned int global_announcement_limit = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(NUM_PEERS, 6'000);
252+
const int64_t per_peer_weight_reservation = fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(1, 4'040'000);
253+
auto orphanage = node::MakeTxOrphanage(global_announcement_limit, per_peer_weight_reservation);
254+
255+
// The actual limit, MaxPeerAnnouncements(), may be higher, since TxOrphanage only counts peers
256+
// that have announced an orphan. The honest peer will not experience evictions if it never
257+
// exceeds this.
258+
const unsigned int honest_ann_limit = global_announcement_limit / NUM_PEERS;
259+
// Honest peer will not experience evictions if it never exceeds this.
260+
const int64_t honest_mem_limit = per_peer_weight_reservation;
261+
262+
std::vector<COutPoint> outpoints; // Duplicates are tolerated
263+
outpoints.reserve(200'000);
264+
265+
// initial outpoints used to construct transactions later
266+
for (uint8_t i = 0; i < 4; i++) {
267+
outpoints.emplace_back(Txid::FromUint256(uint256{i}), 0);
268+
}
269+
270+
// These are honest peer's live announcements. We expect them to be protected from eviction.
271+
std::set<Wtxid> protected_wtxids;
272+
273+
LIMITED_WHILE(outpoints.size() < 200'000 && fuzzed_data_provider.ConsumeBool(), 10 * global_announcement_limit)
274+
{
275+
// construct transaction
276+
const CTransactionRef tx = [&] {
277+
CMutableTransaction tx_mut;
278+
const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, outpoints.size());
279+
const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, 256);
280+
// pick outpoints from outpoints as input. We allow input duplicates on purpose, given we are not
281+
// running any transaction validation logic before adding transactions to the orphanage
282+
tx_mut.vin.reserve(num_in);
283+
for (uint32_t i = 0; i < num_in; i++) {
284+
auto& prevout = PickValue(fuzzed_data_provider, outpoints);
285+
// try making transactions unique by setting a random nSequence, but allow duplicate transactions if they happen
286+
tx_mut.vin.emplace_back(prevout, CScript{}, fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, CTxIn::SEQUENCE_FINAL));
287+
}
288+
// output amount or spendability will not affect txorphanage
289+
tx_mut.vout.reserve(num_out);
290+
for (uint32_t i = 0; i < num_out; i++) {
291+
const auto payload_size = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(0, 100000);
292+
if (payload_size) {
293+
tx_mut.vout.emplace_back(0, CScript() << OP_RETURN << std::vector<unsigned char>(payload_size));
294+
} else {
295+
tx_mut.vout.emplace_back(0, CScript{});
296+
}
297+
}
298+
auto new_tx = MakeTransactionRef(tx_mut);
299+
// add newly constructed outpoints to the coin pool
300+
for (uint32_t i = 0; i < num_out; i++) {
301+
outpoints.emplace_back(new_tx->GetHash(), i);
302+
}
303+
return new_tx;
304+
}();
305+
306+
const auto wtxid{tx->GetWitnessHash()};
307+
308+
// orphanage functions
309+
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10 * global_announcement_limit)
310+
{
311+
NodeId peer_id = fuzzed_data_provider.ConsumeIntegralInRange<NodeId>(0, NUM_PEERS - 1);
312+
const auto tx_weight{GetTransactionWeight(*tx)};
313+
314+
// This protected peer will never send orphans that would
315+
// exceed their own personal allotment, so is never evicted.
316+
const bool peer_is_protected{protected_peers[peer_id]};
317+
318+
CallOneOf(
319+
fuzzed_data_provider,
320+
[&] { // AddTx
321+
bool have_tx_and_peer = orphanage->HaveTxFromPeer(wtxid, peer_id);
322+
if (peer_is_protected && !have_tx_and_peer &&
323+
(orphanage->UsageFromPeer(peer_id) + tx_weight > honest_mem_limit ||
324+
orphanage->AnnouncementsFromPeer(peer_id) + 1 > honest_ann_limit)) {
325+
// We never want our protected peer oversized or over-announced
326+
} else {
327+
orphanage->AddTx(tx, peer_id);
328+
if (peer_is_protected && orphanage->HaveTxFromPeer(wtxid, peer_id)) {
329+
protected_wtxids.insert(wtxid);
330+
}
331+
}
332+
},
333+
[&] { // AddAnnouncer
334+
bool have_tx_and_peer = orphanage->HaveTxFromPeer(tx->GetWitnessHash(), peer_id);
335+
// AddAnnouncer should return false if tx doesn't exist or we already HaveTxFromPeer.
336+
{
337+
if (peer_is_protected && !have_tx_and_peer &&
338+
(orphanage->UsageFromPeer(peer_id) + tx_weight > honest_mem_limit ||
339+
orphanage->AnnouncementsFromPeer(peer_id) + 1 > honest_ann_limit)) {
340+
// We never want our protected peer oversized
341+
} else {
342+
orphanage->AddAnnouncer(tx->GetWitnessHash(), peer_id);
343+
if (peer_is_protected && orphanage->HaveTxFromPeer(wtxid, peer_id)) {
344+
protected_wtxids.insert(wtxid);
345+
}
346+
}
347+
}
348+
},
349+
[&] { // EraseTx
350+
if (protected_wtxids.count(tx->GetWitnessHash())) {
351+
protected_wtxids.erase(wtxid);
352+
}
353+
orphanage->EraseTx(wtxid);
354+
Assert(!orphanage->HaveTx(wtxid));
355+
},
356+
[&] { // EraseForPeer
357+
if (!protected_peers[peer_id]) {
358+
orphanage->EraseForPeer(peer_id);
359+
}
360+
},
361+
[&] { // LimitOrphans
362+
// Assert that protected peers are never affected by LimitOrphans.
363+
unsigned int protected_count = 0;
364+
unsigned int protected_bytes = 0;
365+
for (unsigned int peer = 0; peer < NUM_PEERS; ++peer) {
366+
if (protected_peers[peer]) {
367+
protected_count += orphanage->AnnouncementsFromPeer(peer);
368+
protected_bytes += orphanage->UsageByPeer(peer);
369+
}
370+
}
371+
orphanage->LimitOrphans();
372+
Assert(!orphanage->NeedsTrim());
373+
Assert(orphanage->CountAnnouncements() <= global_announcement_limit);
374+
Assert(orphanage->TotalOrphanUsage() <= per_peer_weight_reservation * NUM_PEERS);
375+
376+
// Number of announcements and usage should never differ before and after since
377+
// we've never exceeded the per-peer reservations.
378+
for (unsigned int peer = 0; peer < NUM_PEERS; ++peer) {
379+
if (protected_peers[peer]) {
380+
protected_count -= orphanage->AnnouncementsFromPeer(peer);
381+
protected_bytes -= orphanage->UsageByPeer(peer);
382+
}
383+
}
384+
Assert(protected_count == 0);
385+
Assert(protected_bytes == 0);
386+
});
387+
388+
}
389+
}
390+
391+
orphanage->SanityCheck();
392+
// All of the honest peer's announcements are still present.
393+
for (const auto& wtxid : protected_wtxids) {
394+
Assert(orphanage->HaveTx(wtxid));
395+
}
396+
}

0 commit comments

Comments
 (0)