Coverage Report

Created: 2025-09-19 18:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/bitcoin/src/test/fuzz/txorphan.cpp
Line
Count
Source
1
// Copyright (c) 2022-present 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 <consensus/amount.h>
6
#include <consensus/validation.h>
7
#include <net_processing.h>
8
#include <node/eviction.h>
9
#include <node/txorphanage.h>
10
#include <policy/policy.h>
11
#include <primitives/transaction.h>
12
#include <script/script.h>
13
#include <sync.h>
14
#include <test/fuzz/FuzzedDataProvider.h>
15
#include <test/fuzz/fuzz.h>
16
#include <test/fuzz/util.h>
17
#include <test/util/setup_common.h>
18
#include <uint256.h>
19
#include <util/check.h>
20
#include <util/feefrac.h>
21
#include <util/time.h>
22
23
#include <algorithm>
24
#include <bitset>
25
#include <cmath>
26
#include <cstdint>
27
#include <iostream>
28
#include <memory>
29
#include <set>
30
#include <utility>
31
#include <vector>
32
33
void initialize_orphanage()
34
0
{
35
0
    static const auto testing_setup = MakeNoLogFileContext();
36
0
}
37
38
FUZZ_TARGET(txorphan, .init = initialize_orphanage)
39
0
{
40
0
    SeedRandomStateForTest(SeedRand::ZEROS);
41
0
    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
42
0
    FastRandomContext orphanage_rng{ConsumeUInt256(fuzzed_data_provider)};
43
0
    SetMockTime(ConsumeTime(fuzzed_data_provider));
44
45
0
    auto orphanage = node::MakeTxOrphanage();
46
0
    std::vector<COutPoint> outpoints; // Duplicates are tolerated
47
0
    outpoints.reserve(200'000);
48
49
    // initial outpoints used to construct transactions later
50
0
    for (uint8_t i = 0; i < 4; i++) {
51
0
        outpoints.emplace_back(Txid::FromUint256(uint256{i}), 0);
52
0
    }
53
54
0
    CTransactionRef ptx_potential_parent = nullptr;
55
56
0
    std::vector<CTransactionRef> tx_history;
57
58
0
    LIMITED_WHILE(outpoints.size() < 200'000 && fuzzed_data_provider.ConsumeBool(), 1000)
59
0
    {
60
        // construct transaction
61
0
        const CTransactionRef tx = [&] {
62
0
            CMutableTransaction tx_mut;
63
0
            const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, outpoints.size());
64
0
            const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, 256);
65
            // pick outpoints from outpoints as input. We allow input duplicates on purpose, given we are not
66
            // running any transaction validation logic before adding transactions to the orphanage
67
0
            tx_mut.vin.reserve(num_in);
68
0
            for (uint32_t i = 0; i < num_in; i++) {
69
0
                auto& prevout = PickValue(fuzzed_data_provider, outpoints);
70
                // try making transactions unique by setting a random nSequence, but allow duplicate transactions if they happen
71
0
                tx_mut.vin.emplace_back(prevout, CScript{}, fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, CTxIn::SEQUENCE_FINAL));
72
0
            }
73
            // output amount will not affect txorphanage
74
0
            tx_mut.vout.reserve(num_out);
75
0
            for (uint32_t i = 0; i < num_out; i++) {
76
0
                tx_mut.vout.emplace_back(CAmount{0}, CScript{});
77
0
            }
78
0
            auto new_tx = MakeTransactionRef(tx_mut);
79
            // add newly constructed outpoints to the coin pool
80
0
            for (uint32_t i = 0; i < num_out; i++) {
81
0
                outpoints.emplace_back(new_tx->GetHash(), i);
82
0
            }
83
0
            return new_tx;
84
0
        }();
85
86
0
        tx_history.push_back(tx);
87
88
0
        const auto wtxid{tx->GetWitnessHash()};
89
90
        // Trigger orphanage functions that are called using parents. ptx_potential_parent is a tx we constructed in a
91
        // previous loop and potentially the parent of this tx.
92
0
        if (ptx_potential_parent) {
93
            // Set up future GetTxToReconsider call.
94
0
            orphanage->AddChildrenToWorkSet(*ptx_potential_parent, orphanage_rng);
95
96
            // Check that all txns returned from GetChildrenFrom* are indeed a direct child of this tx.
97
0
            NodeId peer_id = fuzzed_data_provider.ConsumeIntegral<NodeId>();
98
0
            for (const auto& child : orphanage->GetChildrenFromSamePeer(ptx_potential_parent, peer_id)) {
99
0
                assert(std::any_of(child->vin.cbegin(), child->vin.cend(), [&](const auto& input) {
100
0
                    return input.prevout.hash == ptx_potential_parent->GetHash();
101
0
                }));
102
0
            }
103
0
        }
104
105
        // trigger orphanage functions
106
0
        LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 1000)
