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