1111#include < util/feefrac.h>
1212
1313#include < algorithm>
14+ #include < iterator>
1415#include < map>
1516#include < memory>
1617#include < set>
@@ -52,6 +53,9 @@ struct SimTxGraph
5253 std::optional<bool > oversized;
5354 /* * The configured maximum number of transactions per cluster. */
5455 DepGraphIndex max_cluster_count;
56+ /* * Which transactions have been modified in the graph since creation, either directly or by
57+ * being in a cluster which includes modifications. Only relevant for the staging graph. */
58+ SetType modified;
5559
5660 /* * Construct a new SimTxGraph with the specified maximum cluster count. */
5761 explicit SimTxGraph (DepGraphIndex max_cluster) : max_cluster_count(max_cluster) {}
@@ -80,9 +84,24 @@ struct SimTxGraph
8084 return *oversized;
8185 }
8286
87+ void MakeModified (DepGraphIndex index)
88+ {
89+ modified |= graph.GetConnectedComponent (graph.Positions (), index);
90+ }
91+
8392 /* * Determine the number of (non-removed) transactions in the graph. */
8493 DepGraphIndex GetTransactionCount () const { return graph.TxCount (); }
8594
95+ /* * Get the sum of all fees/sizes in the graph. */
96+ FeePerWeight SumAll () const
97+ {
98+ FeePerWeight ret;
99+ for (auto i : graph.Positions ()) {
100+ ret += graph.FeeRate (i);
101+ }
102+ return ret;
103+ }
104+
86105 /* * Get the position where ref occurs in this simulated graph, or -1 if it does not. */
87106 Pos Find (const TxGraph::Ref* ref) const
88107 {
@@ -104,6 +123,7 @@ struct SimTxGraph
104123 {
105124 assert (graph.TxCount () < MAX_TRANSACTIONS);
106125 auto simpos = graph.AddTransaction (feerate);
126+ MakeModified (simpos);
107127 assert (graph.Positions ()[simpos]);
108128 simmap[simpos] = std::make_shared<TxGraph::Ref>();
109129 auto ptr = simmap[simpos].get ();
@@ -119,6 +139,7 @@ struct SimTxGraph
119139 auto chl_pos = Find (child);
120140 if (chl_pos == MISSING) return ;
121141 graph.AddDependencies (SetType::Singleton (par_pos), chl_pos);
142+ MakeModified (par_pos);
122143 // This may invalidate our cached oversized value.
123144 if (oversized.has_value () && !*oversized) oversized = std::nullopt ;
124145 }
@@ -128,6 +149,7 @@ struct SimTxGraph
128149 {
129150 auto pos = Find (ref);
130151 if (pos == MISSING) return ;
152+ // No need to invoke MakeModified, because this equally affects main and staging.
131153 graph.FeeRate (pos).fee = fee;
132154 }
133155
@@ -136,6 +158,7 @@ struct SimTxGraph
136158 {
137159 auto pos = Find (ref);
138160 if (pos == MISSING) return ;
161+ MakeModified (pos);
139162 graph.RemoveTransactions (SetType::Singleton (pos));
140163 simrevmap.erase (simmap[pos].get ());
141164 // Retain the TxGraph::Ref corresponding to this position, so the Ref destruction isn't
@@ -160,6 +183,7 @@ struct SimTxGraph
160183 auto remove = std::partition (removed.begin (), removed.end (), [&](auto & arg) { return arg.get () != ref; });
161184 removed.erase (remove, removed.end ());
162185 } else {
186+ MakeModified (pos);
163187 graph.RemoveTransactions (SetType::Singleton (pos));
164188 simrevmap.erase (simmap[pos].get ());
165189 simmap[pos].reset ();
@@ -282,6 +306,39 @@ FUZZ_TARGET(txgraph)
282306 return &empty_ref;
283307 };
284308
309+ /* * Function to construct the correct fee-size diagram a real graph has based on its graph
310+ * order (as reported by GetCluster(), so it works for both main and staging). */
311+ auto get_diagram_fn = [&](bool main_only) -> std::vector<FeeFrac> {
312+ int level = main_only ? 0 : sims.size () - 1 ;
313+ auto & sim = sims[level];
314+ // For every transaction in the graph, request its cluster, and throw them into a set.
315+ std::set<std::vector<TxGraph::Ref*>> clusters;
316+ for (auto i : sim.graph .Positions ()) {
317+ auto ref = sim.GetRef (i);
318+ clusters.insert (real->GetCluster (*ref, main_only));
319+ }
320+ // Compute the chunkings of each (deduplicated) cluster.
321+ size_t num_tx{0 };
322+ std::vector<FeeFrac> chunk_feerates;
323+ for (const auto & cluster : clusters) {
324+ num_tx += cluster.size ();
325+ std::vector<SimTxGraph::Pos> linearization;
326+ linearization.reserve (cluster.size ());
327+ for (auto refptr : cluster) linearization.push_back (sim.Find (refptr));
328+ for (const FeeFrac& chunk_feerate : ChunkLinearization (sim.graph , linearization)) {
329+ chunk_feerates.push_back (chunk_feerate);
330+ }
331+ }
332+ // Verify the number of transactions after deduplicating clusters. This implicitly verifies
333+ // that GetCluster on each element of a cluster reports the cluster transactions in the same
334+ // order.
335+ assert (num_tx == sim.GetTransactionCount ());
336+ // Sort by feerate only, since violating topological constraints within same-feerate
337+ // chunks won't affect diagram comparisons.
338+ std::sort (chunk_feerates.begin (), chunk_feerates.end (), std::greater{});
339+ return chunk_feerates;
340+ };
341+
285342 LIMITED_WHILE (provider.remaining_bytes () > 0 , 200 ) {
286343 // Read a one-byte command.
287344 int command = provider.ConsumeIntegral <uint8_t >();
@@ -444,6 +501,7 @@ FUZZ_TARGET(txgraph)
444501 // Just do some quick checks that the reported value is in range. A full
445502 // recomputation of expected chunk feerates is done at the end.
446503 assert (feerate.size >= main_sim.graph .FeeRate (simpos).size );
504+ assert (feerate.size <= main_sim.SumAll ().size );
447505 }
448506 break ;
449507 } else if (!sel_sim.IsOversized () && command-- == 0 ) {
@@ -517,6 +575,7 @@ FUZZ_TARGET(txgraph)
517575 } else if (sims.size () < 2 && command-- == 0 ) {
518576 // StartStaging.
519577 sims.emplace_back (sims.back ());
578+ sims.back ().modified = SimTxGraph::SetType{};
520579 real->StartStaging ();
521580 break ;
522581 } else if (sims.size () > 1 && command-- == 0 ) {
@@ -586,6 +645,25 @@ FUZZ_TARGET(txgraph)
586645 // DoWork.
587646 real->DoWork ();
588647 break ;
648+ } else if (sims.size () == 2 && !sims[0 ].IsOversized () && !sims[1 ].IsOversized () && command-- == 0 ) {
649+ // GetMainStagingDiagrams()
650+ auto [real_main_diagram, real_staged_diagram] = real->GetMainStagingDiagrams ();
651+ auto real_sum_main = std::accumulate (real_main_diagram.begin (), real_main_diagram.end (), FeeFrac{});
652+ auto real_sum_staged = std::accumulate (real_staged_diagram.begin (), real_staged_diagram.end (), FeeFrac{});
653+ auto real_gain = real_sum_staged - real_sum_main;
654+ auto sim_gain = sims[1 ].SumAll () - sims[0 ].SumAll ();
655+ // Just check that the total fee gained/lost and size gained/lost according to the
656+ // diagram matches the difference in these values in the simulated graph. A more
657+ // complete check of the GetMainStagingDiagrams result is performed at the end.
658+ assert (sim_gain == real_gain);
659+ // Check that the feerates in each diagram are monotonically decreasing.
660+ for (size_t i = 1 ; i < real_main_diagram.size (); ++i) {
661+ assert (FeeRateCompare (real_main_diagram[i], real_main_diagram[i - 1 ]) <= 0 );
662+ }
663+ for (size_t i = 1 ; i < real_staged_diagram.size (); ++i) {
664+ assert (FeeRateCompare (real_staged_diagram[i], real_staged_diagram[i - 1 ]) <= 0 );
665+ }
666+ break ;
589667 }
590668 }
591669 }
@@ -639,6 +717,62 @@ FUZZ_TARGET(txgraph)
639717 assert (FeeRateCompare (after_feerate, pos_feerate) <= 0 );
640718 }
641719 }
720+
721+ // Check that the implied ordering gives rise to a combined diagram that matches the
722+ // diagram constructed from the individual cluster linearization chunkings.
723+ auto main_real_diagram = get_diagram_fn (/* main_only=*/ true );
724+ auto main_implied_diagram = ChunkLinearization (sims[0 ].graph , vec1);
725+ assert (CompareChunks (main_real_diagram, main_implied_diagram) == 0 );
726+
727+ if (sims.size () >= 2 && !sims[1 ].IsOversized ()) {
728+ // When the staging graph is not oversized as well, call GetMainStagingDiagrams, and
729+ // fully verify the result.
730+ auto [main_cmp_diagram, stage_cmp_diagram] = real->GetMainStagingDiagrams ();
731+ // Check that the feerates in each diagram are monotonically decreasing.
732+ for (size_t i = 1 ; i < main_cmp_diagram.size (); ++i) {
733+ assert (FeeRateCompare (main_cmp_diagram[i], main_cmp_diagram[i - 1 ]) <= 0 );
734+ }
735+ for (size_t i = 1 ; i < stage_cmp_diagram.size (); ++i) {
736+ assert (FeeRateCompare (stage_cmp_diagram[i], stage_cmp_diagram[i - 1 ]) <= 0 );
737+ }
738+ // Treat the diagrams as sets of chunk feerates, and sort them in the same way so that
739+ // std::set_difference can be used on them below. The exact ordering does not matter
740+ // here, but it has to be consistent with the one used in main_real_diagram and
741+ // stage_real_diagram).
742+ std::sort (main_cmp_diagram.begin (), main_cmp_diagram.end (), std::greater{});
743+ std::sort (stage_cmp_diagram.begin (), stage_cmp_diagram.end (), std::greater{});
744+ // Find the chunks that appear in main_diagram but are missing from main_cmp_diagram.
745+ // This is allowed, because GetMainStagingDiagrams omits clusters in main unaffected
746+ // by staging.
747+ std::vector<FeeFrac> missing_main_cmp;
748+ std::set_difference (main_real_diagram.begin (), main_real_diagram.end (),
749+ main_cmp_diagram.begin (), main_cmp_diagram.end (),
750+ std::inserter (missing_main_cmp, missing_main_cmp.end ()),
751+ std::greater{});
752+ assert (main_cmp_diagram.size () + missing_main_cmp.size () == main_real_diagram.size ());
753+ // Do the same for chunks in stage_diagram missing from stage_cmp_diagram.
754+ auto stage_real_diagram = get_diagram_fn (/* main_only=*/ false );
755+ std::vector<FeeFrac> missing_stage_cmp;
756+ std::set_difference (stage_real_diagram.begin (), stage_real_diagram.end (),
757+ stage_cmp_diagram.begin (), stage_cmp_diagram.end (),
758+ std::inserter (missing_stage_cmp, missing_stage_cmp.end ()),
759+ std::greater{});
760+ assert (stage_cmp_diagram.size () + missing_stage_cmp.size () == stage_real_diagram.size ());
761+ // The missing chunks must be equal across main & staging (otherwise they couldn't have
762+ // been omitted).
763+ assert (missing_main_cmp == missing_stage_cmp);
764+
765+ // The missing part must include at least all transactions in staging which have not been
766+ // modified, or been in a cluster together with modified transactions, since they were
767+ // copied from main. Note that due to the reordering of removals w.r.t. dependency
768+ // additions, it is possible that the real implementation found more unaffected things.
769+ FeeFrac missing_real;
770+ for (const auto & feerate : missing_main_cmp) missing_real += feerate;
771+ FeeFrac missing_expected = sims[1 ].graph .FeeRate (sims[1 ].graph .Positions () - sims[1 ].modified );
772+ // Note that missing_real.fee < missing_expected.fee is possible to due the presence of
773+ // negative-fee transactions.
774+ assert (missing_real.size >= missing_expected.size );
775+ }
642776 }
643777
644778 assert (real->HaveStaging () == (sims.size () > 1 ));
0 commit comments