107
0
        {
108
0
            NodeId peer_id = fuzzed_data_provider.ConsumeIntegral<NodeId>();
109
0
            const auto total_bytes_start{orphanage->TotalOrphanUsage()};
110
0
            const auto total_peer_bytes_start{orphanage->UsageByPeer(peer_id)};
111
0
            const auto tx_weight{GetTransactionWeight(*tx)};
112
113
0
            CallOneOf(
114
0
                fuzzed_data_provider,
115
0
                [&] {
116
0
                    {
117
0
                        CTransactionRef ref = orphanage->GetTxToReconsider(peer_id);
118
0
                        if (ref) {
119
0
                            Assert(orphanage->HaveTx(ref->GetWitnessHash()));
120
0
                        }
121
0
                    }
122
0
                },
123
0
                [&] {
124
0
                    bool have_tx = orphanage->HaveTx(tx->GetWitnessHash());
125
0
                    bool have_tx_and_peer = orphanage->HaveTxFromPeer(wtxid, peer_id);
126
                    // AddTx should return false if tx is too big or already have it
127
                    // tx weight is unknown, we only check when tx is already in orphanage
128
0
                    {
129
0
                        bool add_tx = orphanage->AddTx(tx, peer_id);
130
                        // have_tx == true -> add_tx == false
131
0
                        Assert(!have_tx || !add_tx);
132
                        // have_tx_and_peer == true -> add_tx == false
133
0
                        Assert(!have_tx_and_peer || !add_tx);
134
                        // After AddTx, the orphanage may trim itself, so the peer's usage may have gone up or down.
135
136
0
                        if (add_tx) {
137
0
                            Assert(tx_weight <= MAX_STANDARD_TX_WEIGHT);
138
0
                        } else {
139
                            // Peer may have been added as an announcer.
140
0
                            if (orphanage->UsageByPeer(peer_id) > total_peer_bytes_start) {
141
0
                                Assert(orphanage->HaveTxFromPeer(wtxid, peer_id));
142
0
                            }
143
144
                            // If announcement was added, total bytes does not increase.
145
                            // However, if eviction was triggered, the value may decrease.
146
0
                            Assert(orphanage->TotalOrphanUsage() <= total_bytes_start);
147
0
                        }
148
0
                    }
149
                    // We are not guaranteed to have_tx after AddTx. There are a few possible reasons:
150
                    // - tx itself exceeds the per-peer memory usage limit, so LimitOrphans had to remove it immediately
151
                    // - tx itself exceeds the per-peer latency score limit, so LimitOrphans had to remove it immediately
152
                    // - the orphanage needed trim and all other announcements from this peer are reconsiderable
153
0
                },
154
0
                [&] {
155
0
                    bool have_tx = orphanage->HaveTx(tx->GetWitnessHash());
156
0
                    bool have_tx_and_peer = orphanage->HaveTxFromPeer(tx->GetWitnessHash(), peer_id);
157
                    // AddAnnouncer should return false if tx doesn't exist or we already HaveTxFromPeer.
158
0
                    {
159
0
                        bool added_announcer = orphanage->AddAnnouncer(tx->GetWitnessHash(), peer_id);
160
                        // have_tx == false -> added_announcer == false
161
0
                        Assert(have_tx || !added_announcer);
162
                        // have_tx_and_peer == true -> added_announcer == false
163
0
                        Assert(!have_tx_and_peer || !added_announcer);
164
165
                        // If announcement was added, total bytes does not increase.
166
                        // However, if eviction was triggered, the value may decrease.
167
0
                        Assert(orphanage->TotalOrphanUsage() <= total_bytes_start);
168
0
                    }
169
0
                },
170
0
                [&] {
171
0
                    bool have_tx = orphanage->HaveTx(tx->GetWitnessHash());
172
0
                    bool have_tx_and_peer{orphanage->HaveTxFromPeer(wtxid, peer_id)};
173
                    // EraseTx should return 0 if m_orphans doesn't have the tx
174
0
                    {
175
0
                        auto bytes_from_peer_before{orphanage->UsageByPeer(peer_id)};
176
0
                        Assert(have_tx == orphanage->EraseTx(tx->GetWitnessHash()));
177
                        // After EraseTx, the orphanage may trim itself, so all peers' usage may have gone up or down.
178
0
                        if (have_tx) {
179
0
                            if (!have_tx_and_peer) {
180
0
                                Assert(orphanage->UsageByPeer(peer_id) == bytes_from_peer_before);
181
0
                            }
182
0
                        }
183
0
                    }
184
0
                    have_tx = orphanage->HaveTx(tx->GetWitnessHash());
185
0
                    have_tx_and_peer = orphanage->HaveTxFromPeer(wtxid, peer_id);
186
                    // have_tx should be false and EraseTx should fail
187
0
                    {
188
0
                        Assert(!have_tx && !have_tx_and_peer && !orphanage->EraseTx(wtxid));
189
0
                    }
190
0
                },
191
0
                [&] {
192
0
                    orphanage->EraseForPeer(peer_id);
193
0
                    Assert(!orphanage->HaveTxFromPeer(tx->GetWitnessHash(), peer_id));
194
0
                    Assert(orphanage->UsageByPeer(peer_id) == 0);
195
0
                },
196
0
                [&] {
197
                    // Make a block out of txs and then EraseForBlock
198
0
                    CBlock block;
199
0
                    int num_txs = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(0, 1000);
200
0
                    for (int i{0}; i < num_txs; ++i) {
201
0
                        auto& tx_to_remove = PickValue(fuzzed_data_provider, tx_history);
202
0
                        block.vtx.push_back(tx_to_remove);
203
0
                    }
204
0
                    orphanage->EraseForBlock(block);
205
0
                    for (const auto& tx_removed : block.vtx) {
206
0
                        Assert(!orphanage->HaveTx(tx_removed->GetWitnessHash()));
207
0
                        Assert(!orphanage->HaveTxFromPeer(tx_removed->GetWitnessHash(), peer_id));
208
0
                    }
209
0
                }
210
0
            );
211
0
        }
212
213
        // Set tx as potential parent to be used for future GetChildren() calls.
214
0
        if (!ptx_potential_parent || fuzzed_data_provider.ConsumeBool()) {
215
0
            ptx_potential_parent = tx;
216
0
        }
217
218
0
        const bool have_tx{orphanage->HaveTx(tx->GetWitnessHash())};
219
0
        const bool get_tx_nonnull{orphanage->GetTx(tx->GetWitnessHash()) != nullptr};
