Coverage Report

Created: 2025-10-29 16:48

/root/bitcoin/src/test/fuzz/cluster_linearize.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 <cluster_linearize.h>
6
#include <random.h>
7
#include <serialize.h>
8
#include <streams.h>
9
#include <test/fuzz/FuzzedDataProvider.h>
10
#include <test/fuzz/fuzz.h>
11
#include <test/util/cluster_linearize.h>
12
#include <util/bitset.h>
13
#include <util/feefrac.h>
14
15
#include <algorithm>
16
#include <cstdint>
17
#include <utility>
18
#include <vector>
19
20
/*
21
 * The tests in this file primarily cover the candidate finder classes and linearization algorithms.
22
 *
23
 *   <----: An implementation (at the start of the line --) is tested in the test marked with *,
24
 *          possibly by comparison with other implementations (at the end of the line ->).
25
 *   <<---: The right side is implemented using the left side.
26
 *
27
 *   +-----------------------+
28
 *   | SearchCandidateFinder | <<---------------------\
29
 *   +-----------------------+                        |
30
 *     |                                            +-----------+
31
 *     |                                            | Linearize |
32
 *     |                                            +-----------+
33
 *     |        +-------------------------+           |  |
34
 *     |        | AncestorCandidateFinder | <<--------/  |
35
 *     |        +-------------------------+              |
36
 *     |          |                     ^                |        ^^  PRODUCTION CODE
37
 *     |          |                     |                |        ||
38
 *  ==============================================================================================
39
 *     |          |                     |                |        ||
40
 *     | clusterlin_ancestor_finder*    |                |        vv  TEST CODE
41
 *     |                                |                |
42
 *     |-clusterlin_search_finder*      |                |-clusterlin_linearize*
43
 *     |                                |                |
44
 *     v                                |                v
45
 *   +-----------------------+          |           +-----------------+
46
 *   | SimpleCandidateFinder | <<-------------------| SimpleLinearize |
47
 *   +-----------------------+          |           +-----------------+
48
 *                  |                   |                |
49
 *                  +-------------------/                |
50
 *                  |                                    |
51
 *                  |-clusterlin_simple_finder*          |-clusterlin_simple_linearize*
52
 *                  v                                    v
53
 *   +---------------------------+                  +---------------------+
54
 *   | ExhaustiveCandidateFinder |                  | ExhaustiveLinearize |
55
 *   +---------------------------+                  +---------------------+
56
 *
57
 * More tests are included for lower-level and related functions and classes:
58
 * - DepGraph tests:
59
 *   - clusterlin_depgraph_sim
60
 *   - clusterlin_depgraph_serialization
61
 *   - clusterlin_components
62
 * - ChunkLinearization and LinearizationChunking tests:
63
 *   - clusterlin_chunking
64
 *   - clusterlin_linearization_chunking
65
 * - PostLinearize tests:
66
 *   - clusterlin_postlinearize
67
 *   - clusterlin_postlinearize_tree
68
 *   - clusterlin_postlinearize_moved_leaf
69
 * - MergeLinearization tests:
70
 *   - clusterlin_merge
71
 * - FixLinearization tests:
72
 *   - clusterlin_fix_linearization
73
 * - MakeConnected tests (a test-only function):
74
 *   - clusterlin_make_connected
75
 */
76
77
using namespace cluster_linearize;
78
79
namespace {
80
81
/** A simple finder class for candidate sets.
82
 *
83
 * This class matches SearchCandidateFinder in interface and behavior, though with fewer
84
 * optimizations.
85
 */
86
template<typename SetType>
87
class SimpleCandidateFinder
88
{
89
    /** Internal dependency graph. */
90
    const DepGraph<SetType>& m_depgraph;
91
    /** Which transaction are left to include. */
92
    SetType m_todo;
93
94
public:
95
    /** Construct an SimpleCandidateFinder for a given graph. */
96
    SimpleCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept :
97
0
        m_depgraph(depgraph), m_todo{depgraph.Positions()} {}
98
99
    /** Remove a set of transactions from the set of to-be-linearized ones. */
100
0
    void MarkDone(SetType select) noexcept { m_todo -= select; }
101
102
    /** Determine whether unlinearized transactions remain. */
103
0
    bool AllDone() const noexcept { return m_todo.None(); }
104
105
    /** Find a candidate set using at most max_iterations iterations, and the number of iterations
106
     *  actually performed. If that number is less than max_iterations, then the result is optimal.
107
     *
108
     * Always returns a connected set of transactions.
109
     *
110
     * Complexity: O(N * M), where M is the number of connected topological subsets of the cluster.
111
     *             That number is bounded by M <= 2^(N-1).
112
     */
113
    std::pair<SetInfo<SetType>, uint64_t> FindCandidateSet(uint64_t max_iterations) const noexcept
114
0
    {
115
0
        uint64_t iterations_left = max_iterations;
116
        // Queue of work units. Each consists of:
117
        // - inc: set of transactions definitely included
118
        // - und: set of transactions that can be added to inc still
119
0
        std::vector<std::pair<SetType, SetType>> queue;
120
        // Initially we have just one queue element, with the entire graph in und.
121
0
        queue.emplace_back(SetType{}, m_todo);
122
        // Best solution so far. Initialize with the remaining ancestors of the first remaining
123
        // transaction.
124
0
        SetInfo best(m_depgraph, m_depgraph.Ancestors(m_todo.First()) & m_todo);
125
        // Process the queue.
126
0
        while (!queue.empty() && iterations_left) {
127
            // Pop top element of the queue.
128
0
            auto [inc, und] = queue.back();
129
0
            queue.pop_back();
130
            // Look for a transaction to consider adding/removing.
131
0
            bool inc_none = inc.None();
132
0
            for (auto split : und) {
133
                // If inc is empty, consider any split transaction. Otherwise only consider
134
                // transactions that share ancestry with inc so far (which means only connected
135
                // sets will be considered).
136
0
                if (inc_none || inc.Overlaps(m_depgraph.Ancestors(split))) {
137
0
                    --iterations_left;
138
                    // Add a queue entry with split included.
139
0
                    SetInfo new_inc(m_depgraph, inc | (m_todo & m_depgraph.Ancestors(split)));
140
0
                    queue.emplace_back(new_inc.transactions, und - new_inc.transactions);
141
                    // Add a queue entry with split excluded.
142
0
                    queue.emplace_back(inc, und - m_depgraph.Descendants(split));
143
                    // Update statistics to account for the candidate new_inc.
144
0
                    if (new_inc.feerate > best.feerate) best = new_inc;
145
0
                    break;
146
0
                }
147
0
            }
148
0
        }
149
0
        return {std::move(best), max_iterations - iterations_left};
150
0
    }
151
};
152
153
/** A very simple finder class for optimal candidate sets, which tries every subset.
154
 *
155
 * It is even simpler than SimpleCandidateFinder, and exists just to help test the correctness of
156
 * SimpleCandidateFinder, which is then used to test the correctness of SearchCandidateFinder.
157
 */
158
template<typename SetType>
159
class ExhaustiveCandidateFinder
160
{
161
    /** Internal dependency graph. */
162
    const DepGraph<SetType>& m_depgraph;
163
    /** Which transaction are left to include. */
164
    SetType m_todo;
165
166
public:
167
    /** Construct an ExhaustiveCandidateFinder for a given graph. */
168
    ExhaustiveCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept :
169
0
        m_depgraph(depgraph), m_todo{depgraph.Positions()} {}
170
171
    /** Remove a set of transactions from the set of to-be-linearized ones. */
172
0
    void MarkDone(SetType select) noexcept { m_todo -= select; }
173
174
    /** Determine whether unlinearized transactions remain. */
175
0
    bool AllDone() const noexcept { return m_todo.None(); }
176
177
    /** Find the optimal remaining candidate set.
178
     *
179
     * Complexity: O(N * 2^N).
180
     */
181
    SetInfo<SetType> FindCandidateSet() const noexcept
182
0
    {
183
        // Best solution so far.
184
0
        SetInfo<SetType> best{m_todo, m_depgraph.FeeRate(m_todo)};
185
        // The number of combinations to try.
186
0
        uint64_t limit = (uint64_t{1} << m_todo.Count()) - 1;
187
        // Try the transitive closure of every non-empty subset of m_todo.
188
0
        for (uint64_t x = 1; x < limit; ++x) {
189
            // If bit number b is set in x, then the remaining ancestors of the b'th remaining
190
            // transaction in m_todo are included.
191
0
            SetType txn;
192
0
            auto x_shifted{x};
193
0
            for (auto i : m_todo) {
194
0
                if (x_shifted & 1) txn |= m_depgraph.Ancestors(i);
195
0
                x_shifted >>= 1;
196
0
            }
197
0
            SetInfo cur(m_depgraph, txn & m_todo);
198
0
            if (cur.feerate > best.feerate) best = cur;
199
0
        }
200
0
        return best;
201
0
    }
202
};
203
204
/** A simple linearization algorithm.
205
 *
206
 * This matches Linearize() in interface and behavior, though with fewer optimizations, lacking
207
 * the ability to pass in an existing linearization, and using just SimpleCandidateFinder rather
208
 * than AncestorCandidateFinder and SearchCandidateFinder.
209
 */
210
template<typename SetType>
211
std::pair<std::vector<DepGraphIndex>, bool> SimpleLinearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations)
212
0
{
213
0
    std::vector<DepGraphIndex> linearization;
214
0
    SimpleCandidateFinder finder(depgraph);
215
0
    SetType todo = depgraph.Positions();
216
0
    bool optimal = true;
217
0
    while (todo.Any()) {
218
0
        auto [candidate, iterations_done] = finder.FindCandidateSet(max_iterations);
219
0
        if (iterations_done == max_iterations) optimal = false;
220
0
        depgraph.AppendTopo(linearization, candidate.transactions);
221
0
        todo -= candidate.transactions;
222
0
        finder.MarkDone(candidate.transactions);
223
0
        max_iterations -= iterations_done;
224
0
    }
225
0
    return {std::move(linearization), optimal};
226
0
}
227
228
/** An even simpler linearization algorithm that tries all permutations.
229
 *
230
 * This roughly matches SimpleLinearize() (and Linearize) in interface and behavior, but always
231
 * tries all topologically-valid transaction orderings, has no way to bound how much work it does,
232
 * and always finds the optimal. With an O(n!) complexity, it should only be used for small
233
 * clusters.
234
 */
