Coverage Report

Created: 2025-04-14 16:24

/Users/mcomp/contrib/bitcoin/src/test/fuzz/txgraph.cpp
Line
Count
Source (jump to first uncovered line)
1
// Copyright (c) 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 <txgraph.h>
6
#include <cluster_linearize.h>
7
#include <test/fuzz/fuzz.h>
8
#include <test/fuzz/FuzzedDataProvider.h>
9
#include <test/util/random.h>
10
#include <util/bitset.h>
11
#include <util/feefrac.h>
12
13
#include <algorithm>
14
#include <map>
15
#include <memory>
16
#include <set>
17
#include <stdint.h>
18
#include <utility>
19
20
using namespace cluster_linearize;
21
22
namespace {
23
24
/** Data type representing a naive simulated TxGraph, keeping all transactions (even from
25
 *  disconnected components) in a single DepGraph. Unlike the real TxGraph, this only models
26
 *  a single graph, and multiple instances are used to simulate main/staging. */
27
struct SimTxGraph
28
{
29
    /** Maximum number of transactions to support simultaneously. Set this higher than txgraph's
30
     *  cluster count, so we can exercise situations with more transactions than fit in one
31
     *  cluster. */
32
    static constexpr unsigned MAX_TRANSACTIONS = MAX_CLUSTER_COUNT_LIMIT * 2;
33
    /** Set type to use in the simulation. */
34
    using SetType = BitSet<MAX_TRANSACTIONS>;
35
    /** Data type for representing positions within SimTxGraph::graph. */
36
    using Pos = DepGraphIndex;
37
    /** Constant to mean "missing in this graph". */
38
    static constexpr auto MISSING = Pos(-1);
39
40
    /** The dependency graph (for all transactions in the simulation, regardless of
41
     *  connectivity/clustering). */
42
    DepGraph<SetType> graph;
43
    /** For each position in graph, which TxGraph::Ref it corresponds with (if any). Use shared_ptr
44
     *  so that a SimTxGraph can be copied to create a staging one, while sharing Refs with
45
     *  the main graph. */
46
    std::array<std::shared_ptr<TxGraph::Ref>, MAX_TRANSACTIONS> simmap;
47
    /** For each TxGraph::Ref in graph, the position it corresponds with. */
48
    std::map<const TxGraph::Ref*, Pos> simrevmap;
49
    /** The set of TxGraph::Ref entries that have been removed, but not yet destroyed. */
50
    std::vector<std::shared_ptr<TxGraph::Ref>> removed;
51
    /** Whether the graph is oversized (true = yes, false = no, std::nullopt = unknown). */
52
    std::optional<bool> oversized;
53
    /** The configured maximum number of transactions per cluster. */
54
    DepGraphIndex max_cluster_count;
55
56
    /** Construct a new SimTxGraph with the specified maximum cluster count. */
57
0
    explicit SimTxGraph(DepGraphIndex max_cluster) : max_cluster_count(max_cluster) {}
58
59
    // Permit copying and moving.
60
0
    SimTxGraph(const SimTxGraph&) noexcept = default;
61
    SimTxGraph& operator=(const SimTxGraph&) noexcept = default;
62
0
    SimTxGraph(SimTxGraph&&) noexcept = default;
63
0
    SimTxGraph& operator=(SimTxGraph&&) noexcept = default;
64
65
    /** Check whether this graph is oversized (contains a connected component whose number of
66
     *  transactions exceeds max_cluster_count. */
67
    bool IsOversized()
68
0
    {
69
0
        if (!oversized.has_value()) {
70
            // Only recompute when oversized isn't already known.
71
0
            oversized = false;
72
0
            auto todo = graph.Positions();
73
            // Iterate over all connected components of the graph.
74
0
            while (todo.Any()) {
75
0
                auto component = graph.FindConnectedComponent(todo);
76
0
                if (component.Count() > max_cluster_count) oversized = true;
77
0
                todo -= component;
78
0
            }
79
0
        }
80
0
        return *oversized;
81
0
    }
82
83
    /** Determine the number of (non-removed) transactions in the graph. */
84
0
    DepGraphIndex GetTransactionCount() const { return graph.TxCount(); }
85
86
    /** Get the position where ref occurs in this simulated graph, or -1 if it does not. */
87
    Pos Find(const TxGraph::Ref* ref) const
88
0
    {
89
0
        auto it = simrevmap.find(ref);
90
0
        if (it != simrevmap.end()) return it->second;
91
0
        return MISSING;
92
0
    }
93
94
    /** Given a position in this simulated graph, get the corresponding TxGraph::Ref. */
95
    TxGraph::Ref* GetRef(Pos pos)
96
0
    {
97
0
        assert(graph.Positions()[pos]);
98
0
        assert(simmap[pos]);
99
0
        return simmap[pos].get();
100
0
    }
101
102
    /** Add a new transaction to the simulation. */
103
    TxGraph::Ref* AddTransaction(const FeePerWeight& feerate)
104
0
    {
105
0
        assert(graph.TxCount() < MAX_TRANSACTIONS);
106
0
        auto simpos = graph.AddTransaction(feerate);
107
0
        assert(graph.Positions()[simpos]);
108
0
        simmap[simpos] = std::make_shared<TxGraph::Ref>();
109
0
        auto ptr = simmap[simpos].get();
110
0
        simrevmap[ptr] = simpos;
111
0
        return ptr;
112
0
    }
113
114
    /** Add a dependency between two positions in this graph. */
115
    void AddDependency(TxGraph::Ref* parent, TxGraph::Ref* child)
116
0
    {
117
0
        auto par_pos = Find(parent);
118
0
        if (par_pos == MISSING) return;
119
0
        auto chl_pos = Find(child);
120
0
        if (chl_pos == MISSING) return;
121
0
        graph.AddDependencies(SetType::Singleton(par_pos), chl_pos);
122
        // This may invalidate our cached oversized value.
123
0
        if (oversized.has_value() && !*oversized) oversized = std::nullopt;
124
0
    }
125
126
    /** Modify the transaction fee of a ref, if it exists. */
127
    void SetTransactionFee(TxGraph::Ref* ref, int64_t fee)
128
0
    {
129
0
        auto pos = Find(ref);
130
0
        if (pos == MISSING) return;
131
0
        graph.FeeRate(pos).fee = fee;
132
0
    }
133
134
    /** Remove the transaction in the specified position from the graph. */
135
    void RemoveTransaction(TxGraph::Ref* ref)
136
0
    {
137
0
        auto pos = Find(ref);
138
0
        if (pos == MISSING) return;
139
0
        graph.RemoveTransactions(SetType::Singleton(pos));
140
0
        simrevmap.erase(simmap[pos].get());
141
        // Retain the TxGraph::Ref corresponding to this position, so the Ref destruction isn't
142
        // invoked until the simulation explicitly decided to do so.
143
0
        removed.push_back(std::move(simmap[pos]));
144
0
        simmap[pos].reset();
145
        // This may invalidate our cached oversized value.
146
0
        if (oversized.has_value() && *oversized) oversized = std::nullopt;
147
0
    }
148
149
    /** Destroy the transaction from the graph, including from the removed set. This will
150
     *  trigger TxGraph::Ref::~Ref. reset_oversize controls whether the cached oversized
151
     *  value is cleared (destroying does not clear oversizedness in TxGraph of the main
152
     *  graph while staging exists). */
153
    void DestroyTransaction(TxGraph::Ref* ref, bool reset_oversize)
154
0
    {
155
0
        auto pos = Find(ref);
156
0
        if (pos == MISSING) {
157
            // Wipe the ref, if it exists, from the removed vector. Use std::partition rather
158
            // than std::erase because we don't care about the order of the entries that
159
            // remain.
160
0
            auto remove = std::partition(removed.begin(), removed.end(), [&](auto& arg) { return arg.get() != ref; });
161
0
            removed.erase(remove, removed.end());
162
0
        } else {
163
0
            graph.RemoveTransactions(SetType::Singleton(pos));
164
0
            simrevmap.erase(simmap[pos].get());
165
0
            simmap[pos].reset();
166
            // This may invalidate our cached oversized value.
167
0
            if (reset_oversize && oversized.has_value() && *oversized) {
168
0
                oversized = std::nullopt;
169
0
            }
170
0
        }
171
0
    }
172
173
    /** Construct the set with all positions in this graph corresponding to the specified
174
     *  TxGraph::Refs. All of them must occur in this graph and not be removed. */
175
    SetType MakeSet(std::span<TxGraph::Ref* const> arg)
176
0
    {
177
0
        SetType ret;
178
0
        for (TxGraph::Ref* ptr : arg) {
179
0
            auto pos = Find(ptr);
180
0
            assert(pos != Pos(-1));
181
0
            ret.Set(pos);
182
0
        }
183
0
        return ret;
184
0
    }
185
186
    /** Get the set of ancestors (desc=false) or descendants (desc=true) in this graph. */
187
    SetType GetAncDesc(TxGraph::Ref* arg, bool desc)
188
0
    {
189
0
        auto pos = Find(arg);
190
0
        if (pos == MISSING) return {};
191
0
        return desc ? graph.Descendants(pos) : graph.Ancestors(pos);
192
0
    }
193
194
    /** Given a set of Refs (given as a vector of pointers), expand the set to include all its
195
     *  ancestors (desc=false) or all its descendants (desc=true) in this graph. */
196
    void IncludeAncDesc(std::vector<TxGraph::Ref*>& arg, bool desc)
197
0
    {
198
0
        std::vector<TxGraph::Ref*> ret;
199
0
        for (auto ptr : arg) {
200
0
            auto simpos = Find(ptr);
201
0
            if (simpos != MISSING) {
202
0
                for (auto i : desc ? graph.Descendants(simpos) : graph.Ancestors(simpos)) {
203
0
                    ret.push_back(simmap[i].get());
204
0
                }
205
0
            } else {
206
0
                ret.push_back(ptr);
207
0
            }
208
0
        }
209
        // Deduplicate.
210
0
        std::sort(ret.begin(), ret.end());
211
0
        ret.erase(std::unique(ret.begin(), ret.end()), ret.end());
212
        // Replace input.
213
0
        arg = std::move(ret);
214
0
    }
215
};
216
217
} // namespace
218
219
FUZZ_TARGET(txgraph)
220
0
{
221
    // This is a big simulation test for TxGraph, which performs a fuzz-derived sequence of valid
222
    // operations on a TxGraph instance, as well as on a simpler (mostly) reimplementation (see
223
    // SimTxGraph above), comparing the outcome of functions that return a result, and finally
224
    // performing a full comparison between the two.
225
226
0
    SeedRandomStateForTest(SeedRand::ZEROS);
227
0
    FuzzedDataProvider provider(buffer.data(), buffer.size());
228
229
    /** Internal test RNG, used only for decisions which would require significant amount of data
230
     *  to be read from the provider, without realistically impacting test sensitivity. */
231
0
    InsecureRandomContext rng(0xdecade2009added + buffer.size());
232
233
    /** Variable used whenever an empty TxGraph::Ref is needed. */
234
0
    TxGraph::Ref empty_ref;
235
236
    // Decide the maximum number of transactions per cluster we will use in this simulation.
237
0
    auto max_count = provider.ConsumeIntegralInRange<DepGraphIndex>(1, MAX_CLUSTER_COUNT_LIMIT);
238
239
    // Construct a real graph, and a vector of simulated graphs (main, and possibly staging).
240
0
    auto real = MakeTxGraph(max_count);
241
0
    std::vector<SimTxGraph> sims;
242
0
    sims.reserve(2);
243
0
    sims.emplace_back(max_count);
244
245
    /** Function to pick any Ref (for either sim in sims: from sim.simmap or sim.removed, or the
246
     *  empty Ref). */
247
0
    auto pick_fn = [&]() noexcept -> TxGraph::Ref* {
248
0
        size_t tx_count[2] = {sims[0].GetTransactionCount(), 0};
249
        /** The number of possible choices. */
250
0
        size_t choices = tx_count[0] + sims[0].removed.size() + 1;
251
0
        if (sims.size() == 2) {
252
0
            tx_count[1] = sims[1].GetTransactionCount();
253
0
            choices += tx_count[1] + sims[1].removed.size();
254
0
        }
255
        /** Pick one of them. */
256
0
        auto choice = provider.ConsumeIntegralInRange<size_t>(0, choices - 1);
257
        // Consider both main and (if it exists) staging.
258
0
        for (size_t level = 0; level < sims.size(); ++level) {
259
0
            auto& sim = sims[level];
260
0
            if (choice < tx_count[level]) {
261
                // Return from graph.
262
0
                for (auto i : sim.graph.Positions()) {
263
0
                    if (choice == 0) return sim.GetRef(i);
264
0
                    --choice;
265
0
                }
266
0
                assert(false);
267
0
            } else {
268
0
                choice -= tx_count[level];
269
0
            }
270
0
            if (choice < sim.removed.size()) {
271
                // Return from removed.
272
0
                return sim.removed[choice].get();
273
0
            } else {
274
0
                choice -= sim.removed.size();
275
0
            }
276
0
        }
277
        // Return empty.
278
0
        assert(choice == 0);
279
0
        return &empty_ref;
280
0
    };
281
282
0
    LIMITED_WHILE(provider.remaining_bytes() > 0, 200) {
283
        // Read a one-byte command.
284
0
        int command = provider.ConsumeIntegral<uint8_t>();
285
        // Treat the lowest bit of a command as a flag (which selects a variant of some of the
286
        // operations), and the second-lowest bit as a way of selecting main vs. staging, and leave
287
        // the rest of the bits in command.
288
0
        bool alt = command & 1;
289
0
        bool use_main = command & 2;
290
0
        command >>= 2;
291
292
        // Provide convenient aliases for the top simulated graph (main, or staging if it exists),
293
        // one for the simulated graph selected based on use_main (for operations that can operate
294
        // on both graphs), and one that always refers to the main graph.
295
0
        auto& top_sim = sims.back();
296
0
        auto& sel_sim = use_main ? sims[0] : top_sim;
297
0
        auto& main_sim = sims[0];
298
299
        // Keep decrementing command for each applicable operation, until one is hit. Multiple
300
        // iterations may be necessary.
301
0
        while (true) {
302
0
            if (top_sim.GetTransactionCount() < SimTxGraph::MAX_TRANSACTIONS && command-- == 0) {
303
                // AddTransaction.
304
0
                int64_t fee;
305
0
                int32_t size;
306
0
                if (alt) {
307
                    // If alt is true, pick fee and size from the entire range.
308
0
                    fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
309
0
                    size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
310
0
                } else {
311
                    // Otherwise, use smaller range which consume fewer fuzz input bytes, as just
312
                    // these are likely sufficient to trigger all interesting code paths already.
313
0
                    fee = provider.ConsumeIntegral<uint8_t>();
314
0
                    size = provider.ConsumeIntegral<uint8_t>() + 1;
315
0
                }
316
0
                FeePerWeight feerate{fee, size};
317
                // Create a real TxGraph::Ref.
318
0
                auto ref = real->AddTransaction(feerate);
319
                // Create a shared_ptr place in the simulation to put the Ref in.
320
0
                auto ref_loc = top_sim.AddTransaction(feerate);
321
                // Move it in place.
322
0
                *ref_loc = std::move(ref);
323
0
                break;
324
0
            } else if (top_sim.GetTransactionCount() + top_sim.removed.size() > 1 && command-- == 0) {
325
                // AddDependency.
326
0
                auto par = pick_fn();
327
0
                auto chl = pick_fn();
328
0
                auto pos_par = top_sim.Find(par);
329
0
                auto pos_chl = top_sim.Find(chl);
330
0
                if (pos_par != SimTxGraph::MISSING && pos_chl != SimTxGraph::MISSING) {
331
                    // Determine if adding this would introduce a cycle (not allowed by TxGraph),
332
                    // and if so, skip.
333
0
                    if (top_sim.graph.Ancestors(pos_par)[pos_chl]) break;
334
0
                }
335
0
                top_sim.AddDependency(par, chl);
336
0
                real->AddDependency(*par, *chl);
337
0
                break;
338
0
            } else if (top_sim.removed.size() < 100 && command-- == 0) {
339
                // RemoveTransaction. Either all its ancestors or all its descendants are also
340
                // removed (if any), to make sure TxGraph's reordering of removals and dependencies
341
                // has no effect.
342
0
                std::vector<TxGraph::Ref*> to_remove;
343
0
                to_remove.push_back(pick_fn());
344
0
                top_sim.IncludeAncDesc(to_remove, alt);
345
                // The order in which these ancestors/descendants are removed should not matter;
346
                // randomly shuffle them.
347
0
                std::shuffle(to_remove.begin(), to_remove.end(), rng);
348
0
                for (TxGraph::Ref* ptr : to_remove) {
349
0
                    real->RemoveTransaction(*ptr);
350
0
                    top_sim.RemoveTransaction(ptr);
351
0
                }
352
0
                break;
353
0
            } else if (sel_sim.removed.size() > 0 && command-- == 0) {
354
                // ~Ref (of an already-removed transaction). Destroying a TxGraph::Ref has an
355
                // observable effect on the TxGraph it refers to, so this simulation permits doing
356
                // so separately from other actions on TxGraph.
357
358
                // Pick a Ref of sel_sim.removed to destroy. Note that the same Ref may still occur
359
                // in the other graph, and thus not actually trigger ~Ref yet (which is exactly
360
                // what we want, as destroying Refs is only allowed when it does not refer to an
361
                // existing transaction in either graph).
362
0
                auto removed_pos = provider.ConsumeIntegralInRange<size_t>(0, sel_sim.removed.size() - 1);
363
0
                if (removed_pos != sel_sim.removed.size() - 1) {
364
0
                    std::swap(sel_sim.removed[removed_pos], sel_sim.removed.back());
365
0
                }
366
0
                sel_sim.removed.pop_back();
367
0
                break;
368
0
            } else if (command-- == 0) {
369
                // ~Ref (of any transaction).
370
0
                std::vector<TxGraph::Ref*> to_destroy;
371
0
                to_destroy.push_back(pick_fn());
372
0
                while (true) {
373
                    // Keep adding either the ancestors or descendants the already picked
374
                    // transactions have in both graphs (main and staging) combined. Destroying
375
                    // will trigger deletions in both, so to have consistent TxGraph behavior, the
376
                    // set must be closed under ancestors, or descendants, in both graphs.
377
0
                    auto old_size = to_destroy.size();
378
0
                    for (auto& sim : sims) sim.IncludeAncDesc(to_destroy, alt);
379
0
                    if (to_destroy.size() == old_size) break;
380
0
                }
381
                // The order in which these ancestors/descendants are destroyed should not matter;
382
                // randomly shuffle them.
383
0
                std::shuffle(to_destroy.begin(), to_destroy.end(), rng);
384
0
                for (TxGraph::Ref* ptr : to_destroy) {
385
0
                    for (size_t level = 0; level < sims.size(); ++level) {
386
0
                        sims[level].DestroyTransaction(ptr, level == sims.size() - 1);
387
0
                    }
388
0
                }
389
0
                break;
390
0
            } else if (command-- == 0) {
391
                // SetTransactionFee.
392
0
                int64_t fee;
393
0
                if (alt) {
394
0
                    fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
395
0
                } else {
396
0
                    fee = provider.ConsumeIntegral<uint8_t>();
397
0
                }
398
0
                auto ref = pick_fn();
399
0
                real->SetTransactionFee(*ref, fee);
400
0
                for (auto& sim : sims) {
401
0
                    sim.SetTransactionFee(ref, fee);
402
0
                }
403
0
                break;
404
0
            } else if (command-- == 0) {
405
                // GetTransactionCount.
406
0
                assert(real->GetTransactionCount(use_main) == sel_sim.GetTransactionCount());
407
0
                break;
408
0
            } else if (command-- == 0) {
409
                // Exists.
410
0
                auto ref = pick_fn();
411
0
                bool exists = real->Exists(*ref, use_main);
412
0
                bool should_exist = sel_sim.Find(ref) != SimTxGraph::MISSING;
413
0
                assert(exists == should_exist);
414
0
                break;
415
0
            } else if (command-- == 0) {
416
                // IsOversized.
417
0
                assert(sel_sim.IsOversized() == real->IsOversized(use_main));
418
0
                break;
419
0
            } else if (command-- == 0) {
420
                // GetIndividualFeerate.
421
0
                auto ref = pick_fn();
422
0
                auto feerate = real->GetIndividualFeerate(*ref);
423
0
                bool found{false};
424
0
                for (auto& sim : sims) {
425
0
                    auto simpos = sim.Find(ref);
426
0
                    if (simpos != SimTxGraph::MISSING) {
427
0
                        found = true;
428
0
                        assert(feerate == sim.graph.FeeRate(simpos));
429
0
                    }
430
0
                }
431
0
                if (!found) assert(feerate.IsEmpty());
432
0
                break;
433
0
            } else if (!main_sim.IsOversized() && command-- == 0) {
434
                // GetMainChunkFeerate.
435
0
                auto ref = pick_fn();
436
0
                auto feerate = real->GetMainChunkFeerate(*ref);
437
0
                auto simpos = main_sim.Find(ref);
438
0
                if (simpos == SimTxGraph::MISSING) {
439
0
                    assert(feerate.IsEmpty());
440
0
                } else {
441
                    // Just do some quick checks that the reported value is in range. A full
442
                    // recomputation of expected chunk feerates is done at the end.
443
0
                    assert(feerate.size >= main_sim.graph.FeeRate(simpos).size);
444
0
                }
445
0
                break;
446
0
            } else if (!sel_sim.IsOversized() && command-- == 0) {
447
                // GetAncestors/GetDescendants.
448
0
                auto ref = pick_fn();
449
0
                auto result = alt ? real->GetDescendants(*ref, use_main)
450
0
                                  : real->GetAncestors(*ref, use_main);
451
0
                assert(result.size() <= max_count);
452
0
                auto result_set = sel_sim.MakeSet(result);
453
0
                assert(result.size() == result_set.Count());
454
0
                auto expect_set = sel_sim.GetAncDesc(ref, alt);
455
0
                assert(result_set == expect_set);
456
0
                break;
457
0
            } else if (!sel_sim.IsOversized() && command-- == 0) {
458
                // GetAncestorsUnion/GetDescendantsUnion.
459
0
                std::vector<TxGraph::Ref*> refs;
460
                // Gather a list of up to 15 Ref pointers.
461
0
                auto count = provider.ConsumeIntegralInRange<size_t>(0, 15);
462
0
                refs.resize(count);
463
0
                for (size_t i = 0; i < count; ++i) {
464
0
                    refs[i] = pick_fn();
465
0
                }
466
                // Their order should not matter, shuffle them.
467
0
                std::shuffle(refs.begin(), refs.end(), rng);
468
                // Invoke the real function, and convert to SimPos set.
469
0
                auto result = alt ? real->GetDescendantsUnion(refs, use_main)
470
0
                                  : real->GetAncestorsUnion(refs, use_main);
471
0
                auto result_set = sel_sim.MakeSet(result);
472
0
                assert(result.size() == result_set.Count());
473
                // Compute the expected result.
474
0
                SimTxGraph::SetType expect_set;
475
0
                for (TxGraph::Ref* ref : refs) expect_set |= sel_sim.GetAncDesc(ref, alt);
476
                // Compare.
477
0
                assert(result_set == expect_set);
478
0
                break;
479
0
            } else if (!sel_sim.IsOversized() && command-- == 0) {
480
                // GetCluster.
481
0
                auto ref = pick_fn();
482
0
                auto result = real->GetCluster(*ref, use_main);
483
                // Check cluster count limit.
484
0
                assert(result.size() <= max_count);
485
                // Require the result to be topologically valid and not contain duplicates.
486
0
                auto left = sel_sim.graph.Positions();
487
0
                for (auto refptr : result) {
488
0
                    auto simpos = sel_sim.Find(refptr);
489
0
                    assert(simpos != SimTxGraph::MISSING);
490
0
                    assert(left[simpos]);
491
0
                    left.Reset(simpos);
492
0
                    assert(!sel_sim.graph.Ancestors(simpos).Overlaps(left));
493
0
                }
494
                // Require the set to be connected.
495
0
                auto result_set = sel_sim.MakeSet(result);
496
0
                assert(sel_sim.graph.IsConnected(result_set));
497
                // If ref exists, the result must contain it. If not, it must be empty.
498
0
                auto simpos = sel_sim.Find(ref);
499
0
                if (simpos != SimTxGraph::MISSING) {
500
0
                    assert(result_set[simpos]);
501
0
                } else {
502
0
                    assert(result_set.None());
503
0
                }
504
                // Require the set not to have ancestors or descendants outside of it.
505
0
                for (auto i : result_set) {
506
0
                    assert(sel_sim.graph.Ancestors(i).IsSubsetOf(result_set));
507
0
                    assert(sel_sim.graph.Descendants(i).IsSubsetOf(result_set));
508
0
                }
509
0
                break;
510
0
            } else if (command-- == 0) {
511
                // HaveStaging.
512
0
                assert((sims.size() == 2) == real->HaveStaging());
513
0
                break;
514
0
            } else if (sims.size() < 2 && command-- == 0) {
515
                // StartStaging.
516
0
                sims.emplace_back(sims.back());
517
0
                real->StartStaging();
518
0
                break;
519
0
            } else if (sims.size() > 1 && command-- == 0) {
520
                // CommitStaging.
521
0
                real->CommitStaging();
522
0
                sims.erase(sims.begin());
523
0
                break;
524
0
            } else if (sims.size() > 1 && command-- == 0) {
525
                // AbortStaging.
526
0
                real->AbortStaging();
527
0
                sims.pop_back();
528
                // Reset the cached oversized value (if TxGraph::Ref destructions triggered
529
                // removals of main transactions while staging was active, then aborting will
530
                // cause it to be re-evaluated in TxGraph).
531
0
                sims.back().oversized = std::nullopt;
532
0
                break;
533
0
            } else if (!main_sim.IsOversized() && command-- == 0) {
534
                // CompareMainOrder.
535
0
                auto ref_a = pick_fn();
536
0
                auto ref_b = pick_fn();
537
0
                auto sim_a = main_sim.Find(ref_a);
538
0
                auto sim_b = main_sim.Find(ref_b);
539
                // Both transactions must exist in the main graph.
540
0
                if (sim_a == SimTxGraph::MISSING || sim_b == SimTxGraph::MISSING) break;
541
0
                auto cmp = real->CompareMainOrder(*ref_a, *ref_b);
542
                // Distinct transactions have distinct places.
543
0
                if (sim_a != sim_b) assert(cmp != 0);
544
                // Ancestors go before descendants.
545
0
                if (main_sim.graph.Ancestors(sim_a)[sim_b]) assert(cmp >= 0);
546
0
                if (main_sim.graph.Descendants(sim_a)[sim_b]) assert(cmp <= 0);
547
                // Do not verify consistency with chunk feerates, as we cannot easily determine
548
                // these here without making more calls to real, which could affect its internal
549
                // state. A full comparison is done at the end.
550
0
                break;
551
0
            } else if (!sel_sim.IsOversized() && command-- == 0) {
552
                // CountDistinctClusters.
553
0
                std::vector<TxGraph::Ref*> refs;
554
                // Gather a list of up to 15 (or up to 255) Ref pointers.
555
0
                auto count = provider.ConsumeIntegralInRange<size_t>(0, alt ? 255 : 15);
556
0
                refs.resize(count);
557
0
                for (size_t i = 0; i < count; ++i) {
558
0
                    refs[i] = pick_fn();
559
0
                }
560
                // Their order should not matter, shuffle them.
561
0
                std::shuffle(refs.begin(), refs.end(), rng);
562
                // Invoke the real function.
563
0
                auto result = real->CountDistinctClusters(refs, use_main);
564
                // Build a set with representatives of the clusters the Refs occur in in the
565
                // simulated graph. For each, remember the lowest-index transaction SimPos in the
566
                // cluster.
567
0
                SimTxGraph::SetType sim_reps;
568
0
                for (auto ref : refs) {
569
                    // Skip Refs that do not occur in the simulated graph.
570
0
                    auto simpos = sel_sim.Find(ref);
571
0
                    if (simpos == SimTxGraph::MISSING) continue;
572
                    // Find the component that includes ref.
573
0
                    auto component = sel_sim.graph.GetConnectedComponent(sel_sim.graph.Positions(), simpos);
574
                    // Remember the lowest-index SimPos in component, as a representative for it.
575
0
                    assert(component.Any());
576
0
                    sim_reps.Set(component.First());
577
0
                }
578
                // Compare the number of deduplicated representatives with the value returned by
579
                // the real function.
580
0
                assert(result == sim_reps.Count());
581
0
                break;
582
0
            } else if (command-- == 0) {
583
                // DoWork.
584
0
                real->DoWork();
585
0
                break;
586
0
            }
587
0
        }
588
0
    }
589
590
    // After running all modifications, perform an internal sanity check (before invoking
591
    // inspectors that may modify the internal state).
592
0
    real->SanityCheck();
593
594
0
    if (!sims[0].IsOversized()) {
595
        // If the main graph is not oversized, verify the total ordering implied by
596
        // CompareMainOrder.
597
        // First construct two distinct randomized permutations of the positions in sims[0].
598
0
        std::vector<SimTxGraph::Pos> vec1;
599
0
        for (auto i : sims[0].graph.Positions()) vec1.push_back(i);
600
0
        std::shuffle(vec1.begin(), vec1.end(), rng);
601
0
        auto vec2 = vec1;
602
0
        std::shuffle(vec2.begin(), vec2.end(), rng);
603
0
        if (vec1 == vec2) std::next_permutation(vec2.begin(), vec2.end());
604
        // Sort both according to CompareMainOrder. By having randomized starting points, the order
605
        // of CompareMainOrder invocations is somewhat randomized as well.
606
0
        auto cmp = [&](SimTxGraph::Pos a, SimTxGraph::Pos b) noexcept {
607
0
            return real->CompareMainOrder(*sims[0].GetRef(a), *sims[0].GetRef(b)) < 0;
608
0
        };
609
0
        std::sort(vec1.begin(), vec1.end(), cmp);
610
0
        std::sort(vec2.begin(), vec2.end(), cmp);
611
612
        // Verify the resulting orderings are identical. This could only fail if the ordering was
613
        // not total.
614
0
        assert(vec1 == vec2);
615
616
        // Verify that the ordering is topological.
617
0
        auto todo = sims[0].graph.Positions();
618
0
        for (auto i : vec1) {
619
0
            todo.Reset(i);
620
0
            assert(!sims[0].graph.Ancestors(i).Overlaps(todo));
621
0
        }
622
0
        assert(todo.None());
623
624
        // For every transaction in the total ordering, find a random one before it and after it,
625
        // and compare their chunk feerates, which must be consistent with the ordering.
626
0
        for (size_t pos = 0; pos < vec1.size(); ++pos) {
627
0
            auto pos_feerate = real->GetMainChunkFeerate(*sims[0].GetRef(vec1[pos]));
628
0
            if (pos > 0) {
629
0
                size_t before = rng.randrange<size_t>(pos);
630
0
                auto before_feerate = real->GetMainChunkFeerate(*sims[0].GetRef(vec1[before]));
631
0
                assert(FeeRateCompare(before_feerate, pos_feerate) >= 0);
632
0
            }
633
0
            if (pos + 1 < vec1.size()) {
634
0
                size_t after = pos + 1 + rng.randrange<size_t>(vec1.size() - 1 - pos);
635
0
                auto after_feerate = real->GetMainChunkFeerate(*sims[0].GetRef(vec1[after]));
636
0
                assert(FeeRateCompare(after_feerate, pos_feerate) <= 0);
637
0
            }
638
0
        }
639
0
    }
640
641
0
    assert(real->HaveStaging() == (sims.size() > 1));
642
643
    // Try to run a full comparison, for both main_only=false and main_only=true in TxGraph
644
    // inspector functions that support both.
645
0
    for (int main_only = 0; main_only < 2; ++main_only) {
646
0
        auto& sim = main_only ? sims[0] : sims.back();
647
        // Compare simple properties of the graph with the simulation.
648
0
        assert(real->IsOversized(main_only) == sim.IsOversized());
649
0
        assert(real->GetTransactionCount(main_only) == sim.GetTransactionCount());
650
        // If the graph (and the simulation) are not oversized, perform a full comparison.
651
0
        if (!sim.IsOversized()) {
652
0
            auto todo = sim.graph.Positions();
653
            // Iterate over all connected components of the resulting (simulated) graph, each of which
654
            // should correspond to a cluster in the real one.
655
0
            while (todo.Any()) {
656
0
                auto component = sim.graph.FindConnectedComponent(todo);
657
0
                todo -= component;
658
                // Iterate over the transactions in that component.
659
0
                for (auto i : component) {
660
                    // Check its individual feerate against simulation.
661
0
                    assert(sim.graph.FeeRate(i) == real->GetIndividualFeerate(*sim.GetRef(i)));
662
                    // Check its ancestors against simulation.
663
0
                    auto expect_anc = sim.graph.Ancestors(i);
664
0
                    auto anc = sim.MakeSet(real->GetAncestors(*sim.GetRef(i), main_only));
665
0
                    assert(anc.Count() <= max_count);
666
0
                    assert(anc == expect_anc);
667
                    // Check its descendants against simulation.
668
0
                    auto expect_desc = sim.graph.Descendants(i);
669
0
                    auto desc = sim.MakeSet(real->GetDescendants(*sim.GetRef(i), main_only));
670
0
                    assert(desc.Count() <= max_count);
671
0
                    assert(desc == expect_desc);
672
                    // Check the cluster the transaction is part of.
673
0
                    auto cluster = real->GetCluster(*sim.GetRef(i), main_only);
674
0
                    assert(cluster.size() <= max_count);
675
0
                    assert(sim.MakeSet(cluster) == component);
676
                    // Check that the cluster is reported in a valid topological order (its
677
                    // linearization).
678
0
                    std::vector<DepGraphIndex> simlin;
679
0
                    SimTxGraph::SetType done;
680
0
                    for (TxGraph::Ref* ptr : cluster) {
681
0
                        auto simpos = sim.Find(ptr);
682
0
                        assert(sim.graph.Descendants(simpos).IsSubsetOf(component - done));
683
0
                        done.Set(simpos);
684
0
                        assert(sim.graph.Ancestors(simpos).IsSubsetOf(done));
685
0
                        simlin.push_back(simpos);
686
0
                    }
687
                    // Construct a chunking object for the simulated graph, using the reported cluster
688
                    // linearization as ordering, and compare it against the reported chunk feerates.
689
0
                    if (sims.size() == 1 || main_only) {
690
0
                        cluster_linearize::LinearizationChunking simlinchunk(sim.graph, simlin);
691
0
                        DepGraphIndex idx{0};
692
0
                        for (unsigned chunknum = 0; chunknum < simlinchunk.NumChunksLeft(); ++chunknum) {
693
0
                            auto chunk = simlinchunk.GetChunk(chunknum);
694
                            // Require that the chunks of cluster linearizations are connected (this must
695
                            // be the case as all linearizations inside are PostLinearized).
696
0
                            assert(sim.graph.IsConnected(chunk.transactions));
697
                            // Check the chunk feerates of all transactions in the cluster.
698
0
                            while (chunk.transactions.Any()) {
699
0
                                assert(chunk.transactions[simlin[idx]]);
700
0
                                chunk.transactions.Reset(simlin[idx]);
701
0
                                assert(chunk.feerate == real->GetMainChunkFeerate(*cluster[idx]));
702
0
                                ++idx;
703
0
                            }
704
0
                        }
705
0
                    }
706
0
                }
707
0
            }
708
0
        }
709
0
    }
710
711
    // Sanity check again (because invoking inspectors may modify internal unobservable state).
712
0
    real->SanityCheck();
713
714
    // Kill the TxGraph object.
715
0
    real.reset();
716
    // Kill the simulated graphs, with all remaining Refs in it. If any, this verifies that Refs
717
    // can outlive the TxGraph that created them.
718
0
    sims.clear();
719
0
}