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