235
template<typename SetType>
236
std::vector<DepGraphIndex> ExhaustiveLinearize(const DepGraph<SetType>& depgraph)
237
0
{
238
    // The best linearization so far, and its chunking.
239
0
    std::vector<DepGraphIndex> linearization;
240
0
    std::vector<FeeFrac> chunking;
241
242
0
    std::vector<DepGraphIndex> perm_linearization;
243
    // Initialize with the lexicographically-first linearization.
244
0
    for (DepGraphIndex i : depgraph.Positions()) perm_linearization.push_back(i);
245
    // Iterate over all valid permutations.
246
0
    do {
247
        /** What prefix of perm_linearization is topological. */
248
0
        DepGraphIndex topo_length{0};
249
0
        TestBitSet perm_done;
250
0
        while (topo_length < perm_linearization.size()) {
251
0
            auto i = perm_linearization[topo_length];
252
0
            perm_done.Set(i);
253
0
            if (!depgraph.Ancestors(i).IsSubsetOf(perm_done)) break;
254
0
            ++topo_length;
255
0
        }
256
0
        if (topo_length == perm_linearization.size()) {
257
            // If all of perm_linearization is topological, check if it is perhaps our best
258
            // linearization so far.
259
0
            auto perm_chunking = ChunkLinearization(depgraph, perm_linearization);
260
0
            auto cmp = CompareChunks(perm_chunking, chunking);
261
            // If the diagram is better, or if it is equal but with more chunks (because we
262
            // prefer minimal chunks), consider this better.
263
0
            if (linearization.empty() || cmp > 0 || (cmp == 0 && perm_chunking.size() > chunking.size())) {
264
0
                linearization = perm_linearization;
265
0
                chunking = perm_chunking;
266
0
            }
267
0
        } else {
268
            // Otherwise, fast forward to the last permutation with the same non-topological
269
            // prefix.
270
0
            auto first_non_topo = perm_linearization.begin() + topo_length;
271
0
            assert(std::is_sorted(first_non_topo + 1, perm_linearization.end()));
272
0
            std::reverse(first_non_topo + 1, perm_linearization.end());
273
0
        }
274
0
    } while(std::next_permutation(perm_linearization.begin(), perm_linearization.end()));
275
276
0
    return linearization;
277
0
}
278
279
280
/** Stitch connected components together in a DepGraph, guaranteeing its corresponding cluster is connected. */
281
template<typename BS>
282
void MakeConnected(DepGraph<BS>& depgraph)
283
0
{
284
0
    auto todo = depgraph.Positions();
285
0
    auto comp = depgraph.FindConnectedComponent(todo);
286
0
    Assume(depgraph.IsConnected(comp));
287
0
    todo -= comp;
288
0
    while (todo.Any()) {
289
0
        auto nextcomp = depgraph.FindConnectedComponent(todo);
290
0
        Assume(depgraph.IsConnected(nextcomp));
291
0
        depgraph.AddDependencies(BS::Singleton(comp.Last()), nextcomp.First());
292
0
        todo -= nextcomp;
293
0
        comp = nextcomp;
294
0
    }
295
0
}
296
297
/** Given a dependency graph, and a todo set, read a topological subset of todo from reader. */
298
template<typename SetType>
299
SetType ReadTopologicalSet(const DepGraph<SetType>& depgraph, const SetType& todo, SpanReader& reader, bool non_empty)
300
0
{
301
    // Read a bitmask from the fuzzing input. Add 1 if non_empty, so the mask is definitely not
302
    // zero in that case.
303
0
    uint64_t mask{0};
304
0
    try {
305
0
        reader >> VARINT(mask);
306
0
    } catch(const std::ios_base::failure&) {}
307
0
    if (mask != uint64_t(-1)) mask += non_empty;
308
309
0
    SetType ret;
310
0
    for (auto i : todo) {
311
0
        if (!ret[i]) {
312
0
            if (mask & 1) ret |= depgraph.Ancestors(i);
313
0
            mask >>= 1;
314
0
        }
315
0
    }
316
0
    ret &= todo;
317
318
    // While mask starts off non-zero if non_empty is true, it is still possible that all its low
319
    // bits are 0, and ret ends up being empty. As a last resort, use the in-todo ancestry of the
320
    // first todo position.
321
0
    if (non_empty && ret.None()) {
322
0
        Assume(todo.Any());
323
0
        ret = depgraph.Ancestors(todo.First()) & todo;
324
0
        Assume(ret.Any());
325
0
    }
326
0
    return ret;
327
0
}
328
329
/** Given a dependency graph, construct any valid linearization for it, reading from a SpanReader. */
330
template<typename BS>
331
std::vector<DepGraphIndex> ReadLinearization(const DepGraph<BS>& depgraph, SpanReader& reader)
332
0
{
333
0
    std::vector<DepGraphIndex> linearization;
334
0
    TestBitSet todo = depgraph.Positions();
335
    // In every iteration one topologically-valid transaction is appended to linearization.
336
0
    while (todo.Any()) {
337
        // Compute the set of transactions with no not-yet-included ancestors.
338
0
        TestBitSet potential_next;
339
0
        for (auto j : todo) {
340
0
            if ((depgraph.Ancestors(j) & todo) == TestBitSet::Singleton(j)) {
341
0
                potential_next.Set(j);
342
0
            }
343
0
        }
344
        // There must always be one (otherwise there is a cycle in the graph).
345
0
        assert(potential_next.Any());
346
        // Read a number from reader, and interpret it as index into potential_next.
347
0
        uint64_t idx{0};
348
0
        try {
349
0
            reader >> VARINT(idx);
350
0
        } catch (const std::ios_base::failure&) {}
351
0
        idx %= potential_next.Count();
352
        // Find out which transaction that corresponds to.
353
0
        for (auto j : potential_next) {
354
0
            if (idx == 0) {
355
                // When found, add it to linearization and remove it from todo.
356
0
                linearization.push_back(j);
357
0
                assert(todo[j]);
358
0
                todo.Reset(j);
359
0
                break;
360
0
            }
361
0
            --idx;
362
0
        }
363
0
    }
364
0
    return linearization;
365
0
}
366
367
} // namespace
368
369
FUZZ_TARGET(clusterlin_depgraph_sim)
370
0
{
371
    // Simulation test to verify the full behavior of DepGraph.
372
373
0
    FuzzedDataProvider provider(buffer.data(), buffer.size());
374
375
    /** Real DepGraph being tested. */
376
0
    DepGraph<TestBitSet> real;
377
    /** Simulated DepGraph (sim[i] is std::nullopt if position i does not exist; otherwise,
378
     *  sim[i]->first is its individual feerate, and sim[i]->second is its set of ancestors. */
379
0
    std::array<std::optional<std::pair<FeeFrac, TestBitSet>>, TestBitSet::Size()> sim;
380
    /** The number of non-nullopt position in sim. */
381
0
    DepGraphIndex num_tx_sim{0};
382
383
    /** Read a valid index of a transaction from the provider. */
384
0
    auto idx_fn = [&]() {
385
0
        auto offset = provider.ConsumeIntegralInRange<DepGraphIndex>(0, num_tx_sim - 1);
386
0
        for (DepGraphIndex i = 0; i < sim.size(); ++i) {
387
0
            if (!sim[i].has_value()) continue;
388
0
            if (offset == 0) return i;
389
0
            --offset;
390
0
        }
391
0
        assert(false);
392
0
        return DepGraphIndex(-1);
393
0
    };
394
395
    /** Read a valid subset of the transactions from the provider. */
396
0
    auto subset_fn = [&]() {
397
0
        auto range = (uint64_t{1} << num_tx_sim) - 1;
398
0
        const auto mask = provider.ConsumeIntegralInRange<uint64_t>(0, range);
399
0
        auto mask_shifted = mask;
400
0
        TestBitSet subset;
401
0
        for (DepGraphIndex i = 0; i < sim.size(); ++i) {
402
0
            if (!sim[i].has_value()) continue;
403
0
            if (mask_shifted & 1) {
404
0
                subset.Set(i);
405
0
            }
406
0
            mask_shifted >>= 1;
407
0
        }
408
0
        assert(mask_shifted == 0);
409
0
        return subset;
410
0
    };
411
412
    /** Read any set of transactions from the provider (including unused positions). */
413
0
    auto set_fn = [&]() {
414
0
        auto range = (uint64_t{1} << sim.size()) - 1;
415
0
        const auto mask = provider.ConsumeIntegralInRange<uint64_t>(0, range);
416
0
        TestBitSet set;
417
0
        for (DepGraphIndex i = 0; i < sim.size(); ++i) {
418
0
            if ((mask >> i) & 1) {
419
0
                set.Set(i);
420
0
            }
421
0
        }
422
0
        return set;
423
0
    };
424
425
    /** Propagate ancestor information in sim. */
426
0
    auto anc_update_fn = [&]() {
427
0
        while (true) {
428
0
            bool updates{false};
429
0
            for (DepGraphIndex chl = 0; chl < sim.size(); ++chl) {
430
0
                if (!sim[chl].has_value()) continue;
431
0
                for (auto par : sim[chl]->second) {
432
0
                    if (!sim[chl]->second.IsSupersetOf(sim[par]->second)) {
433
0
                        sim[chl]->second |= sim[par]->second;
434
0
                        updates = true;
435
0
                    }
436
0
                }
437
0
            }
438
0
            if (!updates) break;
439
0
        }
440
0
    };
441
442
    /** Compare the state of transaction i in the simulation with the real one. */
443
0
    auto check_fn = [&](DepGraphIndex i) {
444
        // Compare used positions.
445
0
        assert(real.Positions()[i] == sim[i].has_value());
446
0
        if (sim[i].has_value()) {
447
            // Compare feerate.
448
0
            assert(real.FeeRate(i) == sim[i]->first);
449
            // Compare ancestors (note that SanityCheck verifies correspondence between ancestors
450
            // and descendants, so we can restrict ourselves to ancestors here).
451
0
            assert(real.Ancestors(i) == sim[i]->second);
452
0
        }
453
0
    };
454
455
0
    auto last_compaction_pos{real.PositionRange()};
456
457
0
    LIMITED_WHILE(provider.remaining_bytes() > 0, 1000) {
458
0
        int command = provider.ConsumeIntegral<uint8_t>() % 4;
459
0
        while (true) {
460
            // Iterate decreasing command until an applicable branch is found.
461
0
            if (num_tx_sim < TestBitSet::Size() && command-- == 0) {
462
                // AddTransaction.
463
0
                auto fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
464
0
                auto size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
465
0
                FeeFrac feerate{fee, size};
466
                // Apply to DepGraph.
467
0
                auto idx = real.AddTransaction(feerate);
468
                // Verify that the returned index is correct.
469
0
                assert(!sim[idx].has_value());
470
0
                for (DepGraphIndex i = 0; i < TestBitSet::Size(); ++i) {
471
0
                    if (!sim[i].has_value()) {
472
0
                        assert(idx == i);
473
0
                        break;
474
0
                    }
475
0
                }
476
                // Update sim.
477
0
                sim[idx] = {feerate, TestBitSet::Singleton(idx)};
478
0
                ++num_tx_sim;
479
0
                break;
480
0
            } else if (num_tx_sim > 0 && command-- == 0) {
481
                // AddDependencies.
482
0
                DepGraphIndex child = idx_fn();
483
0
                auto parents = subset_fn();
484
                // Apply to DepGraph.
485
0
                real.AddDependencies(parents, child);
486
                // Apply to sim.
487
0
                sim[child]->second |= parents;
488
0
                break;
489
0
            } else if (num_tx_sim > 0 && command-- == 0) {
490
                // Remove transactions.
491
0
                auto del = set_fn();
492
                // Propagate all ancestry information before deleting anything in the simulation (as
493
                // intermediary transactions may be deleted which impact connectivity).
494
0
                anc_update_fn();
495
                // Compare the state of the transactions being deleted.
496
0
                for (auto i : del) check_fn(i);
497
                // Apply to DepGraph.
498
0
                real.RemoveTransactions(del);
499
                // Apply to sim.
500
0
                for (DepGraphIndex i = 0; i < sim.size(); ++i) {
501
0
                    if (sim[i].has_value()) {
502
0
                        if (del[i]) {
503
0
                            --num_tx_sim;
504
0
                            sim[i] = std::nullopt;
505
0
                        } else {
506
0
                            sim[i]->second -= del;
507
0
                        }
508
0
                    }
509
0
                }
510
0
                break;
511
0
            } else if (command-- == 0) {
512
                // Compact.
513
0
                const size_t mem_before{real.DynamicMemoryUsage()};
514
0
                real.Compact();
515
0
                const size_t mem_after{real.DynamicMemoryUsage()};
516
0
                assert(real.PositionRange() < last_compaction_pos ? mem_after < mem_before : mem_after <= mem_before);
517
0
                last_compaction_pos = real.PositionRange();
518
0
                break;
519
0
            }
520
0
        }
521
0
    }
522
523
    // Compare the real obtained depgraph against the simulation.
524
0
    anc_update_fn();
525
0
    for (DepGraphIndex i = 0; i < sim.size(); ++i) check_fn(i);
526
0
    assert(real.TxCount() == num_tx_sim);
527
    // Sanity check the result (which includes round-tripping serialization, if applicable).
528
0
    SanityCheck(real);
529
0
}
530
531
FUZZ_TARGET(clusterlin_depgraph_serialization)
532
0
{
533
    // Verify that any deserialized depgraph is acyclic and roundtrips to an identical depgraph.
534
535
    // Construct a graph by deserializing.
536
0
    SpanReader reader(buffer);
537
0
    DepGraph<TestBitSet> depgraph;
538
0
    DepGraphIndex par_code{0}, chl_code{0};
539
0
    try {
540
0
        reader >> Using<DepGraphFormatter>(depgraph) >> VARINT(par_code) >> VARINT(chl_code);
541
0
    } catch (const std::ios_base::failure&) {}
542
0
    SanityCheck(depgraph);
543
544
    // Verify the graph is a DAG.
545
0
    assert(depgraph.IsAcyclic());
546
547
    // Introduce a cycle, and then test that IsAcyclic returns false.
548
0
    if (depgraph.TxCount() < 2) return;
549
0
    DepGraphIndex par(0), chl(0);
550
    // Pick any transaction of depgraph as parent.
551
0
    par_code %= depgraph.TxCount();
552
0
    for (auto i : depgraph.Positions()) {
553
0
        if (par_code == 0) {
554
0
            par = i;
555
0
            break;
556
0
        }
557
0
        --par_code;
558
0
    }
559
    // Pick any ancestor of par (excluding itself) as child, if any.
560
0
    auto ancestors = depgraph.Ancestors(par) - TestBitSet::Singleton(par);
561
0
    if (ancestors.None()) return;
562
0
    chl_code %= ancestors.Count();
563
0
    for (auto i : ancestors) {
564
0
        if (chl_code == 0) {
565
0
            chl = i;
566
0
            break;
567
0
        }
568
0
        --chl_code;
569
0
    }
570
    // Add the cycle-introducing dependency.
571
0
    depgraph.AddDependencies(TestBitSet::Singleton(par), chl);
572
    // Check that we now detect a cycle.
573
0
    assert(!depgraph.IsAcyclic());
574
0
}
575
576
FUZZ_TARGET(clusterlin_components)
577
0
{
578
    // Verify the behavior of DepGraphs's FindConnectedComponent and IsConnected functions.
579
580
    // Construct a depgraph.
581
0
    SpanReader reader(buffer);
582
0
    DepGraph<TestBitSet> depgraph;
583
0
    std::vector<DepGraphIndex> linearization;
584
0
    try {
585
0
        reader >> Using<DepGraphFormatter>(depgraph);
586
0
    } catch (const std::ios_base::failure&) {}
587
588
0
    TestBitSet todo = depgraph.Positions();
589
0
    while (todo.Any()) {
590
        // Pick a transaction in todo, or nothing.
591
0
        std::optional<DepGraphIndex> picked;
592
0
        {
593
0
            uint64_t picked_num{0};
594
0
            try {
595
0
                reader >> VARINT(picked_num);
596
0
            } catch (const std::ios_base::failure&) {}
597
0
            if (picked_num < todo.Size() && todo[picked_num]) {
598
0
                picked = picked_num;
599
0
            }
600
0
        }
601
602
        // Find a connected component inside todo, including picked if any.
603
0
        auto component = picked ? depgraph.GetConnectedComponent(todo, *picked)
604
0
                                : depgraph.FindConnectedComponent(todo);
605
606
        // The component must be a subset of todo and non-empty.
607
0
        assert(component.IsSubsetOf(todo));
608
0
        assert(component.Any());
609
610
        // If picked was provided, the component must include it.
611
0
        if (picked) assert(component[*picked]);
612
613
        // If todo is the entire graph, and the entire graph is connected, then the component must
614
        // be the entire graph.
615
0
        if (todo == depgraph.Positions()) {
616
0
            assert((component == todo) == depgraph.IsConnected());
617
0
        }
618
619
        // If subset is connected, then component must match subset.
620
0
        assert((component == todo) == depgraph.IsConnected(todo));
621
622
        // The component cannot have any ancestors or descendants outside of component but in todo.
623
0
        for (auto i : component) {
624
0
            assert((depgraph.Ancestors(i) & todo).IsSubsetOf(component));
625
0
            assert((depgraph.Descendants(i) & todo).IsSubsetOf(component));
626
0
        }
627
628
        // Starting from any component element, we must be able to reach every element.
629
0
        for (auto i : component) {
630
            // Start with just i as reachable.
631
0
            TestBitSet reachable = TestBitSet::Singleton(i);
632
            // Add in-todo descendants and ancestors to reachable until it does not change anymore.
633
0
            while (true) {
634
0
                TestBitSet new_reachable = reachable;
635
0
                for (auto j : new_reachable) {
636
0
                    new_reachable |= depgraph.Ancestors(j) & todo;
637
0
                    new_reachable |= depgraph.Descendants(j) & todo;
638
0
                }
639
0
                if (new_reachable == reachable) break;
640
0
                reachable = new_reachable;
641
0
            }
642
            // Verify that the result is the entire component.
643
0
            assert(component == reachable);
644
0
        }
645
646
        // Construct an arbitrary subset of todo.
647
0
        uint64_t subset_bits{0};
648
0
        try {
649
0
            reader >> VARINT(subset_bits);
650
0
        } catch (const std::ios_base::failure&) {}
651
0
        TestBitSet subset;
652
0
        for (DepGraphIndex i : depgraph.Positions()) {
653
0
            if (todo[i]) {
654
0
                if (subset_bits & 1) subset.Set(i);
655
0
                subset_bits >>= 1;
656
0
            }
657
0
        }
658
        // Which must be non-empty.
659
0
        if (subset.None()) subset = TestBitSet::Singleton(todo.First());
660
        // Remove it from todo.
661
0
        todo -= subset;
662
0
    }
663
664
    // No components can be found in an empty subset.
665
0
    assert(depgraph.FindConnectedComponent(todo).None());
666
0
}
667
668
FUZZ_TARGET(clusterlin_make_connected)
669
0
{
670
    // Verify that MakeConnected makes graphs connected.
671
672
0
    SpanReader reader(buffer);
673
0
    DepGraph<TestBitSet> depgraph;
674
0
    try {
675
0
        reader >> Using<DepGraphFormatter>(depgraph);
676
0
    } catch (const std::ios_base::failure&) {}
677
0
    MakeConnected(depgraph);
678
0
    SanityCheck(depgraph);
679
0
    assert(depgraph.IsConnected());
680
0
}
681
682
FUZZ_TARGET(clusterlin_chunking)
683
0
{
684
    // Verify the correctness of the ChunkLinearization function.
685
686
    // Construct a graph by deserializing.
687
0
    SpanReader reader(buffer);
688
0
    DepGraph<TestBitSet> depgraph;
689
0
    try {
690
0
        reader >> Using<DepGraphFormatter>(depgraph);
691
0
    } catch (const std::ios_base::failure&) {}
692
693
    // Read a valid linearization for depgraph.
694
0
    auto linearization = ReadLinearization(depgraph, reader);
695
696
    // Invoke the chunking function.
697
0
    auto chunking = ChunkLinearization(depgraph, linearization);
698
699
    // Verify that chunk feerates are monotonically non-increasing.
700
0
    for (size_t i = 1; i < chunking.size(); ++i) {
701
0
        assert(!(chunking[i] >> chunking[i - 1]));
702
0
    }
703
704
    // Naively recompute the chunks (each is the highest-feerate prefix of what remains).
705
0
    auto todo = depgraph.Positions();
706
0
    for (const auto& chunk_feerate : chunking) {
707
0
        assert(todo.Any());
708
0
        SetInfo<TestBitSet> accumulator, best;
709
0
        for (DepGraphIndex idx : linearization) {
710
0
            if (todo[idx]) {
711
0
                accumulator.Set(depgraph, idx);
712
0
                if (best.feerate.IsEmpty() || accumulator.feerate >> best.feerate) {
713
0
                    best = accumulator;
714
0
                }
715
0
            }
716
0
        }
717
0
        assert(chunk_feerate == best.feerate);
718
0
        assert(best.transactions.IsSubsetOf(todo));
719
0
        todo -= best.transactions;
720
0
    }
721
0
    assert(todo.None());
722
0
}
723
724
FUZZ_TARGET(clusterlin_ancestor_finder)
725
0
{
726
    // Verify that AncestorCandidateFinder works as expected.
727
728
    // Retrieve a depgraph from the fuzz input.
729
0
    SpanReader reader(buffer);
730
0
    DepGraph<TestBitSet> depgraph;
731
0
    try {
732
0
        reader >> Using<DepGraphFormatter>(depgraph);
733
0
    } catch (const std::ios_base::failure&) {}
734
735
0
    AncestorCandidateFinder anc_finder(depgraph);
736
0
    auto todo = depgraph.Positions();
737
0
    while (todo.Any()) {
738
        // Call the ancestor finder's FindCandidateSet for what remains of the graph.
739
0
        assert(!anc_finder.AllDone());
740
0
        assert(todo.Count() == anc_finder.NumRemaining());
741
0
        auto best_anc = anc_finder.FindCandidateSet();
742
        // Sanity check the result.
743
0
        assert(best_anc.transactions.Any());
744
0
        assert(best_anc.transactions.IsSubsetOf(todo));
745
0
        assert(depgraph.FeeRate(best_anc.transactions) == best_anc.feerate);
746
0
        assert(depgraph.IsConnected(best_anc.transactions));
747
        // Check that it is topologically valid.
748
0
        for (auto i : best_anc.transactions) {
749
0
            assert((depgraph.Ancestors(i) & todo).IsSubsetOf(best_anc.transactions));
750
0
        }
751
752
        // Compute all remaining ancestor sets.
753
0
        std::optional<SetInfo<TestBitSet>> real_best_anc;
754
0
        for (auto i : todo) {
755
0
            SetInfo info(depgraph, todo & depgraph.Ancestors(i));
756
0
            if (!real_best_anc.has_value() || info.feerate > real_best_anc->feerate) {
757
0
                real_best_anc = info;
758
0
            }
759
0
        }
760
        // The set returned by anc_finder must equal the real best ancestor sets.
761
0
        assert(real_best_anc.has_value());
762
0
        assert(*real_best_anc == best_anc);
763
764
        // Find a non-empty topologically valid subset of transactions to remove from the graph.
765
        // Using an empty set would mean the next iteration is identical to the current one, and
766
        // could cause an infinite loop.
767
0
        auto del_set = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true);
768
0
        todo -= del_set;
769
0
        anc_finder.MarkDone(del_set);
770
0
    }