220
0
        Assert(have_tx == get_tx_nonnull);
221
0
    }
222
0
    orphanage->SanityCheck();
223
0
}
224
225
FUZZ_TARGET(txorphan_protected, .init = initialize_orphanage)
226
0
{
227
0
    SeedRandomStateForTest(SeedRand::ZEROS);
228
0
    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
229
0
    FastRandomContext orphanage_rng{ConsumeUInt256(fuzzed_data_provider)};
230
0
    SetMockTime(ConsumeTime(fuzzed_data_provider));
231
232
    // We have num_peers peers. Some subset of them will never exceed their reserved weight or announcement count, and
233
    // should therefore never have any orphans evicted.
234
0
    const unsigned int MAX_PEERS = 125;
235
0
    const unsigned int num_peers = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(1, MAX_PEERS);
236
    // Generate a vector of bools for whether each peer is protected from eviction
237
0
    std::bitset<MAX_PEERS> protected_peers;
238
0
    for (unsigned int i = 0; i < num_peers; i++) {
239
0
        protected_peers.set(i, fuzzed_data_provider.ConsumeBool());
240
0
    }
241
242
    // Params for orphanage.
243
0
    const unsigned int global_latency_score_limit = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(num_peers, 6'000);
244
0
    const int64_t per_peer_weight_reservation = fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(1, 4'040'000);
245
0
    auto orphanage = node::MakeTxOrphanage(global_latency_score_limit, per_peer_weight_reservation);
246
247
    // The actual limit, MaxPeerLatencyScore(), may be higher, since TxOrphanage only counts peers
248
    // that have announced an orphan. The honest peer will not experience evictions if it never
249
    // exceeds this.
250
0
    const unsigned int honest_latency_limit = global_latency_score_limit / num_peers;
251
    // Honest peer will not experience evictions if it never exceeds this.
252
0
    const int64_t honest_mem_limit = per_peer_weight_reservation;
253
254
0
    std::vector<COutPoint> outpoints; // Duplicates are tolerated
255
0
    outpoints.reserve(400);
256
257
    // initial outpoints used to construct transactions later
258
0
    for (uint8_t i = 0; i < 4; i++) {
259
0
        outpoints.emplace_back(Txid::FromUint256(uint256{i}), 0);
260
0
    }
261
262
    // These are honest peer's live announcements. We expect them to be protected from eviction.
263
0
    std::set<Wtxid> protected_wtxids;
264
265
0
    LIMITED_WHILE(outpoints.size() < 400 && fuzzed_data_provider.ConsumeBool(), 1000)
266
0
    {
267
        // construct transaction
268
0
        const CTransactionRef tx = [&] {
269
0
            CMutableTransaction tx_mut;
270
0
            const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, outpoints.size());
271
0
            const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, 256);
272
            // pick outpoints from outpoints as input. We allow input duplicates on purpose, given we are not
273
            // running any transaction validation logic before adding transactions to the orphanage
274
0
            tx_mut.vin.reserve(num_in);
275
0
            for (uint32_t i = 0; i < num_in; i++) {
276
0
                auto& prevout = PickValue(fuzzed_data_provider, outpoints);
277
                // try making transactions unique by setting a random nSequence, but allow duplicate transactions if they happen
278
0
                tx_mut.vin.emplace_back(prevout, CScript{}, fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, CTxIn::SEQUENCE_FINAL));
279
0
            }
280
            // output amount or spendability will not affect txorphanage
281
0
            tx_mut.vout.reserve(num_out);
282
0
            for (uint32_t i = 0; i < num_out; i++) {
283
0
                const auto payload_size = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(0, 100000);
284
0
                if (payload_size) {
285
0
                    tx_mut.vout.emplace_back(0, CScript() << OP_RETURN << std::vector<unsigned char>(payload_size));
286
0
                } else {
287
0
                    tx_mut.vout.emplace_back(0, CScript{});
288
0
                }
289
0
            }
290
0
            auto new_tx = MakeTransactionRef(tx_mut);
291
            // add newly constructed outpoints to the coin pool
292
0
            for (uint32_t i = 0; i < num_out; i++) {
293
0
                outpoints.emplace_back(new_tx->GetHash(), i);
294
0
            }
295
0
            return new_tx;
296
0
        }();
297
298
0
        const auto wtxid{tx->GetWitnessHash()};
299
300
        // orphanage functions
301
0
        LIMITED_WHILE(fuzzed_data_provider.remaining_bytes(), 10 * global_latency_score_limit)
