Coverage Report

Created: 2026-10-07 20:27

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