771
0
    assert(anc_finder.AllDone());
772
0
    assert(anc_finder.NumRemaining() == 0);
773
0
}
774
775
static constexpr auto MAX_SIMPLE_ITERATIONS = 300000;
776
777
FUZZ_TARGET(clusterlin_simple_finder)
778
0
{
779
    // Verify that SimpleCandidateFinder works as expected by sanity checking the results
780
    // and comparing them (if claimed to be optimal) against the sets found by
781
    // ExhaustiveCandidateFinder and AncestorCandidateFinder.
782
    //
783
    // Note that SimpleCandidateFinder is only used in tests; the purpose of this fuzz test is to
784
    // establish confidence in SimpleCandidateFinder, so that it can be used to test
785
    // SearchCandidateFinder below.
786
787
    // Retrieve a depgraph from the fuzz input.
788
0
    SpanReader reader(buffer);
789
0
    DepGraph<TestBitSet> depgraph;
790
0
    try {
791
0
        reader >> Using<DepGraphFormatter>(depgraph);
792
0
    } catch (const std::ios_base::failure&) {}
793
794
    // Instantiate the SimpleCandidateFinder to be tested, and the ExhaustiveCandidateFinder and
795
    // AncestorCandidateFinder it is being tested against.
796
0
    SimpleCandidateFinder smp_finder(depgraph);
797
0
    ExhaustiveCandidateFinder exh_finder(depgraph);
798
0
    AncestorCandidateFinder anc_finder(depgraph);
799
800
0
    auto todo = depgraph.Positions();
801
0
    while (todo.Any()) {
802
0
        assert(!smp_finder.AllDone());
803
0
        assert(!exh_finder.AllDone());
804
0
        assert(!anc_finder.AllDone());
805
0
        assert(anc_finder.NumRemaining() == todo.Count());
806
807
        // Call SimpleCandidateFinder.
808
0
        auto [found, iterations_done] = smp_finder.FindCandidateSet(MAX_SIMPLE_ITERATIONS);
809
0
        bool optimal = (iterations_done != MAX_SIMPLE_ITERATIONS);
810
811
        // Sanity check the result.
812
0
        assert(iterations_done <= MAX_SIMPLE_ITERATIONS);
813
0
        assert(found.transactions.Any());
814
0
        assert(found.transactions.IsSubsetOf(todo));
815
0
        assert(depgraph.FeeRate(found.transactions) == found.feerate);
816
        // Check that it is topologically valid.
817
0
        for (auto i : found.transactions) {
818
0
            assert(found.transactions.IsSupersetOf(depgraph.Ancestors(i) & todo));
819
0
        }
820
821
        // At most 2^(N-1) iterations can be required: the number of non-empty connected subsets a
822
        // graph with N transactions can have. If MAX_SIMPLE_ITERATIONS exceeds this number, the
823
        // result is necessarily optimal.
824
0
        assert(iterations_done <= (uint64_t{1} << (todo.Count() - 1)));
825
0
        if (MAX_SIMPLE_ITERATIONS > (uint64_t{1} << (todo.Count() - 1))) assert(optimal);
826
827
        // SimpleCandidateFinder only finds connected sets.
828
0
        assert(depgraph.IsConnected(found.transactions));
829
830
        // Perform further quality checks only if SimpleCandidateFinder claims an optimal result.
831
0
        if (optimal) {
832
            // Compare with AncestorCandidateFinder.
833
0
            auto anc = anc_finder.FindCandidateSet();
834
0
            assert(anc.feerate <= found.feerate);
835
836
0
            if (todo.Count() <= 12) {
837
                // Compare with ExhaustiveCandidateFinder. This quickly gets computationally
838
                // expensive for large clusters (O(2^n)), so only do it for sufficiently small ones.
839
0
                auto exhaustive = exh_finder.FindCandidateSet();
840
0
                assert(exhaustive.feerate == found.feerate);
841
0
            }
842
843
            // Compare with a non-empty topological set read from the fuzz input (comparing with an
844
            // empty set is not interesting).
845
0
            auto read_topo = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true);
846
0
            assert(found.feerate >= depgraph.FeeRate(read_topo));
847
0
        }
848
849
        // Find a non-empty topologically valid subset of transactions to remove from the graph.
850
        // Using an empty set would mean the next iteration is identical to the current one, and
851
        // could cause an infinite loop.
852
0
        auto del_set = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true);