302
0
        {
303
0
            NodeId peer_id = fuzzed_data_provider.ConsumeIntegralInRange<NodeId>(0, num_peers - 1);
304
0
            const auto tx_weight{GetTransactionWeight(*tx)};
305
306
            // This protected peer will never send orphans that would
307
            // exceed their own personal allotment, so is never evicted.
308
0
            const bool peer_is_protected{protected_peers[peer_id]};
309
310
0
            CallOneOf(
311
0
                fuzzed_data_provider,
312
0
                [&] { // AddTx
313
0
                    bool have_tx_and_peer = orphanage->HaveTxFromPeer(wtxid, peer_id);
314
0
                    if (peer_is_protected && !have_tx_and_peer &&
315
0
                        (orphanage->UsageByPeer(peer_id) + tx_weight > honest_mem_limit ||
316
0
                        orphanage->LatencyScoreFromPeer(peer_id) + (tx->vin.size() / 10) + 1 > honest_latency_limit)) {
317
                        // We never want our protected peer oversized or over-announced
318
0
                    } else {
319
0
                        orphanage->AddTx(tx, peer_id);
320
0
                        if (peer_is_protected && orphanage->HaveTxFromPeer(wtxid, peer_id)) {
321
0
                            protected_wtxids.insert(wtxid);
322
0
                        }
323
0
                    }
324
0
                },
325
0
                [&] { // AddAnnouncer
326
0
                    bool have_tx_and_peer = orphanage->HaveTxFromPeer(tx->GetWitnessHash(), peer_id);
327
                    // AddAnnouncer should return false if tx doesn't exist or we already HaveTxFromPeer.
328
0
                    {
329
0
                        if (peer_is_protected && !have_tx_and_peer &&
330
0
                            (orphanage->UsageByPeer(peer_id) + tx_weight > honest_mem_limit ||
331
0
                            orphanage->LatencyScoreFromPeer(peer_id) + (tx->vin.size() / 10) + 1 > honest_latency_limit)) {
332
                            // We never want our protected peer oversized
333
0
                        } else {
334
0
                            orphanage->AddAnnouncer(tx->GetWitnessHash(), peer_id);
335
0
                            if (peer_is_protected && orphanage->HaveTxFromPeer(wtxid, peer_id)) {
336
0
                                protected_wtxids.insert(wtxid);
337
0
                            }
338
0
                        }
339
0
                    }
340
0
                },
341
0
                [&] { // EraseTx
342
0
                    if (protected_wtxids.count(tx->GetWitnessHash())) {
343
0
                        protected_wtxids.erase(wtxid);
344
0
                    }
345
0
                    orphanage->EraseTx(wtxid);
346
0
                    Assert(!orphanage->HaveTx(wtxid));
347
0
                },
348
0
                [&] { // EraseForPeer
349
0
                    if (!protected_peers[peer_id]) {
350
0
                        orphanage->EraseForPeer(peer_id);
351
0
                        Assert(orphanage->UsageByPeer(peer_id) == 0);
352
0
                        Assert(orphanage->LatencyScoreFromPeer(peer_id) == 0);
353
0
                        Assert(orphanage->AnnouncementsFromPeer(peer_id) == 0);
354
0
                    }
355
0
                }
356
0
            );
357
0
        }
358
0
    }
359
360
0
    orphanage->SanityCheck();
361
    // All of the honest peer's announcements are still present.
362
0
    for (const auto& wtxid : protected_wtxids) {
363
0
        Assert(orphanage->HaveTx(wtxid));
364
0
    }
