Bitcoin Core 30.99.0
P2P Digital Currency
txgraph.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>
7#include <test/fuzz/fuzz.h>
9#include <test/util/random.h>
10#include <txgraph.h>
11#include <util/bitset.h>
12#include <util/feefrac.h>
13
14#include <algorithm>
15#include <cstdint>
16#include <iterator>
17#include <map>
18#include <memory>
19#include <set>
20#include <utility>
21
22using namespace cluster_linearize;
23
24namespace {
25
29struct SimTxGraph
30{
34 static constexpr unsigned MAX_TRANSACTIONS = MAX_CLUSTER_COUNT_LIMIT * 2;
36 using SetType = BitSet<MAX_TRANSACTIONS>;
38 using Pos = DepGraphIndex;
40 static constexpr auto MISSING = Pos(-1);
41
48 std::array<std::shared_ptr<TxGraph::Ref>, MAX_TRANSACTIONS> simmap;
50 std::map<const TxGraph::Ref*, Pos> simrevmap;
52 std::vector<std::shared_ptr<TxGraph::Ref>> removed;
54 std::optional<bool> oversized;
56 DepGraphIndex max_cluster_count;
59 SetType modified;
61 uint64_t max_cluster_size;
63 bool real_is_optimal{false};
64
66 explicit SimTxGraph(DepGraphIndex cluster_count, uint64_t cluster_size) :
67 max_cluster_count(cluster_count), max_cluster_size(cluster_size) {}
68
69 // Permit copying and moving.
70 SimTxGraph(const SimTxGraph&) noexcept = default;
71 SimTxGraph& operator=(const SimTxGraph&) noexcept = default;
72 SimTxGraph(SimTxGraph&&) noexcept = default;
73 SimTxGraph& operator=(SimTxGraph&&) noexcept = default;
74
76 std::vector<SetType> GetComponents()
77 {
78 auto todo = graph.Positions();
79 std::vector<SetType> ret;
80 // Iterate over all connected components of the graph.
81 while (todo.Any()) {
82 auto component = graph.FindConnectedComponent(todo);
83 ret.push_back(component);
84 todo -= component;
85 }
86 return ret;
87 }
88
91 bool IsOversized()
92 {
93 if (!oversized.has_value()) {
94 // Only recompute when oversized isn't already known.
95 oversized = false;
96 for (auto component : GetComponents()) {
97 if (component.Count() > max_cluster_count) oversized = true;
98 uint64_t component_size{0};
99 for (auto i : component) component_size += graph.FeeRate(i).size;
100 if (component_size > max_cluster_size) oversized = true;
101 }
102 }
103 return *oversized;
104 }
105
106 void MakeModified(DepGraphIndex index)
107 {
108 modified |= graph.GetConnectedComponent(graph.Positions(), index);
109 }
110
112 DepGraphIndex GetTransactionCount() const { return graph.TxCount(); }
113
115 FeePerWeight SumAll() const
116 {
118 for (auto i : graph.Positions()) {
119 ret += graph.FeeRate(i);
120 }
121 return ret;
122 }
123
125 Pos Find(const TxGraph::Ref* ref) const
126 {
127 auto it = simrevmap.find(ref);
128 if (it != simrevmap.end()) return it->second;
129 return MISSING;
130 }
131
133 TxGraph::Ref* GetRef(Pos pos)
134 {
135 assert(graph.Positions()[pos]);
136 assert(simmap[pos]);
137 return simmap[pos].get();
138 }
139
141 void AddTransaction(TxGraph::Ref&& ref, const FeePerWeight& feerate)
142 {
143 assert(graph.TxCount() < MAX_TRANSACTIONS);
144 auto simpos = graph.AddTransaction(feerate);
145 real_is_optimal = false;
146 MakeModified(simpos);
147 assert(graph.Positions()[simpos]);
148 simmap[simpos] = std::make_shared<TxGraph::Ref>(std::move(ref));
149 auto ptr = simmap[simpos].get();
150 simrevmap[ptr] = simpos;
151 // This may invalidate our cached oversized value.
152 if (oversized.has_value() && !*oversized) oversized = std::nullopt;
153 }
154
156 void AddDependency(TxGraph::Ref* parent, TxGraph::Ref* child)
157 {
158 auto par_pos = Find(parent);
159 if (par_pos == MISSING) return;
160 auto chl_pos = Find(child);
161 if (chl_pos == MISSING) return;
162 graph.AddDependencies(SetType::Singleton(par_pos), chl_pos);
163 MakeModified(par_pos);
164 real_is_optimal = false;
165 // This may invalidate our cached oversized value.
166 if (oversized.has_value() && !*oversized) oversized = std::nullopt;
167 }
168
170 void SetTransactionFee(TxGraph::Ref* ref, int64_t fee)
171 {
172 auto pos = Find(ref);
173 if (pos == MISSING) return;
174 // No need to invoke MakeModified, because this equally affects main and staging.
175 real_is_optimal = false;
176 graph.FeeRate(pos).fee = fee;
177 }
178
180 void RemoveTransaction(TxGraph::Ref* ref)
181 {
182 auto pos = Find(ref);
183 if (pos == MISSING) return;
184 MakeModified(pos);
185 real_is_optimal = false;
186 graph.RemoveTransactions(SetType::Singleton(pos));
187 simrevmap.erase(simmap[pos].get());
188 // Retain the TxGraph::Ref corresponding to this position, so the Ref destruction isn't
189 // invoked until the simulation explicitly decided to do so.
190 removed.push_back(std::move(simmap[pos]));
191 simmap[pos].reset();
192 // This may invalidate our cached oversized value.
193 if (oversized.has_value() && *oversized) oversized = std::nullopt;
194 }
195
200 void DestroyTransaction(TxGraph::Ref* ref, bool reset_oversize)
201 {
202 auto pos = Find(ref);
203 if (pos == MISSING) {
204 // Wipe the ref, if it exists, from the removed vector. Use std::partition rather
205 // than std::erase because we don't care about the order of the entries that
206 // remain.
207 auto remove = std::partition(removed.begin(), removed.end(), [&](auto& arg) { return arg.get() != ref; });
208 removed.erase(remove, removed.end());
209 } else {
210 MakeModified(pos);
211 graph.RemoveTransactions(SetType::Singleton(pos));
212 real_is_optimal = false;
213 simrevmap.erase(simmap[pos].get());
214 simmap[pos].reset();
215 // This may invalidate our cached oversized value.
216 if (reset_oversize && oversized.has_value() && *oversized) {
217 oversized = std::nullopt;
218 }
219 }
220 }
221
224 SetType MakeSet(std::span<TxGraph::Ref* const> arg)
225 {
226 SetType ret;
227 for (TxGraph::Ref* ptr : arg) {
228 auto pos = Find(ptr);
229 assert(pos != Pos(-1));
230 ret.Set(pos);
231 }
232 return ret;
233 }
234
236 SetType GetAncDesc(TxGraph::Ref* arg, bool desc)
237 {
238 auto pos = Find(arg);
239 if (pos == MISSING) return {};
240 return desc ? graph.Descendants(pos) : graph.Ancestors(pos);
241 }
242
245 void IncludeAncDesc(std::vector<TxGraph::Ref*>& arg, bool desc)
246 {
247 std::vector<TxGraph::Ref*> ret;
248 for (auto ptr : arg) {
249 auto simpos = Find(ptr);
250 if (simpos != MISSING) {
251 for (auto i : desc ? graph.Descendants(simpos) : graph.Ancestors(simpos)) {
252 ret.push_back(simmap[i].get());
253 }
254 } else {
255 ret.push_back(ptr);
256 }
257 }
258 // Construct deduplicated version in input (do not use std::sort/std::unique for
259 // deduplication as it'd rely on non-deterministic pointer comparison).
260 arg.clear();
261 for (auto ptr : ret) {
262 if (std::find(arg.begin(), arg.end(), ptr) == arg.end()) {
263 arg.push_back(ptr);
264 }
265 }
266 }
267
268
271 bool MatchesOversizedClusters(const SetType& set)
272 {
273 if (set.Any() && !IsOversized()) return false;
274
275 auto todo = graph.Positions();
276 if (!set.IsSubsetOf(todo)) return false;
277
278 // Walk all clusters, and make sure all of set doesn't come from non-oversized clusters
279 while (todo.Any()) {
280 auto component = graph.FindConnectedComponent(todo);
281 // Determine whether component is oversized, due to either the size or count limit.
282 bool is_oversized = component.Count() > max_cluster_count;
283 uint64_t component_size{0};
284 for (auto i : component) component_size += graph.FeeRate(i).size;
285 is_oversized |= component_size > max_cluster_size;
286 // Check whether overlap with set matches is_oversized.
287 if (is_oversized != set.Overlaps(component)) return false;
288 todo -= component;
289 }
290 return true;
291 }
292};
293
294} // namespace
295
297{
298 // This is a big simulation test for TxGraph, which performs a fuzz-derived sequence of valid
299 // operations on a TxGraph instance, as well as on a simpler (mostly) reimplementation (see
300 // SimTxGraph above), comparing the outcome of functions that return a result, and finally
301 // performing a full comparison between the two.
302
304 FuzzedDataProvider provider(buffer.data(), buffer.size());
305
309 InsecureRandomContext rng(provider.ConsumeIntegral<uint64_t>());
310
312 TxGraph::Ref empty_ref;
313
316 auto max_cluster_count = provider.ConsumeIntegralInRange<DepGraphIndex>(1, MAX_CLUSTER_COUNT_LIMIT);
318 auto max_cluster_size = provider.ConsumeIntegralInRange<uint64_t>(1, 0x3fffff * MAX_CLUSTER_COUNT_LIMIT);
320 auto acceptable_iters = provider.ConsumeIntegralInRange<uint64_t>(0, 10000);
321
322 // Construct a real graph, and a vector of simulated graphs (main, and possibly staging).
323 auto real = MakeTxGraph(max_cluster_count, max_cluster_size, acceptable_iters);
324 std::vector<SimTxGraph> sims;
325 sims.reserve(2);
326 sims.emplace_back(max_cluster_count, max_cluster_size);
327
329 struct BlockBuilderData
330 {
332 std::unique_ptr<TxGraph::BlockBuilder> builder;
334 SimTxGraph::SetType included;
336 SimTxGraph::SetType done;
338 FeePerWeight last_feerate;
339
340 BlockBuilderData(std::unique_ptr<TxGraph::BlockBuilder> builder_in) : builder(std::move(builder_in)) {}
341 };
342
344 std::vector<BlockBuilderData> block_builders;
345
348 auto pick_fn = [&]() noexcept -> TxGraph::Ref* {
349 size_t tx_count[2] = {sims[0].GetTransactionCount(), 0};
351 size_t choices = tx_count[0] + sims[0].removed.size() + 1;
352 if (sims.size() == 2) {
353 tx_count[1] = sims[1].GetTransactionCount();
354 choices += tx_count[1] + sims[1].removed.size();
355 }
357 auto choice = provider.ConsumeIntegralInRange<size_t>(0, choices - 1);
358 // Consider both main and (if it exists) staging.
359 for (size_t level = 0; level < sims.size(); ++level) {
360 auto& sim = sims[level];
361 if (choice < tx_count[level]) {
362 // Return from graph.
363 for (auto i : sim.graph.Positions()) {
364 if (choice == 0) return sim.GetRef(i);
365 --choice;
366 }
367 assert(false);
368 } else {
369 choice -= tx_count[level];
370 }
371 if (choice < sim.removed.size()) {
372 // Return from removed.
373 return sim.removed[choice].get();
374 } else {
375 choice -= sim.removed.size();
376 }
377 }
378 // Return empty.
379 assert(choice == 0);
380 return &empty_ref;
381 };
382
385 auto get_diagram_fn = [&](TxGraph::Level level_select) -> std::vector<FeeFrac> {
386 int level = level_select == TxGraph::Level::MAIN ? 0 : sims.size() - 1;
387 auto& sim = sims[level];
388 // For every transaction in the graph, request its cluster, and throw them into a set.
389 std::set<std::vector<TxGraph::Ref*>> clusters;
390 for (auto i : sim.graph.Positions()) {
391 auto ref = sim.GetRef(i);
392 clusters.insert(real->GetCluster(*ref, level_select));
393 }
394 // Compute the chunkings of each (deduplicated) cluster.
395 size_t num_tx{0};
396 std::vector<FeeFrac> chunk_feerates;
397 for (const auto& cluster : clusters) {
398 num_tx += cluster.size();
399 std::vector<SimTxGraph::Pos> linearization;
400 linearization.reserve(cluster.size());
401 for (auto refptr : cluster) linearization.push_back(sim.Find(refptr));
402 for (const FeeFrac& chunk_feerate : ChunkLinearization(sim.graph, linearization)) {
403 chunk_feerates.push_back(chunk_feerate);
404 }
405 }
406 // Verify the number of transactions after deduplicating clusters. This implicitly verifies
407 // that GetCluster on each element of a cluster reports the cluster transactions in the same
408 // order.
409 assert(num_tx == sim.GetTransactionCount());
410 // Sort by feerate only, since violating topological constraints within same-feerate
411 // chunks won't affect diagram comparisons.
412 std::sort(chunk_feerates.begin(), chunk_feerates.end(), std::greater{});
413 return chunk_feerates;
414 };
415
416 LIMITED_WHILE(provider.remaining_bytes() > 0, 200) {
417 // Read a one-byte command.
418 int command = provider.ConsumeIntegral<uint8_t>();
419 int orig_command = command;
420
421 // Treat the lowest bit of a command as a flag (which selects a variant of some of the
422 // operations), and the second-lowest bit as a way of selecting main vs. staging, and leave
423 // the rest of the bits in command.
424 bool alt = command & 1;
426 command >>= 2;
427
431 int builder_idx = block_builders.empty() ? -1 : int((orig_command & 3) % block_builders.size());
432
433 // Provide convenient aliases for the top simulated graph (main, or staging if it exists),
434 // one for the simulated graph selected based on level_select (for operations that can operate
435 // on both graphs), and one that always refers to the main graph.
436 auto& top_sim = sims.back();
437 auto& sel_sim = level_select == TxGraph::Level::MAIN ? sims[0] : top_sim;
438 auto& main_sim = sims[0];
439
440 // Keep decrementing command for each applicable operation, until one is hit. Multiple
441 // iterations may be necessary.
442 while (true) {
443 if ((block_builders.empty() || sims.size() > 1) && top_sim.GetTransactionCount() < SimTxGraph::MAX_TRANSACTIONS && command-- == 0) {
444 // AddTransaction.
445 int64_t fee;
446 int32_t size;
447 if (alt) {
448 // If alt is true, pick fee and size from the entire range.
449 fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
450 size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
451 } else {
452 // Otherwise, use smaller range which consume fewer fuzz input bytes, as just
453 // these are likely sufficient to trigger all interesting code paths already.
454 fee = provider.ConsumeIntegral<uint8_t>();
455 size = provider.ConsumeIntegralInRange<uint32_t>(1, 0xff);
456 }
457 FeePerWeight feerate{fee, size};
458 // Create a real TxGraph::Ref.
459 auto ref = real->AddTransaction(feerate);
460 // Create a shared_ptr place in the simulation to put the Ref in.
461 top_sim.AddTransaction(std::move(ref), feerate);
462 break;
463 } else if ((block_builders.empty() || sims.size() > 1) && top_sim.GetTransactionCount() + top_sim.removed.size() > 1 && command-- == 0) {
464 // AddDependency.
465 auto par = pick_fn();
466 auto chl = pick_fn();
467 auto pos_par = top_sim.Find(par);
468 auto pos_chl = top_sim.Find(chl);
469 if (pos_par != SimTxGraph::MISSING && pos_chl != SimTxGraph::MISSING) {
470 // Determine if adding this would introduce a cycle (not allowed by TxGraph),
471 // and if so, skip.
472 if (top_sim.graph.Ancestors(pos_par)[pos_chl]) break;
473 }
474 top_sim.AddDependency(par, chl);
475 top_sim.real_is_optimal = false;
476 real->AddDependency(*par, *chl);
477 break;
478 } else if ((block_builders.empty() || sims.size() > 1) && top_sim.removed.size() < 100 && command-- == 0) {
479 // RemoveTransaction. Either all its ancestors or all its descendants are also
480 // removed (if any), to make sure TxGraph's reordering of removals and dependencies
481 // has no effect.
482 std::vector<TxGraph::Ref*> to_remove;
483 to_remove.push_back(pick_fn());
484 top_sim.IncludeAncDesc(to_remove, alt);
485 // The order in which these ancestors/descendants are removed should not matter;
486 // randomly shuffle them.
487 std::shuffle(to_remove.begin(), to_remove.end(), rng);
488 for (TxGraph::Ref* ptr : to_remove) {
489 real->RemoveTransaction(*ptr);
490 top_sim.RemoveTransaction(ptr);
491 }
492 break;
493 } else if (sel_sim.removed.size() > 0 && command-- == 0) {
494 // ~Ref (of an already-removed transaction). Destroying a TxGraph::Ref has an
495 // observable effect on the TxGraph it refers to, so this simulation permits doing
496 // so separately from other actions on TxGraph.
497
498 // Pick a Ref of sel_sim.removed to destroy. Note that the same Ref may still occur
499 // in the other graph, and thus not actually trigger ~Ref yet (which is exactly
500 // what we want, as destroying Refs is only allowed when it does not refer to an
501 // existing transaction in either graph).
502 auto removed_pos = provider.ConsumeIntegralInRange<size_t>(0, sel_sim.removed.size() - 1);
503 if (removed_pos != sel_sim.removed.size() - 1) {
504 std::swap(sel_sim.removed[removed_pos], sel_sim.removed.back());
505 }
506 sel_sim.removed.pop_back();
507 break;
508 } else if (block_builders.empty() && command-- == 0) {
509 // ~Ref (of any transaction).
510 std::vector<TxGraph::Ref*> to_destroy;
511 to_destroy.push_back(pick_fn());
512 while (true) {
513 // Keep adding either the ancestors or descendants the already picked
514 // transactions have in both graphs (main and staging) combined. Destroying
515 // will trigger deletions in both, so to have consistent TxGraph behavior, the
516 // set must be closed under ancestors, or descendants, in both graphs.
517 auto old_size = to_destroy.size();
518 for (auto& sim : sims) sim.IncludeAncDesc(to_destroy, alt);
519 if (to_destroy.size() == old_size) break;
520 }
521 // The order in which these ancestors/descendants are destroyed should not matter;
522 // randomly shuffle them.
523 std::shuffle(to_destroy.begin(), to_destroy.end(), rng);
524 for (TxGraph::Ref* ptr : to_destroy) {
525 for (size_t level = 0; level < sims.size(); ++level) {
526 sims[level].DestroyTransaction(ptr, level == sims.size() - 1);
527 }
528 }
529 break;
530 } else if (block_builders.empty() && command-- == 0) {
531 // SetTransactionFee.
532 int64_t fee;
533 if (alt) {
534 fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
535 } else {
536 fee = provider.ConsumeIntegral<uint8_t>();
537 }
538 auto ref = pick_fn();
539 real->SetTransactionFee(*ref, fee);
540 for (auto& sim : sims) {
541 sim.SetTransactionFee(ref, fee);
542 }
543 break;
544 } else if (command-- == 0) {
545 // GetTransactionCount.
546 assert(real->GetTransactionCount(level_select) == sel_sim.GetTransactionCount());
547 break;
548 } else if (command-- == 0) {
549 // Exists.
550 auto ref = pick_fn();
551 bool exists = real->Exists(*ref, level_select);
552 bool should_exist = sel_sim.Find(ref) != SimTxGraph::MISSING;
553 assert(exists == should_exist);
554 break;
555 } else if (command-- == 0) {
556 // IsOversized.
557 assert(sel_sim.IsOversized() == real->IsOversized(level_select));
558 break;
559 } else if (command-- == 0) {
560 // GetIndividualFeerate.
561 auto ref = pick_fn();
562 auto feerate = real->GetIndividualFeerate(*ref);
563 bool found{false};
564 for (auto& sim : sims) {
565 auto simpos = sim.Find(ref);
566 if (simpos != SimTxGraph::MISSING) {
567 found = true;
568 assert(feerate == sim.graph.FeeRate(simpos));
569 }
570 }
571 if (!found) assert(feerate.IsEmpty());
572 break;
573 } else if (!main_sim.IsOversized() && command-- == 0) {
574 // GetMainChunkFeerate.
575 auto ref = pick_fn();
576 auto feerate = real->GetMainChunkFeerate(*ref);
577 auto simpos = main_sim.Find(ref);
578 if (simpos == SimTxGraph::MISSING) {
579 assert(feerate.IsEmpty());
580 } else {
581 // Just do some quick checks that the reported value is in range. A full
582 // recomputation of expected chunk feerates is done at the end.
583 assert(feerate.size >= main_sim.graph.FeeRate(simpos).size);
584 assert(feerate.size <= main_sim.SumAll().size);
585 }
586 break;
587 } else if (!sel_sim.IsOversized() && command-- == 0) {
588 // GetAncestors/GetDescendants.
589 auto ref = pick_fn();
590 auto result = alt ? real->GetDescendants(*ref, level_select)
591 : real->GetAncestors(*ref, level_select);
592 assert(result.size() <= max_cluster_count);
593 auto result_set = sel_sim.MakeSet(result);
594 assert(result.size() == result_set.Count());
595 auto expect_set = sel_sim.GetAncDesc(ref, alt);
596 assert(result_set == expect_set);
597 break;
598 } else if (!sel_sim.IsOversized() && command-- == 0) {
599 // GetAncestorsUnion/GetDescendantsUnion.
600 std::vector<TxGraph::Ref*> refs;
601 // Gather a list of up to 15 Ref pointers.
602 auto count = provider.ConsumeIntegralInRange<size_t>(0, 15);
603 refs.resize(count);
604 for (size_t i = 0; i < count; ++i) {
605 refs[i] = pick_fn();
606 }
607 // Their order should not matter, shuffle them.
608 std::shuffle(refs.begin(), refs.end(), rng);
609 // Invoke the real function, and convert to SimPos set.
610 auto result = alt ? real->GetDescendantsUnion(refs, level_select)
611 : real->GetAncestorsUnion(refs, level_select);
612 auto result_set = sel_sim.MakeSet(result);
613 assert(result.size() == result_set.Count());
614 // Compute the expected result.
615 SimTxGraph::SetType expect_set;
616 for (TxGraph::Ref* ref : refs) expect_set |= sel_sim.GetAncDesc(ref, alt);
617 // Compare.
618 assert(result_set == expect_set);
619 break;
620 } else if (!sel_sim.IsOversized() && command-- == 0) {
621 // GetCluster.
622 auto ref = pick_fn();
623 auto result = real->GetCluster(*ref, level_select);
624 // Check cluster count limit.
625 assert(result.size() <= max_cluster_count);
626 // Require the result to be topologically valid and not contain duplicates.
627 auto left = sel_sim.graph.Positions();
628 uint64_t total_size{0};
629 for (auto refptr : result) {
630 auto simpos = sel_sim.Find(refptr);
631 total_size += sel_sim.graph.FeeRate(simpos).size;
632 assert(simpos != SimTxGraph::MISSING);
633 assert(left[simpos]);
634 left.Reset(simpos);
635 assert(!sel_sim.graph.Ancestors(simpos).Overlaps(left));
636 }
637 // Check cluster size limit.
638 assert(total_size <= max_cluster_size);
639 // Require the set to be connected.
640 auto result_set = sel_sim.MakeSet(result);
641 assert(sel_sim.graph.IsConnected(result_set));
642 // If ref exists, the result must contain it. If not, it must be empty.
643 auto simpos = sel_sim.Find(ref);
644 if (simpos != SimTxGraph::MISSING) {
645 assert(result_set[simpos]);
646 } else {
647 assert(result_set.None());
648 }
649 // Require the set not to have ancestors or descendants outside of it.
650 for (auto i : result_set) {
651 assert(sel_sim.graph.Ancestors(i).IsSubsetOf(result_set));
652 assert(sel_sim.graph.Descendants(i).IsSubsetOf(result_set));
653 }
654 break;
655 } else if (command-- == 0) {
656 // HaveStaging.
657 assert((sims.size() == 2) == real->HaveStaging());
658 break;
659 } else if (sims.size() < 2 && command-- == 0) {
660 // StartStaging.
661 sims.emplace_back(sims.back());
662 sims.back().modified = SimTxGraph::SetType{};
663 real->StartStaging();
664 break;
665 } else if (block_builders.empty() && sims.size() > 1 && command-- == 0) {
666 // CommitStaging.
667 real->CommitStaging();
668 // Resulting main level is only guaranteed to be optimal if all levels are
669 const bool main_optimal = std::all_of(sims.cbegin(), sims.cend(), [](const auto &sim) { return sim.real_is_optimal; });
670 sims.erase(sims.begin());
671 sims.front().real_is_optimal = main_optimal;
672 break;
673 } else if (sims.size() > 1 && command-- == 0) {
674 // AbortStaging.
675 real->AbortStaging();
676 sims.pop_back();
677 // Reset the cached oversized value (if TxGraph::Ref destructions triggered
678 // removals of main transactions while staging was active, then aborting will
679 // cause it to be re-evaluated in TxGraph).
680 sims.back().oversized = std::nullopt;
681 break;
682 } else if (!main_sim.IsOversized() && command-- == 0) {
683 // CompareMainOrder.
684 auto ref_a = pick_fn();
685 auto ref_b = pick_fn();
686 auto sim_a = main_sim.Find(ref_a);
687 auto sim_b = main_sim.Find(ref_b);
688 // Both transactions must exist in the main graph.
689 if (sim_a == SimTxGraph::MISSING || sim_b == SimTxGraph::MISSING) break;
690 auto cmp = real->CompareMainOrder(*ref_a, *ref_b);
691 // Distinct transactions have distinct places.
692 if (sim_a != sim_b) assert(cmp != 0);
693 // Ancestors go before descendants.
694 if (main_sim.graph.Ancestors(sim_a)[sim_b]) assert(cmp >= 0);
695 if (main_sim.graph.Descendants(sim_a)[sim_b]) assert(cmp <= 0);
696 // Do not verify consistency with chunk feerates, as we cannot easily determine
697 // these here without making more calls to real, which could affect its internal
698 // state. A full comparison is done at the end.
699 break;
700 } else if (!sel_sim.IsOversized() && command-- == 0) {
701 // CountDistinctClusters.
702 std::vector<TxGraph::Ref*> refs;
703 // Gather a list of up to 15 (or up to 255) Ref pointers.
704 auto count = provider.ConsumeIntegralInRange<size_t>(0, alt ? 255 : 15);
705 refs.resize(count);
706 for (size_t i = 0; i < count; ++i) {
707 refs[i] = pick_fn();
708 }
709 // Their order should not matter, shuffle them.
710 std::shuffle(refs.begin(), refs.end(), rng);
711 // Invoke the real function.
712 auto result = real->CountDistinctClusters(refs, level_select);
713 // Build a set with representatives of the clusters the Refs occur in in the
714 // simulated graph. For each, remember the lowest-index transaction SimPos in the
715 // cluster.
716 SimTxGraph::SetType sim_reps;
717 for (auto ref : refs) {
718 // Skip Refs that do not occur in the simulated graph.
719 auto simpos = sel_sim.Find(ref);
720 if (simpos == SimTxGraph::MISSING) continue;
721 // Find the component that includes ref.
722 auto component = sel_sim.graph.GetConnectedComponent(sel_sim.graph.Positions(), simpos);
723 // Remember the lowest-index SimPos in component, as a representative for it.
724 assert(component.Any());
725 sim_reps.Set(component.First());
726 }
727 // Compare the number of deduplicated representatives with the value returned by
728 // the real function.
729 assert(result == sim_reps.Count());
730 break;
731 } else if (command-- == 0) {
732 // DoWork.
733 uint64_t iters = provider.ConsumeIntegralInRange<uint64_t>(0, alt ? 10000 : 255);
734 bool ret = real->DoWork(iters);
735 uint64_t iters_for_optimal{0};
736 for (unsigned level = 0; level < sims.size(); ++level) {
737 // DoWork() will not optimize oversized levels, or the main level if a builder
738 // is present. Note that this impacts the DoWork() return value, as true means
739 // that non-optimal clusters may remain within such oversized or builder-having
740 // levels.
741 if (sims[level].IsOversized()) continue;
742 if (level == 0 && !block_builders.empty()) continue;
743 // If neither of the two above conditions holds, and DoWork() returned true,
744 // then the level is optimal.
745 if (ret) {
746 sims[level].real_is_optimal = true;
747 }
748 // Compute how many iterations would be needed to make everything optimal.
749 for (auto component : sims[level].GetComponents()) {
750 auto iters_opt_this_cluster = MaxOptimalLinearizationIters(component.Count());
751 if (iters_opt_this_cluster > acceptable_iters) {
752 // If the number of iterations required to linearize this cluster
753 // optimally exceeds acceptable_iters, DoWork() may process it in two
754 // stages: once to acceptable, and once to optimal.
755 iters_for_optimal += iters_opt_this_cluster + acceptable_iters;
756 } else {
757 iters_for_optimal += iters_opt_this_cluster;
758 }
759 }
760 }
761 if (!ret) {
762 // DoWork can only have more work left if the requested number of iterations
763 // was insufficient to linearize everything optimally within the levels it is
764 // allowed to touch.
765 assert(iters <= iters_for_optimal);
766 }
767 break;
768 } else if (sims.size() == 2 && !sims[0].IsOversized() && !sims[1].IsOversized() && command-- == 0) {
769 // GetMainStagingDiagrams()
770 auto [real_main_diagram, real_staged_diagram] = real->GetMainStagingDiagrams();
771 auto real_sum_main = std::accumulate(real_main_diagram.begin(), real_main_diagram.end(), FeeFrac{});
772 auto real_sum_staged = std::accumulate(real_staged_diagram.begin(), real_staged_diagram.end(), FeeFrac{});
773 auto real_gain = real_sum_staged - real_sum_main;
774 auto sim_gain = sims[1].SumAll() - sims[0].SumAll();
775 // Just check that the total fee gained/lost and size gained/lost according to the
776 // diagram matches the difference in these values in the simulated graph. A more
777 // complete check of the GetMainStagingDiagrams result is performed at the end.
778 assert(sim_gain == real_gain);
779 // Check that the feerates in each diagram are monotonically decreasing.
780 for (size_t i = 1; i < real_main_diagram.size(); ++i) {
781 assert(FeeRateCompare(real_main_diagram[i], real_main_diagram[i - 1]) <= 0);
782 }
783 for (size_t i = 1; i < real_staged_diagram.size(); ++i) {
784 assert(FeeRateCompare(real_staged_diagram[i], real_staged_diagram[i - 1]) <= 0);
785 }
786 break;
787 } else if (block_builders.size() < 4 && !main_sim.IsOversized() && command-- == 0) {
788 // GetBlockBuilder.
789 block_builders.emplace_back(real->GetBlockBuilder());
790 break;
791 } else if (!block_builders.empty() && command-- == 0) {
792 // ~BlockBuilder.
793 block_builders.erase(block_builders.begin() + builder_idx);
794 break;
795 } else if (!block_builders.empty() && command-- == 0) {
796 // BlockBuilder::GetCurrentChunk, followed by Include/Skip.
797 auto& builder_data = block_builders[builder_idx];
798 auto new_included = builder_data.included;
799 auto new_done = builder_data.done;
800 auto chunk = builder_data.builder->GetCurrentChunk();
801 if (chunk) {
802 // Chunk feerates must be monotonously decreasing.
803 if (!builder_data.last_feerate.IsEmpty()) {
804 assert(!(chunk->second >> builder_data.last_feerate));
805 }
806 builder_data.last_feerate = chunk->second;
807 // Verify the contents of GetCurrentChunk.
808 FeePerWeight sum_feerate;
809 for (TxGraph::Ref* ref : chunk->first) {
810 // Each transaction in the chunk must exist in the main graph.
811 auto simpos = main_sim.Find(ref);
812 assert(simpos != SimTxGraph::MISSING);
813 // Verify the claimed chunk feerate.
814 sum_feerate += main_sim.graph.FeeRate(simpos);
815 // Make sure no transaction is reported twice.
816 assert(!new_done[simpos]);
817 new_done.Set(simpos);
818 // The concatenation of all included transactions must be topologically valid.
819 new_included.Set(simpos);
820 assert(main_sim.graph.Ancestors(simpos).IsSubsetOf(new_included));
821 }
822 assert(sum_feerate == chunk->second);
823 } else {
824 // When we reach the end, if nothing was skipped, the entire graph should have
825 // been reported.
826 if (builder_data.done == builder_data.included) {
827 assert(builder_data.done.Count() == main_sim.GetTransactionCount());
828 }
829 }
830 // Possibly invoke GetCurrentChunk() again, which should give the same result.
831 if ((orig_command % 7) >= 5) {
832 auto chunk2 = builder_data.builder->GetCurrentChunk();
833 assert(chunk == chunk2);
834 }
835 // Skip or include.
836 if ((orig_command % 5) >= 3) {
837 // Skip.
838 builder_data.builder->Skip();
839 } else {
840 // Include.
841 builder_data.builder->Include();
842 builder_data.included = new_included;
843 }
844 builder_data.done = new_done;
845 break;
846 } else if (!main_sim.IsOversized() && command-- == 0) {
847 // GetWorstMainChunk.
848 auto [worst_chunk, worst_chunk_feerate] = real->GetWorstMainChunk();
849 // Just do some sanity checks here. Consistency with GetBlockBuilder is checked
850 // below.
851 if (main_sim.GetTransactionCount() == 0) {
852 assert(worst_chunk.empty());
853 assert(worst_chunk_feerate.IsEmpty());
854 } else {
855 assert(!worst_chunk.empty());
856 SimTxGraph::SetType done;
858 for (TxGraph::Ref* ref : worst_chunk) {
859 // Each transaction in the chunk must exist in the main graph.
860 auto simpos = main_sim.Find(ref);
861 assert(simpos != SimTxGraph::MISSING);
862 sum += main_sim.graph.FeeRate(simpos);
863 // Make sure the chunk contains no duplicate transactions.
864 assert(!done[simpos]);
865 done.Set(simpos);
866 // All elements are preceded by all their descendants.
867 assert(main_sim.graph.Descendants(simpos).IsSubsetOf(done));
868 }
869 assert(sum == worst_chunk_feerate);
870 }
871 break;
872 } else if ((block_builders.empty() || sims.size() > 1) && command-- == 0) {
873 // Trim.
874 bool was_oversized = top_sim.IsOversized();
875 auto removed = real->Trim();
876 // Verify that something was removed if and only if there was an oversized cluster.
877 assert(was_oversized == !removed.empty());
878 if (!was_oversized) break;
879 auto removed_set = top_sim.MakeSet(removed);
880 // The removed set must contain all its own descendants.
881 for (auto simpos : removed_set) {
882 assert(top_sim.graph.Descendants(simpos).IsSubsetOf(removed_set));
883 }
884 // Something from every oversized cluster should have been removed, and nothing
885 // else.
886 assert(top_sim.MatchesOversizedClusters(removed_set));
887
888 // Apply all removals to the simulation, and verify the result is no longer
889 // oversized. Don't query the real graph for oversizedness; it is compared
890 // against the simulation anyway later.
891 for (auto simpos : removed_set) {
892 top_sim.RemoveTransaction(top_sim.GetRef(simpos));
893 }
894 assert(!top_sim.IsOversized());
895 break;
896 } else if ((block_builders.empty() || sims.size() > 1) &&
897 top_sim.GetTransactionCount() > max_cluster_count && !top_sim.IsOversized() && command-- == 0) {
898 // Trim (special case which avoids apparent cycles in the implicit approximate
899 // dependency graph constructed inside the Trim() implementation). This is worth
900 // testing separately, because such cycles cannot occur in realistic scenarios,
901 // but this is hard to replicate in general in this fuzz test.
902
903 // First, we need to have dependencies applied and linearizations fixed to avoid
904 // circular dependencies in implied graph; trigger it via whatever means.
905 real->CountDistinctClusters({}, TxGraph::Level::TOP);
906
907 // Gather the current clusters.
908 auto clusters = top_sim.GetComponents();
909
910 // Merge clusters randomly until at least one oversized one appears.
911 bool made_oversized = false;
912 auto merges_left = clusters.size() - 1;
913 while (merges_left > 0) {
914 --merges_left;
915 // Find positions of clusters in the clusters vector to merge together.
916 auto par_cl = rng.randrange(clusters.size());
917 auto chl_cl = rng.randrange(clusters.size() - 1);
918 chl_cl += (chl_cl >= par_cl);
919 Assume(chl_cl != par_cl);
920 // Add between 1 and 3 dependencies between them. As all are in the same
921 // direction (from the child cluster to parent cluster), no cycles are possible,
922 // regardless of what internal topology Trim() uses as approximation within the
923 // clusters.
924 int num_deps = rng.randrange(3) + 1;
925 for (int i = 0; i < num_deps; ++i) {
926 // Find a parent transaction in the parent cluster.
927 auto par_idx = rng.randrange(clusters[par_cl].Count());
928 SimTxGraph::Pos par_pos = 0;
929 for (auto j : clusters[par_cl]) {
930 if (par_idx == 0) {
931 par_pos = j;
932 break;
933 }
934 --par_idx;
935 }
936 // Find a child transaction in the child cluster.
937 auto chl_idx = rng.randrange(clusters[chl_cl].Count());
938 SimTxGraph::Pos chl_pos = 0;
939 for (auto j : clusters[chl_cl]) {
940 if (chl_idx == 0) {
941 chl_pos = j;
942 break;
943 }
944 --chl_idx;
945 }
946 // Add dependency to both simulation and real TxGraph.
947 auto par_ref = top_sim.GetRef(par_pos);
948 auto chl_ref = top_sim.GetRef(chl_pos);
949 top_sim.AddDependency(par_ref, chl_ref);
950 real->AddDependency(*par_ref, *chl_ref);
951 }
952 // Compute the combined cluster.
953 auto par_cluster = clusters[par_cl];
954 auto chl_cluster = clusters[chl_cl];
955 auto new_cluster = par_cluster | chl_cluster;
956 // Remove the parent and child cluster from clusters.
957 std::erase_if(clusters, [&](const auto& cl) noexcept { return cl == par_cluster || cl == chl_cluster; });
958 // Add the combined cluster.
959 clusters.push_back(new_cluster);
960 // If this is the first merge that causes an oversized cluster to appear, pick
961 // a random number of further merges to appear.
962 if (!made_oversized) {
963 made_oversized = new_cluster.Count() > max_cluster_count;
964 if (!made_oversized) {
965 FeeFrac total;
966 for (auto i : new_cluster) total += top_sim.graph.FeeRate(i);
967 if (uint32_t(total.size) > max_cluster_size) made_oversized = true;
968 }
969 if (made_oversized) merges_left = rng.randrange(clusters.size());
970 }
971 }
972
973 // Determine an upper bound on how many transactions are removed.
974 uint32_t max_removed = 0;
975 for (auto& cluster : clusters) {
976 // Gather all transaction sizes in the to-be-combined cluster.
977 std::vector<uint32_t> sizes;
978 for (auto i : cluster) sizes.push_back(top_sim.graph.FeeRate(i).size);
979 auto sum_sizes = std::accumulate(sizes.begin(), sizes.end(), uint64_t{0});
980 // Sort from large to small.
981 std::sort(sizes.begin(), sizes.end(), std::greater{});
982 // In the worst case, only the smallest transactions are removed.
983 while (sizes.size() > max_cluster_count || sum_sizes > max_cluster_size) {
984 sum_sizes -= sizes.back();
985 sizes.pop_back();
986 ++max_removed;
987 }
988 }
989
990 // Invoke Trim now on the definitely-oversized txgraph.
991 auto removed = real->Trim();
992 // Verify that the number of removals is within range.
993 assert(removed.size() >= 1);
994 assert(removed.size() <= max_removed);
995 // The removed set must contain all its own descendants.
996 auto removed_set = top_sim.MakeSet(removed);
997 for (auto simpos : removed_set) {
998 assert(top_sim.graph.Descendants(simpos).IsSubsetOf(removed_set));
999 }
1000 // Something from every oversized cluster should have been removed, and nothing
1001 // else.
1002 assert(top_sim.MatchesOversizedClusters(removed_set));
1003
1004 // Apply all removals to the simulation, and verify the result is no longer
1005 // oversized. Don't query the real graph for oversizedness; it is compared
1006 // against the simulation anyway later.
1007 for (auto simpos : removed_set) {
1008 top_sim.RemoveTransaction(top_sim.GetRef(simpos));
1009 }
1010 assert(!top_sim.IsOversized());
1011 break;
1012 } else if (command-- == 0) {
1013 // GetMainMemoryUsage().
1014 auto usage = real->GetMainMemoryUsage();
1015 // Test stability.
1016 if (alt) {
1017 auto usage2 = real->GetMainMemoryUsage();
1018 assert(usage == usage2);
1019 }
1020 // Only empty graphs have 0 memory usage.
1021 if (main_sim.GetTransactionCount() == 0) {
1022 assert(usage == 0);
1023 } else {
1024 assert(usage > 0);
1025 }
1026 break;
1027 }
1028 }
1029 }
1030
1031 // After running all modifications, perform an internal sanity check (before invoking
1032 // inspectors that may modify the internal state).
1033 real->SanityCheck();
1034
1035 if (!sims[0].IsOversized()) {
1036 // If the main graph is not oversized, verify the total ordering implied by
1037 // CompareMainOrder.
1038 // First construct two distinct randomized permutations of the positions in sims[0].
1039 std::vector<SimTxGraph::Pos> vec1;
1040 for (auto i : sims[0].graph.Positions()) vec1.push_back(i);
1041 std::shuffle(vec1.begin(), vec1.end(), rng);
1042 auto vec2 = vec1;
1043 std::shuffle(vec2.begin(), vec2.end(), rng);
1044 if (vec1 == vec2) std::next_permutation(vec2.begin(), vec2.end());
1045 // Sort both according to CompareMainOrder. By having randomized starting points, the order
1046 // of CompareMainOrder invocations is somewhat randomized as well.
1047 auto cmp = [&](SimTxGraph::Pos a, SimTxGraph::Pos b) noexcept {
1048 return real->CompareMainOrder(*sims[0].GetRef(a), *sims[0].GetRef(b)) < 0;
1049 };
1050 std::sort(vec1.begin(), vec1.end(), cmp);
1051 std::sort(vec2.begin(), vec2.end(), cmp);
1052
1053 // Verify the resulting orderings are identical. This could only fail if the ordering was
1054 // not total.
1055 assert(vec1 == vec2);
1056
1057 // Verify that the ordering is topological.
1058 auto todo = sims[0].graph.Positions();
1059 for (auto i : vec1) {
1060 todo.Reset(i);
1061 assert(!sims[0].graph.Ancestors(i).Overlaps(todo));
1062 }
1063 assert(todo.None());
1064
1065 // If the real graph claims to be optimal (the last DoWork() call returned true), verify
1066 // that calling Linearize on it does not improve it further.
1067 if (sims[0].real_is_optimal) {
1068 auto real_diagram = ChunkLinearization(sims[0].graph, vec1);
1069 auto [sim_lin, _optimal, _cost] = Linearize(sims[0].graph, 300000, rng.rand64(), vec1);
1070 auto sim_diagram = ChunkLinearization(sims[0].graph, sim_lin);
1071 auto cmp = CompareChunks(real_diagram, sim_diagram);
1072 assert(cmp == 0);
1073 }
1074
1075 // For every transaction in the total ordering, find a random one before it and after it,
1076 // and compare their chunk feerates, which must be consistent with the ordering.
1077 for (size_t pos = 0; pos < vec1.size(); ++pos) {
1078 auto pos_feerate = real->GetMainChunkFeerate(*sims[0].GetRef(vec1[pos]));
1079 if (pos > 0) {
1080 size_t before = rng.randrange<size_t>(pos);
1081 auto before_feerate = real->GetMainChunkFeerate(*sims[0].GetRef(vec1[before]));
1082 assert(FeeRateCompare(before_feerate, pos_feerate) >= 0);
1083 }
1084 if (pos + 1 < vec1.size()) {
1085 size_t after = pos + 1 + rng.randrange<size_t>(vec1.size() - 1 - pos);
1086 auto after_feerate = real->GetMainChunkFeerate(*sims[0].GetRef(vec1[after]));
1087 assert(FeeRateCompare(after_feerate, pos_feerate) <= 0);
1088 }
1089 }
1090
1091 // The same order should be obtained through a BlockBuilder as implied by CompareMainOrder,
1092 // if nothing is skipped.
1093 auto builder = real->GetBlockBuilder();
1094 std::vector<SimTxGraph::Pos> vec_builder;
1095 std::vector<TxGraph::Ref*> last_chunk;
1096 FeePerWeight last_chunk_feerate;
1097 while (auto chunk = builder->GetCurrentChunk()) {
1099 for (TxGraph::Ref* ref : chunk->first) {
1100 // The reported chunk feerate must match the chunk feerate obtained by asking
1101 // it for each of the chunk's transactions individually.
1102 assert(real->GetMainChunkFeerate(*ref) == chunk->second);
1103 // Verify the chunk feerate matches the sum of the reported individual feerates.
1104 sum += real->GetIndividualFeerate(*ref);
1105 // Chunks must contain transactions that exist in the graph.
1106 auto simpos = sims[0].Find(ref);
1107 assert(simpos != SimTxGraph::MISSING);
1108 vec_builder.push_back(simpos);
1109 }
1110 assert(sum == chunk->second);
1111 last_chunk = std::move(chunk->first);
1112 last_chunk_feerate = chunk->second;
1113 builder->Include();
1114 }
1115 assert(vec_builder == vec1);
1116
1117 // The last chunk returned by the BlockBuilder must match GetWorstMainChunk, in reverse.
1118 std::reverse(last_chunk.begin(), last_chunk.end());
1119 auto [worst_chunk, worst_chunk_feerate] = real->GetWorstMainChunk();
1120 assert(last_chunk == worst_chunk);
1121 assert(last_chunk_feerate == worst_chunk_feerate);
1122
1123 // Check that the implied ordering gives rise to a combined diagram that matches the
1124 // diagram constructed from the individual cluster linearization chunkings.
1125 auto main_real_diagram = get_diagram_fn(TxGraph::Level::MAIN);
1126 auto main_implied_diagram = ChunkLinearization(sims[0].graph, vec1);
1127 assert(CompareChunks(main_real_diagram, main_implied_diagram) == 0);
1128
1129 if (sims.size() >= 2 && !sims[1].IsOversized()) {
1130 // When the staging graph is not oversized as well, call GetMainStagingDiagrams, and
1131 // fully verify the result.
1132 auto [main_cmp_diagram, stage_cmp_diagram] = real->GetMainStagingDiagrams();
1133 // Check that the feerates in each diagram are monotonically decreasing.
1134 for (size_t i = 1; i < main_cmp_diagram.size(); ++i) {
1135 assert(FeeRateCompare(main_cmp_diagram[i], main_cmp_diagram[i - 1]) <= 0);
1136 }
1137 for (size_t i = 1; i < stage_cmp_diagram.size(); ++i) {
1138 assert(FeeRateCompare(stage_cmp_diagram[i], stage_cmp_diagram[i - 1]) <= 0);
1139 }
1140 // Treat the diagrams as sets of chunk feerates, and sort them in the same way so that
1141 // std::set_difference can be used on them below. The exact ordering does not matter
1142 // here, but it has to be consistent with the one used in main_real_diagram and
1143 // stage_real_diagram).
1144 std::sort(main_cmp_diagram.begin(), main_cmp_diagram.end(), std::greater{});
1145 std::sort(stage_cmp_diagram.begin(), stage_cmp_diagram.end(), std::greater{});
1146 // Find the chunks that appear in main_diagram but are missing from main_cmp_diagram.
1147 // This is allowed, because GetMainStagingDiagrams omits clusters in main unaffected
1148 // by staging.
1149 std::vector<FeeFrac> missing_main_cmp;
1150 std::set_difference(main_real_diagram.begin(), main_real_diagram.end(),
1151 main_cmp_diagram.begin(), main_cmp_diagram.end(),
1152 std::inserter(missing_main_cmp, missing_main_cmp.end()),
1153 std::greater{});
1154 assert(main_cmp_diagram.size() + missing_main_cmp.size() == main_real_diagram.size());
1155 // Do the same for chunks in stage_diagram missing from stage_cmp_diagram.
1156 auto stage_real_diagram = get_diagram_fn(TxGraph::Level::TOP);
1157 std::vector<FeeFrac> missing_stage_cmp;
1158 std::set_difference(stage_real_diagram.begin(), stage_real_diagram.end(),
1159 stage_cmp_diagram.begin(), stage_cmp_diagram.end(),
1160 std::inserter(missing_stage_cmp, missing_stage_cmp.end()),
1161 std::greater{});
1162 assert(stage_cmp_diagram.size() + missing_stage_cmp.size() == stage_real_diagram.size());
1163 // The missing chunks must be equal across main & staging (otherwise they couldn't have
1164 // been omitted).
1165 assert(missing_main_cmp == missing_stage_cmp);
1166
1167 // The missing part must include at least all transactions in staging which have not been
1168 // modified, or been in a cluster together with modified transactions, since they were
1169 // copied from main. Note that due to the reordering of removals w.r.t. dependency
1170 // additions, it is possible that the real implementation found more unaffected things.
1171 FeeFrac missing_real;
1172 for (const auto& feerate : missing_main_cmp) missing_real += feerate;
1173 FeeFrac missing_expected = sims[1].graph.FeeRate(sims[1].graph.Positions() - sims[1].modified);
1174 // Note that missing_real.fee < missing_expected.fee is possible to due the presence of
1175 // negative-fee transactions.
1176 assert(missing_real.size >= missing_expected.size);
1177 }
1178 }
1179
1180 assert(real->HaveStaging() == (sims.size() > 1));
1181
1182 // Try to run a full comparison, for both TxGraph::Level::MAIN and TxGraph::Level::TOP in
1183 // TxGraph inspector functions that support both.
1184 for (auto level : {TxGraph::Level::TOP, TxGraph::Level::MAIN}) {
1185 auto& sim = level == TxGraph::Level::TOP ? sims.back() : sims.front();
1186 // Compare simple properties of the graph with the simulation.
1187 assert(real->IsOversized(level) == sim.IsOversized());
1188 assert(real->GetTransactionCount(level) == sim.GetTransactionCount());
1189 // If the graph (and the simulation) are not oversized, perform a full comparison.
1190 if (!sim.IsOversized()) {
1191 auto todo = sim.graph.Positions();
1192 // Iterate over all connected components of the resulting (simulated) graph, each of which
1193 // should correspond to a cluster in the real one.
1194 while (todo.Any()) {
1195 auto component = sim.graph.FindConnectedComponent(todo);
1196 todo -= component;
1197 // Iterate over the transactions in that component.
1198 for (auto i : component) {
1199 // Check its individual feerate against simulation.
1200 assert(sim.graph.FeeRate(i) == real->GetIndividualFeerate(*sim.GetRef(i)));
1201 // Check its ancestors against simulation.
1202 auto expect_anc = sim.graph.Ancestors(i);
1203 auto anc = sim.MakeSet(real->GetAncestors(*sim.GetRef(i), level));
1204 assert(anc.Count() <= max_cluster_count);
1205 assert(anc == expect_anc);
1206 // Check its descendants against simulation.
1207 auto expect_desc = sim.graph.Descendants(i);
1208 auto desc = sim.MakeSet(real->GetDescendants(*sim.GetRef(i), level));
1209 assert(desc.Count() <= max_cluster_count);
1210 assert(desc == expect_desc);
1211 // Check the cluster the transaction is part of.
1212 auto cluster = real->GetCluster(*sim.GetRef(i), level);
1213 assert(cluster.size() <= max_cluster_count);
1214 assert(sim.MakeSet(cluster) == component);
1215 // Check that the cluster is reported in a valid topological order (its
1216 // linearization).
1217 std::vector<DepGraphIndex> simlin;
1218 SimTxGraph::SetType done;
1219 uint64_t total_size{0};
1220 for (TxGraph::Ref* ptr : cluster) {
1221 auto simpos = sim.Find(ptr);
1222 assert(sim.graph.Descendants(simpos).IsSubsetOf(component - done));
1223 done.Set(simpos);
1224 assert(sim.graph.Ancestors(simpos).IsSubsetOf(done));
1225 simlin.push_back(simpos);
1226 total_size += sim.graph.FeeRate(simpos).size;
1227 }
1228 // Check cluster size.
1229 assert(total_size <= max_cluster_size);
1230 // Construct a chunking object for the simulated graph, using the reported cluster
1231 // linearization as ordering, and compare it against the reported chunk feerates.
1232 if (sims.size() == 1 || level == TxGraph::Level::MAIN) {
1233 cluster_linearize::LinearizationChunking simlinchunk(sim.graph, simlin);
1234 DepGraphIndex idx{0};
1235 for (unsigned chunknum = 0; chunknum < simlinchunk.NumChunksLeft(); ++chunknum) {
1236 auto chunk = simlinchunk.GetChunk(chunknum);
1237 // Require that the chunks of cluster linearizations are connected (this must
1238 // be the case as all linearizations inside are PostLinearized).
1239 assert(sim.graph.IsConnected(chunk.transactions));
1240 // Check the chunk feerates of all transactions in the cluster.
1241 while (chunk.transactions.Any()) {
1242 assert(chunk.transactions[simlin[idx]]);
1243 chunk.transactions.Reset(simlin[idx]);
1244 assert(chunk.feerate == real->GetMainChunkFeerate(*cluster[idx]));
1245 ++idx;
1246 }
1247 }
1248 }
1249 }
1250 }
1251 }
1252 }
1253
1254 // Sanity check again (because invoking inspectors may modify internal unobservable state).
1255 real->SanityCheck();
1256
1257 // Kill the block builders.
1258 block_builders.clear();
1259 // Kill the TxGraph object.
1260 real.reset();
1261 // Kill the simulated graphs, with all remaining Refs in it. If any, this verifies that Refs
1262 // can outlive the TxGraph that created them.
1263 sims.clear();
1264}
int ret
const auto command
std::conditional_t<(BITS<=32), bitset_detail::IntBitSet< uint32_t >, std::conditional_t<(BITS<=std::numeric_limits< size_t >::digits), bitset_detail::IntBitSet< size_t >, bitset_detail::MultiIntBitSet< size_t,(BITS+std::numeric_limits< size_t >::digits - 1)/std::numeric_limits< size_t >::digits > > > BitSet
Definition: bitset.h:525
#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
I randrange(I range) noexcept
Generate a random integer in the range [0..range), with range > 0.
Definition: random.h:254
@ MAIN
Always refers to the main graph, whether staging is present or not.
@ TOP
Refers to staging if it exists, main otherwise.
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.
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).
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.
const SetType & Positions() const noexcept
Get the set of transactions positions in use.
void AddDependencies(const SetType &parents, DepGraphIndex child) noexcept
Modify this transaction graph, adding multiple parents to a specified child.
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.
constexpr MaybeCoin MISSING
volatile double sum
Definition: examples.cpp:10
#define LIMITED_WHILE(condition, limit)
Can be used to limit a theoretically unbounded loop.
Definition: fuzz.h:22
uint64_t fee
Value Find(const std::map< Key, Value > &map, const Key &key)
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.
uint32_t DepGraphIndex
Data type to represent transaction indices in DepGraphs and the clusters they represent.
Data structure storing a fee and size, ordered by increasing fee/size.
Definition: feefrac.h:40
int64_t fee
Definition: feefrac.h:107
int32_t size
Definition: feefrac.h:108
bool IsEmpty() const noexcept
Check if this is empty (size and fee are 0).
Definition: feefrac.h:120
Tagged wrapper around FeeFrac to avoid unit confusion.
Definition: feefrac.h:239
FUZZ_TARGET(txgraph)
Definition: txgraph.cpp:296
void SeedRandomStateForTest(SeedRand seedtype)
Seed the global RNG state for testing and log the seed value.
Definition: random.cpp:19
@ ZEROS
Seed with a compile time constant of zeros.
static int count
std::unique_ptr< TxGraph > MakeTxGraph(unsigned max_cluster_count, uint64_t max_cluster_size, uint64_t acceptable_iters) noexcept
Construct a new TxGraph with the specified limit on the number of transactions within a cluster,...
Definition: txgraph.cpp:3463
static constexpr unsigned MAX_CLUSTER_COUNT_LIMIT
Definition: txgraph.h:17
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())