853
0
        todo -= del_set;
854
0
        smp_finder.MarkDone(del_set);
855
0
        exh_finder.MarkDone(del_set);
856
0
        anc_finder.MarkDone(del_set);
857
0
    }
858
859
0
    assert(smp_finder.AllDone());
860
0
    assert(exh_finder.AllDone());
861
0
    assert(anc_finder.AllDone());
862
0
    assert(anc_finder.NumRemaining() == 0);
863
0
}
864
865
FUZZ_TARGET(clusterlin_search_finder)
866
0
{
867
    // Verify that SearchCandidateFinder works as expected by sanity checking the results
868
    // and comparing with the results from SimpleCandidateFinder and AncestorCandidateFinder,
869
    // if the result is claimed to be optimal.
870
871
    // Retrieve an RNG seed, a depgraph, and whether to make it connected, from the fuzz input.
872
0
    SpanReader reader(buffer);
873
0
    DepGraph<TestBitSet> depgraph;
874
0
    uint64_t rng_seed{0};
875
0
    uint8_t make_connected{1};
876
0
    try {
877
0
        reader >> Using<DepGraphFormatter>(depgraph) >> rng_seed >> make_connected;
878
0
    } catch (const std::ios_base::failure&) {}
879
    // The most complicated graphs are connected ones (other ones just split up). Optionally force
880
    // the graph to be connected.
881
0
    if (make_connected) MakeConnected(depgraph);
882
883
    // Instantiate the candidate finders.
884
0
    SearchCandidateFinder src_finder(depgraph, rng_seed);
885
0
    SimpleCandidateFinder smp_finder(depgraph);
886
0
    AncestorCandidateFinder anc_finder(depgraph);
887
888
0
    auto todo = depgraph.Positions();
889
0
    while (todo.Any()) {
890
0
        assert(!src_finder.AllDone());
891
0
        assert(!smp_finder.AllDone());
892
0
        assert(!anc_finder.AllDone());
893
0
        assert(anc_finder.NumRemaining() == todo.Count());
894
895
        // For each iteration, read an iteration count limit from the fuzz input.
896
0
        uint64_t max_iterations = 1;
897
0
        try {
898
0
            reader >> VARINT(max_iterations);
899
0
        } catch (const std::ios_base::failure&) {}
900
0
        max_iterations &= 0xfffff;
901
902
        // Read an initial subset from the fuzz input (allowed to be empty).
903
0
        auto init_set = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/false);
904
0
        SetInfo init_best(depgraph, init_set);
905
906
        // Call the search finder's FindCandidateSet for what remains of the graph.
907
0
        auto [found, iterations_done] = src_finder.FindCandidateSet(max_iterations, init_best);
908
0
        bool optimal = iterations_done < max_iterations;
909
910
        // Sanity check the result.
911
0
        assert(iterations_done <= max_iterations);
912
0
        assert(found.transactions.Any());
913
0
        assert(found.transactions.IsSubsetOf(todo));
914
0
        assert(depgraph.FeeRate(found.transactions) == found.feerate);
915
0
        if (!init_best.feerate.IsEmpty()) assert(found.feerate >= init_best.feerate);
916
        // Check that it is topologically valid.
917
0
        for (auto i : found.transactions) {
918
0
            assert(found.transactions.IsSupersetOf(depgraph.Ancestors(i) & todo));
919
0
        }
920
921
        // At most 2^(N-1) iterations can be required: the maximum number of non-empty topological
922
        // subsets a (connected) cluster with N transactions can have. Even when the cluster is no
923
        // longer connected after removing certain transactions, this holds, because the connected
924
        // components are searched separately.
925
0
        assert(iterations_done <= (uint64_t{1} << (todo.Count() - 1)));
926
        // Additionally, test that no more than sqrt(2^N)+1 iterations are required. This is just
927
        // an empirical bound that seems to hold, without proof. Still, add a test for it so we
928
        // can learn about counterexamples if they exist.
929
0
        if (iterations_done >= 1 && todo.Count() <= 63) {
930
0
            Assume((iterations_done - 1) * (iterations_done - 1) <= uint64_t{1} << todo.Count());
931
0
        }
932
933
        // Perform quality checks only if SearchCandidateFinder claims an optimal result.
934
0
        if (optimal) {
935
            // Optimal sets are always connected.
936
0
            assert(depgraph.IsConnected(found.transactions));
937
938
            // Compare with SimpleCandidateFinder.
939
0
            auto [simple, simple_iters] = smp_finder.FindCandidateSet(MAX_SIMPLE_ITERATIONS);
940
0
            assert(found.feerate >= simple.feerate);
941
0
            if (simple_iters < MAX_SIMPLE_ITERATIONS) {
942
0
                assert(found.feerate == simple.feerate);
943
0
            }
944
945
            // Compare with AncestorCandidateFinder;
946
0
            auto anc = anc_finder.FindCandidateSet();
947
0
            assert(found.feerate >= anc.feerate);
948
949
            // Compare with a non-empty topological set read from the fuzz input (comparing with an
950
            // empty set is not interesting).
951
0
            auto read_topo = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true);
952
0
            assert(found.feerate >= depgraph.FeeRate(read_topo));
953
0
        }