365
0
}
366
367
FUZZ_TARGET(txorphanage_sim)
368
0
{
369
0
    SeedRandomStateForTest(SeedRand::ZEROS);
370
    // This is a comprehensive simulation fuzz test, which runs through a scenario involving up to
371
    // 16 transactions (which may have simple or complex topology, and may have duplicate txids
372
    // with distinct wtxids, and up to 16 peers. The scenario is performed both on a real
373
    // TxOrphanage object and the behavior is compared with a naive reimplementation (just a vector
374
    // of announcements) where possible, and tested for desired properties where not possible.
375
376
    //
377
    // 1. Setup.
378
    //
379
380
    /** The total number of transactions this simulation uses (not all of which will necessarily
381
     *  be present in the orphanage at once). */
382
0
    static constexpr unsigned NUM_TX = 16;
383
    /** The number of peers this simulation uses (not all of which will necessarily be present in
384
     *  the orphanage at once). */
385
0
    static constexpr unsigned NUM_PEERS = 16;
386
    /** The maximum number of announcements this simulation uses (which may be higher than the
387
     *  number permitted inside the orphanage). */
388
0
    static constexpr unsigned MAX_ANN = 64;
389
390
0
    FuzzedDataProvider provider(buffer.data(), buffer.size());
391
    /** Local RNG. Only used for topology/sizes of the transaction set, the order of transactions
392
     *  in EraseForBlock, and for the randomized passed to AddChildrenToWorkSet. */
393
0
    InsecureRandomContext rng(provider.ConsumeIntegral<uint64_t>());
394
395
    //
396
    // 2. Construct an interesting set of 16 transactions.
397
    //
398
399
    // - Pick a topological order among the transactions.
400
0
    std::vector<unsigned> txorder(NUM_TX);
401
0
    std::iota(txorder.begin(), txorder.end(), unsigned{0});
402
0
    std::shuffle(txorder.begin(), txorder.end(), rng);
403
    // - Pick a set of dependencies (pair<child_index, parent_index>).
404
0
    std::vector<std::pair<unsigned, unsigned>> deps;
405
0
    deps.reserve((NUM_TX * (NUM_TX - 1)) / 2);
406
0
    for (unsigned p = 0; p < NUM_TX - 1; ++p) {
407
0
        for (unsigned c = p + 1; c < NUM_TX; ++c) {
408
0
            deps.emplace_back(c, p);
409
0
        }
410
0
    }
411
0
    std::shuffle(deps.begin(), deps.end(), rng);
412
0
    deps.resize(provider.ConsumeIntegralInRange<unsigned>(0, NUM_TX * 4 - 1));
413
    // - Construct the actual transactions.
414
0
    std::set<Wtxid> wtxids;
415
0
    std::vector<CTransactionRef> txn(NUM_TX);
416
0
    node::TxOrphanage::Usage total_usage{0};
417
0
    for (unsigned t = 0; t < NUM_TX; ++t) {
418
0
        CMutableTransaction tx;
419
0
        if (t > 0 && rng.randrange(4) == 0) {
420
            // Occasionally duplicate the previous transaction, so that repetitions of the same
421
            // txid are possible (with different wtxid).
422
0
            tx = CMutableTransaction(*txn[txorder[t - 1]]);
423
0
        } else {
424
0
            tx.version = 1;
425
0
            tx.nLockTime = 0xffffffff;
426
            // Construct 1 to 16 outputs.
427
0
            auto num_outputs = rng.randrange<unsigned>(1 << rng.randrange<unsigned>(5)) + 1;
428
0
            for (unsigned output = 0; output < num_outputs; ++output) {
429
0
                CScript scriptpubkey;
430
0
                scriptpubkey.resize(provider.ConsumeIntegralInRange<unsigned>(20, 34));
431
0
                tx.vout.emplace_back(CAmount{0}, std::move(scriptpubkey));
432
0
            }
433
            // Construct inputs (one for each dependency).
434
0
            for (auto& [child, parent] : deps) {
435
0
                if (child == t) {
436
0
                    auto& partx = txn[txorder[parent]];
437
0
                    assert(partx->version == 1);
438
0
                    COutPoint outpoint(partx->GetHash(), rng.randrange<size_t>(partx->vout.size()));
439
0
                    tx.vin.emplace_back(outpoint);
440
0
                    tx.vin.back().scriptSig.resize(provider.ConsumeIntegralInRange<unsigned>(16, 200));
441
0
                }
442
0
            }
443
            // Construct fallback input in case there are no dependencies.
444
0
            if (tx.vin.empty()) {
445
0
                COutPoint outpoint(Txid::FromUint256(rng.rand256()), rng.randrange<size_t>(16));
446
0
                tx.vin.emplace_back(outpoint);
447
0
                tx.vin.back().scriptSig.resize(provider.ConsumeIntegralInRange<unsigned>(16, 200));
448
0
            }
449
0
        }
450
        // Optionally modify the witness (allowing wtxid != txid), and certainly when the wtxid
451
        // already exists.
452
0
        while (wtxids.contains(CTransaction(tx).GetWitnessHash()) || rng.randrange(4) == 0) {
453
0
            auto& input = tx.vin[rng.randrange(tx.vin.size())];
454
0
            if (rng.randbool()) {
455
0
                input.scriptWitness.stack.resize(1);
456
0
                input.scriptWitness.stack[0].resize(rng.randrange(100));
457
0
            } else {
458
0
                input.scriptWitness.stack.resize(0);
459
0
            }
460
0
        }
461
        // Convert to CTransactionRef.
462
0
        txn[txorder[t]] = MakeTransactionRef(std::move(tx));
463
0
        wtxids.insert(txn[txorder[t]]->GetWitnessHash());
464
0
        auto weight = GetTransactionWeight(*txn[txorder[t]]);
465
0
        assert(weight < MAX_STANDARD_TX_WEIGHT);
466
0
        total_usage += GetTransactionWeight(*txn[txorder[t]]);
467
0
    }
468
469
    //
470
    // 3. Initialize real orphanage
471
    //
472
473
0
    auto max_global_latency_score = provider.ConsumeIntegralInRange<node::TxOrphanage::Count>(NUM_PEERS, MAX_ANN);
474
0
    auto reserved_peer_usage = provider.ConsumeIntegralInRange<node::TxOrphanage::Usage>(1, total_usage);
475
0
    auto real = node::MakeTxOrphanage(max_global_latency_score, reserved_peer_usage);
476
477
    //
478
    // 4. Functions and data structures for the simulation.
479
    //
480
481
    /** Data structure representing one announcement (pair of (tx, peer), plus whether it's
482
     *  reconsiderable or not. */
483
0
    struct SimAnnouncement
484
0
    {
485
0
        unsigned tx;
486
0
        NodeId announcer;
487
0
        bool reconsider{false};
488
0
        SimAnnouncement(unsigned tx_in, NodeId announcer_in, bool reconsider_in) noexcept :
489
0
            tx(tx_in), announcer(announcer_in), reconsider(reconsider_in) {}
490
0
    };
491
    /** The entire simulated orphanage is represented by this list of announcements, in
492
     *  announcement order (unlike TxOrphanageImpl which uses a sequence number to represent
493
     *  announcement order). New announcements are added to the back. */
494
0
    std::vector<SimAnnouncement> sim_announcements;
495
496
    /** Consume a transaction (index into txn) from provider. */
497
0
    auto read_tx_fn = [&]() -> unsigned { return provider.ConsumeIntegralInRange<unsigned>(0, NUM_TX - 1); };
498
    /** Consume a NodeId from provider. */
499
0
    auto read_peer_fn = [&]() -> NodeId { return provider.ConsumeIntegralInRange<unsigned>(0, NUM_PEERS - 1); };
500
    /** Consume both a transaction (index into txn) and a NodeId from provider. */
501
0
    auto read_tx_peer_fn = [&]() -> std::pair<unsigned, NodeId> {
502
0
        auto code = provider.ConsumeIntegralInRange<unsigned>(0, NUM_TX * NUM_PEERS - 1);
503
0
        return {code % NUM_TX, code / NUM_TX};
504
0
    };
505
    /** Determine if we have any announcements of the given transaction in the simulation. */
506
0
    auto have_tx_fn = [&](unsigned tx) -> bool {
507
0
        for (auto& ann : sim_announcements) {
508
0
            if (ann.tx == tx) return true;
509
0
        }
510
0
        return false;
511
0
    };
512
    /** Count the number of peers in the simulation. */
513
0
    auto count_peers_fn = [&]() -> unsigned {
514
0
        std::bitset<NUM_PEERS> mask;
515
0
        for (auto& ann : sim_announcements) {
516
0
            mask.set(ann.announcer);
517
0
        }
518
0
        return mask.count();
519
0
    };
520
    /** Determine if we have any reconsiderable announcements of a given transaction. */
521
0
    auto have_reconsiderable_fn = [&](unsigned tx) -> bool {
522
0
        for (auto& ann : sim_announcements) {
523
0
            if (ann.reconsider && ann.tx == tx) return true;
524
0
        }
525
0
        return false;
526
0
    };
527
    /** Determine if a peer has any transactions to reconsider. */
528
0
    auto have_reconsider_fn = [&](NodeId peer) -> bool {
529
0
        for (auto& ann : sim_announcements) {
530
0
            if (ann.reconsider && ann.announcer == peer) return true;
531
0
        }
532
0
        return false;
533
0
    };
534
    /** Get an iterator to an existing (wtxid, peer) pair in the simulation. */
535
0
    auto find_announce_wtxid_fn = [&](const Wtxid& wtxid, NodeId peer) -> std::vector<SimAnnouncement>::iterator {
536
0
        for (auto it = sim_announcements.begin(); it != sim_announcements.end(); ++it) {
537
0
            if (txn[it->tx]->GetWitnessHash() == wtxid && it->announcer == peer) return it;
538
0
        }
539
0
        return sim_announcements.end();
540
0
    };
541
    /** Get an iterator to an existing (tx, peer) pair in the simulation. */
542
0
    auto find_announce_fn = [&](unsigned tx, NodeId peer) {
543
0
        for (auto it = sim_announcements.begin(); it != sim_announcements.end(); ++it) {
544
0
            if (it->tx == tx && it->announcer == peer) return it;
545
0
        }
546
0
        return sim_announcements.end();
547
0
    };
548
    /** Compute a peer's DoS score according to simulation data. */
549
0
    auto dos_score_fn = [&](NodeId peer, int32_t max_count, int32_t max_usage) -> FeeFrac {
550
0
        int64_t count{0};
551
0
        int64_t usage{0};
552
0
        for (auto& ann : sim_announcements) {
553
0
            if (ann.announcer != peer) continue;
554
0
            count += 1 + (txn[ann.tx]->vin.size() / 10);
555
0
            usage += GetTransactionWeight(*txn[ann.tx]);
556
0
        }
557
0
        return std::max(FeeFrac{count, max_count}, FeeFrac{usage, max_usage});
558
0
    };
559
560
    //
561
    // 5. Run through a scenario of mutators on both real and simulated orphanage.
562
    //
563
564
0
    LIMITED_WHILE(provider.remaining_bytes() > 0, 200) {
565
0
        int command = provider.ConsumeIntegralInRange<uint8_t>(0, 15);
566
0
        while (true) {
567
0
            if (sim_announcements.size() < MAX_ANN && command-- == 0) {
568
                // AddTx
569
0
                auto [tx, peer] = read_tx_peer_fn();
570
0
                bool added = real->AddTx(txn[tx], peer);
571
0
                bool sim_have_tx = have_tx_fn(tx);
572
0
                assert(added == !sim_have_tx);
573
0
                if (find_announce_fn(tx, peer) == sim_announcements.end()) {
574
0
                    sim_announcements.emplace_back(tx, peer, false);
575
0
                }
576
0
                break;
577
0
            } else if (sim_announcements.size() < MAX_ANN && command-- == 0) {
578
                // AddAnnouncer
579
0
                auto [tx, peer] = read_tx_peer_fn();
580
0
                bool added = real->AddAnnouncer(txn[tx]->GetWitnessHash(), peer);
581
0
                bool sim_have_tx = have_tx_fn(tx);
582
0
                auto sim_it = find_announce_fn(tx, peer);
583
0
                assert(added == (sim_it == sim_announcements.end() && sim_have_tx));
584
0
                if (added) {
585
0
                    sim_announcements.emplace_back(tx, peer, false);
586
0
                }
587
0
                break;
588
0
            } else if (command-- == 0) {
589
                // EraseTx
590
0
                auto tx = read_tx_fn();
591
0
                bool erased = real->EraseTx(txn[tx]->GetWitnessHash());
592
0
                bool sim_have = have_tx_fn(tx);
593
0
                assert(erased == sim_have);
594
0
                std::erase_if(sim_announcements, [&](auto& ann) { return ann.tx == tx; });
595
0
                break;
596
0
           } else if (command-- == 0) {
597
                // EraseForPeer
598
0
                auto peer = read_peer_fn();
599
0
                real->EraseForPeer(peer);
600
0
                std::erase_if(sim_announcements, [&](auto& ann) { return ann.announcer == peer; });
601
0
                break;
602
0
            } else if (command-- == 0) {
603
                // EraseForBlock
604
0
                auto pattern = provider.ConsumeIntegralInRange<uint64_t>(0, (uint64_t{1} << NUM_TX) - 1);
605
0
                CBlock block;
606
0
                std::set<COutPoint> spent;
607
0
                for (unsigned tx = 0; tx < NUM_TX; ++tx) {
608
0
                    if ((pattern >> tx) & 1) {
609
0
                        block.vtx.emplace_back(txn[tx]);
610
0
                        for (auto& txin : block.vtx.back()->vin) {
611
0
                            spent.insert(txin.prevout);
612
0
                        }
613
0
                    }
614
0
                }
615
0
                std::shuffle(block.vtx.begin(), block.vtx.end(), rng);
616
0
                real->EraseForBlock(block);
617
0
                std::erase_if(sim_announcements, [&](auto& ann) {
618
0
                    for (auto& txin : txn[ann.tx]->vin) {
619
0
                        if (spent.count(txin.prevout)) return true;
620
0
                    }
621
0
                    return false;
622
0
                });
623
0
                break;
624
0
            } else if (command-- == 0) {
625
                // AddChildrenToWorkSet
626
0
                auto tx = read_tx_fn();
627
0
                FastRandomContext rand_ctx(rng.rand256());
628
0
                auto added = real->AddChildrenToWorkSet(*txn[tx], rand_ctx);
629
                /** Set of not-already-reconsiderable child wtxids. */
630
0
                std::set<Wtxid> child_wtxids;
631
0
                for (unsigned child_tx = 0; child_tx < NUM_TX; ++child_tx) {
632
0
                    if (!have_tx_fn(child_tx)) continue;
633
0
                    if (have_reconsiderable_fn(child_tx)) continue;
634
0
                    bool child_of = false;
635
0
                    for (auto& txin : txn[child_tx]->vin) {
636
0
                        if (txin.prevout.hash == txn[tx]->GetHash()) {
637
0
                            child_of = true;
638
0
                            break;
639
0
                        }
640
0
                    }
641
0
                    if (child_of) {
642
0
                        child_wtxids.insert(txn[child_tx]->GetWitnessHash());
643
0
                    }
644
0
                }
645
0
                for (auto& [wtxid, peer] : added) {
646
                    // Wtxid must be a child of tx that is not yet reconsiderable.
647
0
                    auto child_wtxid_it = child_wtxids.find(wtxid);
648
0
                    assert(child_wtxid_it != child_wtxids.end());
649
                    // Announcement must exist.
650
0
                    auto sim_ann_it = find_announce_wtxid_fn(wtxid, peer);
651
0
                    assert(sim_ann_it != sim_announcements.end());
652
                    // Announcement must not yet be reconsiderable.
653
0
                    assert(sim_ann_it->reconsider == false);
654
                    // Make reconsiderable.
655
0
                    sim_ann_it->reconsider = true;
656
                    // Remove from child_wtxids map, to disallow it being reported a second time in added.
657
0
                    child_wtxids.erase(wtxid);
658
0
                }
659
                // Verify that AddChildrenToWorkSet does not select announcements that were already reconsiderable:
660
                // Check all child wtxids which did not occur at least once in the result were already reconsiderable
661
                // due to a previous AddChildrenToWorkSet.
662
0
                assert(child_wtxids.empty());
663
0
                break;
664
0
            } else if (command-- == 0) {
665
                // GetTxToReconsider.
666
0
                auto peer = read_peer_fn();
667
0
                auto result = real->GetTxToReconsider(peer);
668
0
                if (result) {
669
                    // A transaction was found. It must have a corresponding reconsiderable
670
                    // announcement from peer.
671
0
                    auto sim_ann_it = find_announce_wtxid_fn(result->GetWitnessHash(), peer);
672
0
                    assert(sim_ann_it != sim_announcements.end());
673
0
                    assert(sim_ann_it->announcer == peer);
674
0
                    assert(sim_ann_it->reconsider);
675
                    // Make it non-reconsiderable.
676
0
                    sim_ann_it->reconsider = false;
677
0
                } else {
678
                    // No reconsiderable transaction was found from peer. Verify that it does not
679
                    // have any.
680
0
                    assert(!have_reconsider_fn(peer));
681
0
                }
682
0
                break;
683
0
            }
684
0
        }
685
        // Always trim after each command if needed.
686
0
        const auto max_ann = max_global_latency_score / std::max<unsigned>(1, count_peers_fn());
687
0
        const auto max_mem = reserved_peer_usage;
688
0
        while (true) {
689
            // Count global usage and number of peers.
690
0
            node::TxOrphanage::Usage total_usage{0};
691
0
            node::TxOrphanage::Count total_latency_score = sim_announcements.size();
692
0
            for (unsigned tx = 0; tx < NUM_TX; ++tx) {
693
0
                if (have_tx_fn(tx)) {
694
0
                    total_usage += GetTransactionWeight(*txn[tx]);
695
0
                    total_latency_score += txn[tx]->vin.size() / 10;
696
0
                }
697
0
            }
698
0
            auto num_peers = count_peers_fn();
699
0
            bool oversized = (total_usage > reserved_peer_usage * num_peers) ||
700
0
                                (total_latency_score > real->MaxGlobalLatencyScore());
701
0
            if (!oversized) break;
702
            // Find worst peer.
703
0
            FeeFrac worst_dos_score{0, 1};
704
0
            unsigned worst_peer = unsigned(-1);
705
0
            for (unsigned peer = 0; peer < NUM_PEERS; ++peer) {
706
0
                auto dos_score = dos_score_fn(peer, max_ann, max_mem);
707
                // Use >= so that the more recent peer (higher NodeId) wins in case of
708
                // ties.
709
0
                if (dos_score >= worst_dos_score) {
710
0
                    worst_dos_score = dos_score;
711
0
                    worst_peer = peer;
712
0
                }
713
0
            }
714
0
            assert(worst_peer != unsigned(-1));
715
0
            assert(worst_dos_score >> FeeFrac(1, 1));
716
            // Find oldest announcement from worst_peer, preferring non-reconsiderable ones.
717
0
            bool done{false};
718
0
            for (int reconsider = 0; reconsider < 2; ++reconsider) {
719
0
                for (auto it = sim_announcements.begin(); it != sim_announcements.end(); ++it) {
720
0
                    if (it->announcer != worst_peer || it->reconsider != reconsider) continue;
721
0
                    sim_announcements.erase(it);
722
0
                    done = true;
723
0
                    break;
724
0
                }
725
0
                if (done) break;
726
0
            }
727
0
            assert(done);
728
0
        }
729
        // We must now be within limits, otherwise LimitOrphans should have continued further.
730
        // We don't check the contents of the orphanage until the end to make fuzz runs faster.
731
0
        assert(real->TotalLatencyScore() <= real->MaxGlobalLatencyScore());
732
0
        assert(real->TotalOrphanUsage() <= real->MaxGlobalUsage());
733
0
    }
734
735
    //
736
    // 6. Perform a full comparison between the real orphanage's inspectors and the simulation.
737
    //
738
739
0
    real->SanityCheck();
740
741
742
0
    auto all_orphans = real->GetOrphanTransactions();
743
0
    node::TxOrphanage::Usage orphan_usage{0};
744
0
    std::vector<node::TxOrphanage::Usage> usage_by_peer(NUM_PEERS);
745
0
    node::TxOrphanage::Count unique_orphans{0};
746
0
    std::vector<node::TxOrphanage::Count> count_by_peer(NUM_PEERS);
747
0
    node::TxOrphanage::Count total_latency_score = sim_announcements.size();
748
0
    for (unsigned tx = 0; tx < NUM_TX; ++tx) {
749
0
        bool sim_have_tx = have_tx_fn(tx);
750
0
        if (sim_have_tx) {
751
0
            orphan_usage += GetTransactionWeight(*txn[tx]);
752
0
            total_latency_score += txn[tx]->vin.size() / 10;
753
0
        }
754
0
        unique_orphans += sim_have_tx;
755
0
        auto orphans_it = std::find_if(all_orphans.begin(), all_orphans.end(), [&](auto& orph) { return orph.tx->GetWitnessHash() == txn[tx]->GetWitnessHash(); });
756
        // GetOrphanTransactions (OrphanBase existence)
757
0
        assert((orphans_it != all_orphans.end()) == sim_have_tx);
758
        // HaveTx
759
0
        bool have_tx = real->HaveTx(txn[tx]->GetWitnessHash());
760
0
        assert(have_tx == sim_have_tx);
761
        // GetTx
762
0
        auto txref = real->GetTx(txn[tx]->GetWitnessHash());
763
0
        assert(!!txref == sim_have_tx);
764
0
        if (sim_have_tx) assert(txref->GetWitnessHash() == txn[tx]->GetWitnessHash());
765
766
0
        for (NodeId peer = 0; peer < NUM_PEERS; ++peer) {
767
0
            auto it_sim_ann = find_announce_fn(tx, peer);
768
0
            bool sim_have_ann = it_sim_ann != sim_announcements.end();
769
0
            if (sim_have_ann) usage_by_peer[peer] += GetTransactionWeight(*txn[tx]);
770
0
            count_by_peer[peer] += sim_have_ann;
771
            // GetOrphanTransactions (announcers presence)
772
0
            if (sim_have_ann) assert(sim_have_tx);
773
0
            if (sim_have_tx) assert(orphans_it->announcers.count(peer) == sim_have_ann);
774
            // HaveTxFromPeer
775
0
            bool have_ann = real->HaveTxFromPeer(txn[tx]->GetWitnessHash(), peer);
776
0
            assert(sim_have_ann == have_ann);
777
            // GetChildrenFromSamePeer
778
0
            auto children_from_peer = real->GetChildrenFromSamePeer(txn[tx], peer);
779
0
            auto it = children_from_peer.rbegin();
780
0
            for (int phase = 0; phase < 2; ++phase) {
781
                // First expect all children which have reconsiderable announcement from peer, then the others.
782
0
                for (auto& ann : sim_announcements) {
783
0
                    if (ann.announcer != peer) continue;
784
0
                    if (ann.reconsider != (phase == 1)) continue;
785
0
                    bool matching_parent{false};
786
0
                    for (const auto& vin : txn[ann.tx]->vin) {
787
0
                        if (vin.prevout.hash == txn[tx]->GetHash()) matching_parent = true;
788
0
                    }
789
0
                    if (!matching_parent) continue;
790
                    // Found an announcement from peer which is a child of txn[tx].
791
0
                    assert(it != children_from_peer.rend());
792
0
                    assert((*it)->GetWitnessHash() == txn[ann.tx]->GetWitnessHash());
793
0
                    ++it;
794
0
                }
795
0
            }
796
0
            assert(it == children_from_peer.rend());
797
0
        }
798
0
    }
799
    // TotalOrphanUsage
800
0
    assert(orphan_usage == real->TotalOrphanUsage());
801
0
    for (NodeId peer = 0; peer < NUM_PEERS; ++peer) {
802
0
        bool sim_have_reconsider = have_reconsider_fn(peer);
803
        // HaveTxToReconsider
804
0
        bool have_reconsider = real->HaveTxToReconsider(peer);
805
0
        assert(have_reconsider == sim_have_reconsider);
806
        // UsageByPeer
807
0
        assert(usage_by_peer[peer] == real->UsageByPeer(peer));
808
        // AnnouncementsFromPeer
809
0
        assert(count_by_peer[peer] == real->AnnouncementsFromPeer(peer));
810
0
    }
811
    // CountAnnouncements
812
0
    assert(sim_announcements.size() == real->CountAnnouncements());
813
    // CountUniqueOrphans
814
0
    assert(unique_orphans == real->CountUniqueOrphans());
815
    // MaxGlobalLatencyScore
816
0
    assert(max_global_latency_score == real->MaxGlobalLatencyScore());
817
    // ReservedPeerUsage
818
0
    assert(reserved_peer_usage == real->ReservedPeerUsage());
819
    // MaxPeerLatencyScore
820
0
    auto present_peers = count_peers_fn();
821
0
    assert(max_global_latency_score / std::max<unsigned>(1, present_peers) == real->MaxPeerLatencyScore());
822
    // MaxGlobalUsage
823
0
    assert(reserved_peer_usage * std::max<unsigned>(1, present_peers) == real->MaxGlobalUsage());
824
    // TotalLatencyScore.
825
0
    assert(real->TotalLatencyScore() == total_latency_score);
826
0
}