954
955
        // Find a non-empty topologically valid subset of transactions to remove from the graph.
956
        // Using an empty set would mean the next iteration is identical to the current one, and
957
        // could cause an infinite loop.
958
0
        auto del_set = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true);
959
0
        todo -= del_set;
960
0
        src_finder.MarkDone(del_set);
961
0
        smp_finder.MarkDone(del_set);
962
0
        anc_finder.MarkDone(del_set);
963
0
    }
964
965
0
    assert(src_finder.AllDone());
966
0
    assert(smp_finder.AllDone());
967
0
    assert(anc_finder.AllDone());
968
0
    assert(anc_finder.NumRemaining() == 0);
969
0
}
970
971
FUZZ_TARGET(clusterlin_linearization_chunking)
972
0
{
973
    // Verify the behavior of LinearizationChunking.
974
975
    // Retrieve a depgraph from the fuzz input.
976
0
    SpanReader reader(buffer);
977
0
    DepGraph<TestBitSet> depgraph;
978
0
    try {
979
0
        reader >> Using<DepGraphFormatter>(depgraph);
980
0
    } catch (const std::ios_base::failure&) {}
981
982
    // Retrieve a topologically-valid subset of depgraph (allowed to be empty, because the argument
983
    // to LinearizationChunking::Intersect is allowed to be empty).
984
0
    auto todo = depgraph.Positions();
985
0
    auto subset = SetInfo(depgraph, ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/false));
986
987
    // Retrieve a valid linearization for depgraph.
988
0
    auto linearization = ReadLinearization(depgraph, reader);
989
990
    // Construct a LinearizationChunking object, initially for the whole linearization.
991
0
    LinearizationChunking chunking(depgraph, linearization);
992
993
    // Incrementally remove transactions from the chunking object, and check various properties at
994
    // every step.
995
0
    while (todo.Any()) {
996
0
        assert(chunking.NumChunksLeft() > 0);
997
998
        // Construct linearization with just todo.
999
0
        std::vector<DepGraphIndex> linearization_left;
1000
0
        for (auto i : linearization) {
1001
0
            if (todo[i]) linearization_left.push_back(i);
1002
0
        }
1003
1004
        // Compute the chunking for linearization_left.
1005
0
        auto chunking_left = ChunkLinearization(depgraph, linearization_left);
1006
1007
        // Verify that it matches the feerates of the chunks of chunking.
1008
0
        assert(chunking.NumChunksLeft() == chunking_left.size());
1009
0
        for (DepGraphIndex i = 0; i < chunking.NumChunksLeft(); ++i) {
1010
0
            assert(chunking.GetChunk(i).feerate == chunking_left[i]);
1011
0
        }
1012
1013
        // Check consistency of chunking.
1014
0
        TestBitSet combined;
1015
0
        for (DepGraphIndex i = 0; i < chunking.NumChunksLeft(); ++i) {
1016
0
            const auto& chunk_info = chunking.GetChunk(i);
1017
            // Chunks must be non-empty.
1018
0
            assert(chunk_info.transactions.Any());
1019
            // Chunk feerates must be monotonically non-increasing.
1020
0
            if (i > 0) assert(!(chunk_info.feerate >> chunking.GetChunk(i - 1).feerate));
1021
            // Chunks must be a subset of what is left of the linearization.
1022
0
            assert(chunk_info.transactions.IsSubsetOf(todo));
1023
            // Chunks' claimed feerates must match their transactions' aggregate feerate.
1024
0
            assert(depgraph.FeeRate(chunk_info.transactions) == chunk_info.feerate);
1025
            // Chunks must be the highest-feerate remaining prefix.
1026
0
            SetInfo<TestBitSet> accumulator, best;
1027
0
            for (auto j : linearization) {
1028
0
                if (todo[j] && !combined[j]) {
1029
0
                    accumulator.Set(depgraph, j);
1030
0
                    if (best.feerate.IsEmpty() || accumulator.feerate > best.feerate) {
1031
0
                        best = accumulator;
1032
0
                    }
1033
0
                }
1034
0
            }
1035
0
            assert(best.transactions == chunk_info.transactions);
1036
0
            assert(best.feerate == chunk_info.feerate);
1037
            // Chunks cannot overlap.
1038
0
            assert(!chunk_info.transactions.Overlaps(combined));
1039
0
            combined |= chunk_info.transactions;
1040
            // Chunks must be topological.
1041
0
            for (auto idx : chunk_info.transactions) {
1042
0
                assert((depgraph.Ancestors(idx) & todo).IsSubsetOf(combined));
1043
0
            }
1044
0
        }
1045
0
        assert(combined == todo);
1046
1047
        // Verify the expected properties of LinearizationChunking::IntersectPrefixes:
1048
0
        auto intersect = chunking.IntersectPrefixes(subset);
1049
        // - Intersecting again doesn't change the result.
1050
0
        assert(chunking.IntersectPrefixes(intersect) == intersect);
1051
        // - The intersection is topological.
1052
0
        TestBitSet intersect_anc;
1053
0
        for (auto idx : intersect.transactions) {
1054
0
            intersect_anc |= (depgraph.Ancestors(idx) & todo);
1055
0
        }
1056
0
        assert(intersect.transactions == intersect_anc);
1057
        // - The claimed intersection feerate matches its transactions.
1058
0
        assert(intersect.feerate == depgraph.FeeRate(intersect.transactions));
1059
        // - The intersection may only be empty if its input is empty.
1060
0
        assert(intersect.transactions.Any() == subset.transactions.Any());
1061
        // - The intersection feerate must be as high as the input.
1062
0
        assert(intersect.feerate >= subset.feerate);
1063
        // - No non-empty intersection between the intersection and a prefix of the chunks of the
1064
        //   remainder of the linearization may be better than the intersection.
1065
0
        TestBitSet prefix;
1066
0
        for (DepGraphIndex i = 0; i < chunking.NumChunksLeft(); ++i) {
1067
0
            prefix |= chunking.GetChunk(i).transactions;
1068
0
            auto reintersect = SetInfo(depgraph, prefix & intersect.transactions);
1069
0
            if (!reintersect.feerate.IsEmpty()) {
1070
0
                assert(reintersect.feerate <= intersect.feerate);
1071
0
            }
1072
0
        }
1073
1074
        // Find a non-empty topologically valid subset of transactions to remove from the graph.
1075
        // Using an empty set would mean the next iteration is identical to the current one, and
1076
        // could cause an infinite loop.
1077
0
        auto done = ReadTopologicalSet(depgraph, todo, reader, /*non_empty=*/true);
1078
0
        todo -= done;
1079
0
        chunking.MarkDone(done);
1080
0
        subset = SetInfo(depgraph, subset.transactions - done);
1081
0
    }
1082
1083
0
    assert(chunking.NumChunksLeft() == 0);
1084
0
}
1085
1086
FUZZ_TARGET(clusterlin_simple_linearize)
1087
0
{
1088
    // Verify the behavior of SimpleLinearize(). Note that SimpleLinearize is only used in tests;
1089
    // the purpose of this fuzz test is to establish confidence in SimpleLinearize, so that it can
1090
    // be used to test the real Linearize function in the fuzz test below.
1091
1092
    // Retrieve an iteration count and a depgraph from the fuzz input.
1093
0
    SpanReader reader(buffer);
1094
0
    uint64_t iter_count{0};
1095
0
    DepGraph<TestBitSet> depgraph;
1096
0
    try {
1097
0
        reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph);
1098
0
    } catch (const std::ios_base::failure&) {}
1099
0
    iter_count %= MAX_SIMPLE_ITERATIONS;
1100
1101
    // Invoke SimpleLinearize().
1102
0
    auto [linearization, optimal] = SimpleLinearize(depgraph, iter_count);
1103
0
    SanityCheck(depgraph, linearization);
1104
0
    auto simple_chunking = ChunkLinearization(depgraph, linearization);
1105
1106
    // If the iteration count is sufficiently high, an optimal linearization must be found.
1107
    // SimpleLinearize on k transactions can take up to 2^(k-1) iterations (one per non-empty
1108
    // connected topologically valid subset), which sums over k=1..n to (2^n)-1.
1109
0
    const uint64_t n = depgraph.TxCount();
1110
0
    if (n <= 63 && (iter_count >> n)) {
1111
0
        assert(optimal);
1112
0
    }
1113
1114
    // If SimpleLinearize claims optimal result, and the cluster is sufficiently small (there are
1115
    // n! linearizations), test that the result is as good as every valid linearization.
1116
0
    if (optimal && depgraph.TxCount() <= 8) {
1117
0
        auto exh_linearization = ExhaustiveLinearize(depgraph);
1118
0
        auto exh_chunking = ChunkLinearization(depgraph, exh_linearization);
1119
0
        auto cmp = CompareChunks(simple_chunking, exh_chunking);
1120
0
        assert(cmp == 0);
1121
0
        assert(simple_chunking.size() == exh_chunking.size());
1122
0
    }
1123
1124
0
    if (optimal) {
1125
        // Compare with a linearization read from the fuzz input.
1126
0
        auto read = ReadLinearization(depgraph, reader);
1127
0
        auto read_chunking = ChunkLinearization(depgraph, read);
1128
0
        auto cmp = CompareChunks(simple_chunking, read_chunking);
1129
0
        assert(cmp >= 0);
1130
0
    }
1131
0
}
1132
1133
FUZZ_TARGET(clusterlin_linearize)
1134
0
{
1135
    // Verify the behavior of Linearize().
1136
1137
    // Retrieve an RNG seed, an iteration count, a depgraph, and whether to make it connected from
1138
    // the fuzz input.
1139
0
    SpanReader reader(buffer);
1140
0
    DepGraph<TestBitSet> depgraph;
1141
0
    uint64_t rng_seed{0};
1142
0
    uint64_t iter_count{0};
1143
0
    uint8_t make_connected{1};
1144
0
    try {
1145
0
        reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph) >> rng_seed >> make_connected;
1146
0
    } catch (const std::ios_base::failure&) {}
1147
    // The most complicated graphs are connected ones (other ones just split up). Optionally force
1148
    // the graph to be connected.
1149
0
    if (make_connected) MakeConnected(depgraph);
1150
1151
    // Optionally construct an old linearization for it.
1152
0
    std::vector<DepGraphIndex> old_linearization;
1153
0
    {
1154
0
        uint8_t have_old_linearization{0};
1155
0
        try {
1156
0
            reader >> have_old_linearization;
1157
0
        } catch(const std::ios_base::failure&) {}
1158
0
        if (have_old_linearization & 1) {
1159
0
            old_linearization = ReadLinearization(depgraph, reader);
1160
0
            SanityCheck(depgraph, old_linearization);
1161
0
        }
1162
0
    }
1163
1164
    // Invoke Linearize().
1165
0
    iter_count &= 0x7ffff;
1166
0
    auto [linearization, optimal, cost] = Linearize(depgraph, iter_count, rng_seed, old_linearization);
1167
0
    assert(cost <= iter_count);
1168
0
    SanityCheck(depgraph, linearization);
1169
0
    auto chunking = ChunkLinearization(depgraph, linearization);
1170
1171
    // Linearization must always be as good as the old one, if provided.
1172
0
    if (!old_linearization.empty()) {
1173
0
        auto old_chunking = ChunkLinearization(depgraph, old_linearization);
1174
0
        auto cmp = CompareChunks(chunking, old_chunking);
1175
0
        assert(cmp >= 0);
1176
0
    }
1177
1178
    // If the iteration count is sufficiently high, an optimal linearization must be found.
1179
0
    if (iter_count >= MaxOptimalLinearizationIters(depgraph.TxCount())) {
1180
0
        assert(optimal);
1181
0
    }
1182
1183
    // If Linearize claims optimal result, run quality tests.
1184
0
    if (optimal) {
1185
        // It must be as good as SimpleLinearize.
1186
0
        auto [simple_linearization, simple_optimal] = SimpleLinearize(depgraph, MAX_SIMPLE_ITERATIONS);
1187
0
        SanityCheck(depgraph, simple_linearization);
1188
0
        auto simple_chunking = ChunkLinearization(depgraph, simple_linearization);
1189
0
        auto cmp = CompareChunks(chunking, simple_chunking);
1190
0
        assert(cmp >= 0);
1191
        // If SimpleLinearize finds the optimal result too, they must be equal (if not,
1192
        // SimpleLinearize is broken).
1193
0
        if (simple_optimal) assert(cmp == 0);
1194
        // If simple_chunking is diagram-optimal, it cannot have more chunks than chunking (as
1195
        // chunking is claimed to be optimal, which implies minimal chunks).
1196
0
        if (cmp == 0) assert(chunking.size() >= simple_chunking.size());
1197
1198
        // Compare with a linearization read from the fuzz input.
1199
0
        auto read = ReadLinearization(depgraph, reader);
1200
0
        auto read_chunking = ChunkLinearization(depgraph, read);
1201
0
        auto cmp_read = CompareChunks(chunking, read_chunking);
1202
0
        assert(cmp_read >= 0);
1203
0
    }
1204
0
}
1205
1206
FUZZ_TARGET(clusterlin_postlinearize)
1207
0
{
1208
    // Verify expected properties of PostLinearize() on arbitrary linearizations.
1209
1210
    // Retrieve a depgraph from the fuzz input.
1211
0
    SpanReader reader(buffer);
1212
0
    DepGraph<TestBitSet> depgraph;
1213
0
    try {
1214
0
        reader >> Using<DepGraphFormatter>(depgraph);
1215
0
    } catch (const std::ios_base::failure&) {}
1216
1217
    // Retrieve a linearization from the fuzz input.
1218
0
    std::vector<DepGraphIndex> linearization;
1219
0
    linearization = ReadLinearization(depgraph, reader);
1220
0
    SanityCheck(depgraph, linearization);
1221
1222
    // Produce a post-processed version.
1223
0
    auto post_linearization = linearization;
1224
0
    PostLinearize(depgraph, post_linearization);
1225
0
    SanityCheck(depgraph, post_linearization);
1226
1227
    // Compare diagrams: post-linearization cannot worsen anywhere.
1228
0
    auto chunking = ChunkLinearization(depgraph, linearization);
1229
0
    auto post_chunking = ChunkLinearization(depgraph, post_linearization);
1230
0
    auto cmp = CompareChunks(post_chunking, chunking);
1231
0
    assert(cmp >= 0);
1232
1233
    // Run again, things can keep improving (and never get worse)
1234
0
    auto post_post_linearization = post_linearization;
1235
0
    PostLinearize(depgraph, post_post_linearization);
1236
0
    SanityCheck(depgraph, post_post_linearization);
1237
0
    auto post_post_chunking = ChunkLinearization(depgraph, post_post_linearization);
1238
0
    cmp = CompareChunks(post_post_chunking, post_chunking);
1239
0
    assert(cmp >= 0);
1240
1241
    // The chunks that come out of postlinearizing are always connected.
1242
0
    LinearizationChunking linchunking(depgraph, post_linearization);
1243
0
    while (linchunking.NumChunksLeft()) {
1244
0
        assert(depgraph.IsConnected(linchunking.GetChunk(0).transactions));
1245
0
        linchunking.MarkDone(linchunking.GetChunk(0).transactions);
1246
0
    }
1247
0
}
1248
1249
FUZZ_TARGET(clusterlin_postlinearize_tree)
1250
0
{
1251
    // Verify expected properties of PostLinearize() on linearizations of graphs that form either
1252
    // an upright or reverse tree structure.
1253
1254
    // Construct a direction, RNG seed, and an arbitrary graph from the fuzz input.
1255
0
    SpanReader reader(buffer);
1256
0
    uint64_t rng_seed{0};
1257
0
    DepGraph<TestBitSet> depgraph_gen;
1258
0
    uint8_t direction{0};
1259
0
    try {
1260
0
        reader >> direction >> rng_seed >> Using<DepGraphFormatter>(depgraph_gen);
1261
0
    } catch (const std::ios_base::failure&) {}
1262
1263
    // Now construct a new graph, copying the nodes, but leaving only the first parent (even
1264
    // direction) or the first child (odd direction).
1265
0
    DepGraph<TestBitSet> depgraph_tree;
1266
0
    for (DepGraphIndex i = 0; i < depgraph_gen.PositionRange(); ++i) {
1267
0
        if (depgraph_gen.Positions()[i]) {
1268
0
            depgraph_tree.AddTransaction(depgraph_gen.FeeRate(i));
1269
0
        } else {
1270
            // For holes, add a dummy transaction which is deleted below, so that non-hole
1271
            // transactions retain their position.
1272
0
            depgraph_tree.AddTransaction(FeeFrac{});
1273
0
        }
1274
0
    }
1275
0
    depgraph_tree.RemoveTransactions(TestBitSet::Fill(depgraph_gen.PositionRange()) - depgraph_gen.Positions());
1276
1277
0
    if (direction & 1) {
1278
0
        for (DepGraphIndex i = 0; i < depgraph_gen.TxCount(); ++i) {
1279
0
            auto children = depgraph_gen.GetReducedChildren(i);
1280
0
            if (children.Any()) {
1281
0
                depgraph_tree.AddDependencies(TestBitSet::Singleton(i), children.First());
1282
0
            }
1283
0
         }
1284
0
    } else {
1285
0
        for (DepGraphIndex i = 0; i < depgraph_gen.TxCount(); ++i) {
1286
0
            auto parents = depgraph_gen.GetReducedParents(i);
1287
0
            if (parents.Any()) {
1288
0
                depgraph_tree.AddDependencies(TestBitSet::Singleton(parents.First()), i);
1289
0
            }
1290
0
        }
1291
0
    }
1292
1293
    // Retrieve a linearization from the fuzz input.
1294
0
    std::vector<DepGraphIndex> linearization;
1295
0
    linearization = ReadLinearization(depgraph_tree, reader);
1296
0
    SanityCheck(depgraph_tree, linearization);
1297
1298
    // Produce a postlinearized version.
1299
0
    auto post_linearization = linearization;
1300
0
    PostLinearize(depgraph_tree, post_linearization);
1301
0
    SanityCheck(depgraph_tree, post_linearization);
1302
1303
    // Compare diagrams.
1304
0
    auto chunking = ChunkLinearization(depgraph_tree, linearization);
1305
0
    auto post_chunking = ChunkLinearization(depgraph_tree, post_linearization);
1306
0
    auto cmp = CompareChunks(post_chunking, chunking);
1307
0
    assert(cmp >= 0);
1308
1309
    // Verify that post-linearizing again does not change the diagram. The result must be identical
1310
    // as post_linearization ought to be optimal already with a tree-structured graph.
1311
0
    auto post_post_linearization = post_linearization;
1312
0
    PostLinearize(depgraph_tree, post_linearization);
1313
0
    SanityCheck(depgraph_tree, post_linearization);
1314
0
    auto post_post_chunking = ChunkLinearization(depgraph_tree, post_post_linearization);
1315
0
    auto cmp_post = CompareChunks(post_post_chunking, post_chunking);
1316
0
    assert(cmp_post == 0);
1317
1318
    // Try to find an even better linearization directly. This must not change the diagram for the
1319
    // same reason.
1320
0
    auto [opt_linearization, _optimal, _cost] = Linearize(depgraph_tree, 100000, rng_seed, post_linearization);
1321
0
    auto opt_chunking = ChunkLinearization(depgraph_tree, opt_linearization);
1322
0
    auto cmp_opt = CompareChunks(opt_chunking, post_chunking);
1323
0
    assert(cmp_opt == 0);
1324
0
}
1325
1326
FUZZ_TARGET(clusterlin_postlinearize_moved_leaf)
1327
0
{
1328
    // Verify that taking an existing linearization, and moving a leaf to the back, potentially
1329
    // increasing its fee, and then post-linearizing, results in something as good as the
1330
    // original. This guarantees that in an RBF that replaces a transaction with one of the same
1331
    // size but higher fee, applying the "remove conflicts, append new transaction, postlinearize"
1332
    // process will never worsen linearization quality.
1333
1334
    // Construct an arbitrary graph and a fee from the fuzz input.
1335
0
    SpanReader reader(buffer);
1336
0
    DepGraph<TestBitSet> depgraph;
1337
0
    int32_t fee_inc{0};
1338
0
    try {
1339
0
        uint64_t fee_inc_code;
1340
0
        reader >> Using<DepGraphFormatter>(depgraph) >> VARINT(fee_inc_code);
1341
0
        fee_inc = fee_inc_code & 0x3ffff;
1342
0
    } catch (const std::ios_base::failure&) {}
1343
0
    if (depgraph.TxCount() == 0) return;
1344
1345
    // Retrieve two linearizations from the fuzz input.
1346
0
    auto lin = ReadLinearization(depgraph, reader);
1347
0
    auto lin_leaf = ReadLinearization(depgraph, reader);
1348
1349
    // Construct a linearization identical to lin, but with the tail end of lin_leaf moved to the
1350
    // back.
1351
0
    std::vector<DepGraphIndex> lin_moved;
1352
0
    for (auto i : lin) {
1353
0
        if (i != lin_leaf.back()) lin_moved.push_back(i);
1354
0
    }
1355
0
    lin_moved.push_back(lin_leaf.back());
1356
1357
    // Postlinearize lin_moved.
1358
0
    PostLinearize(depgraph, lin_moved);
1359
0
    SanityCheck(depgraph, lin_moved);
1360
1361
    // Compare diagrams (applying the fee delta after computing the old one).
1362
0
    auto old_chunking = ChunkLinearization(depgraph, lin);
1363
0
    depgraph.FeeRate(lin_leaf.back()).fee += fee_inc;
1364
0
    auto new_chunking = ChunkLinearization(depgraph, lin_moved);
1365
0
    auto cmp = CompareChunks(new_chunking, old_chunking);
1366
0
    assert(cmp >= 0);
1367
0
}
1368
1369
FUZZ_TARGET(clusterlin_merge)
1370
0
{
1371
    // Construct an arbitrary graph from the fuzz input.
1372
0
    SpanReader reader(buffer);
1373
0
    DepGraph<TestBitSet> depgraph;
1374
0
    try {
1375
0
        reader >> Using<DepGraphFormatter>(depgraph);
1376
0
    } catch (const std::ios_base::failure&) {}
1377
1378
    // Retrieve two linearizations from the fuzz input.
1379
0
    auto lin1 = ReadLinearization(depgraph, reader);
1380
0
    auto lin2 = ReadLinearization(depgraph, reader);
1381
1382
    // Merge the two.
1383
0
    auto lin_merged = MergeLinearizations(depgraph, lin1, lin2);
1384
1385
    // Compute chunkings and compare.
1386
0
    auto chunking1 = ChunkLinearization(depgraph, lin1);
1387
0
    auto chunking2 = ChunkLinearization(depgraph, lin2);
1388
0
    auto chunking_merged = ChunkLinearization(depgraph, lin_merged);
1389
0
    auto cmp1 = CompareChunks(chunking_merged, chunking1);
1390
0
    assert(cmp1 >= 0);
1391
0
    auto cmp2 = CompareChunks(chunking_merged, chunking2);
1392
0
    assert(cmp2 >= 0);
1393
0
}
1394
1395
FUZZ_TARGET(clusterlin_fix_linearization)
1396
0
{
1397
    // Verify expected properties of FixLinearization() on arbitrary linearizations.
1398
1399
    // Retrieve a depgraph from the fuzz input.
1400
0
    SpanReader reader(buffer);
1401
0
    DepGraph<TestBitSet> depgraph;
1402
0
    try {
1403
0
        reader >> Using<DepGraphFormatter>(depgraph);
1404
0
    } catch (const std::ios_base::failure&) {}
1405
1406
    // Construct an arbitrary linearization (not necessarily topological for depgraph).
1407
0
    std::vector<DepGraphIndex> linearization;
1408
    /** Which transactions of depgraph are yet to be included in linearization. */
1409
0
    TestBitSet todo = depgraph.Positions();
1410
0
    while (todo.Any()) {
1411
        // Read a number from the fuzz input in range [0, todo.Count()).
1412
0
        uint64_t val{0};
1413
0
        try {
1414
0
            reader >> VARINT(val);
1415
0
        } catch (const std::ios_base::failure&) {}
1416
0
        val %= todo.Count();
1417
        // Find the val'th element in todo, remove it from todo, and append it to linearization.
1418
0
        for (auto idx : todo) {
1419
0
            if (val == 0) {
1420
0
                linearization.push_back(idx);
1421
0
                todo.Reset(idx);
1422
0
                break;
1423
0
            }
1424
0
            --val;
1425
0
        }
1426
0
    }
1427
0
    assert(linearization.size() == depgraph.TxCount());
1428
1429
    // Determine what prefix of linearization is topological, i.e., the position of the first entry
1430
    // in linearization which corresponds to a transaction that is not preceded by all its
1431
    // ancestors.
1432
0
    size_t topo_prefix = 0;
1433
0
    todo = depgraph.Positions();
1434
0
    while (topo_prefix < linearization.size()) {
1435
0
        DepGraphIndex idx = linearization[topo_prefix];
1436
0
        todo.Reset(idx);
1437
0
        if (todo.Overlaps(depgraph.Ancestors(idx))) break;
1438
0
        ++topo_prefix;
1439
0
    }
1440
1441
    // Then make a fixed copy of linearization.
1442
0
    auto linearization_fixed = linearization;
1443
0
    FixLinearization(depgraph, linearization_fixed);
1444
    // Sanity check it (which includes testing whether it is topological).
1445
0
    SanityCheck(depgraph, linearization_fixed);
1446
1447
    // FixLinearization does not modify the topological prefix of linearization.
1448
0
    assert(std::equal(linearization.begin(), linearization.begin() + topo_prefix,
1449
0
                      linearization_fixed.begin()));
1450
    // This also means that if linearization was entirely topological, FixLinearization cannot have
1451
    // modified it. This is implied by the assertion above already, but repeat it explicitly.
1452
0
    if (topo_prefix == linearization.size()) {
1453
0
        assert(linearization == linearization_fixed);
1454
0
    }
1455
0
}