@@ -69,22 +69,32 @@ struct SimTxGraph
6969 SimTxGraph (SimTxGraph&&) noexcept = default ;
7070 SimTxGraph& operator =(SimTxGraph&&) noexcept = default ;
7171
72+ /* * Get the connected components within this simulated transaction graph. */
73+ std::vector<SetType> GetComponents ()
74+ {
75+ auto todo = graph.Positions ();
76+ std::vector<SetType> ret;
77+ // Iterate over all connected components of the graph.
78+ while (todo.Any ()) {
79+ auto component = graph.FindConnectedComponent (todo);
80+ ret.push_back (component);
81+ todo -= component;
82+ }
83+ return ret;
84+ }
85+
7286 /* * Check whether this graph is oversized (contains a connected component whose number of
7387 * transactions exceeds max_cluster_count. */
7488 bool IsOversized ()
7589 {
7690 if (!oversized.has_value ()) {
7791 // Only recompute when oversized isn't already known.
7892 oversized = false ;
79- auto todo = graph.Positions ();
80- // Iterate over all connected components of the graph.
81- while (todo.Any ()) {
82- auto component = graph.FindConnectedComponent (todo);
93+ for (auto component : GetComponents ()) {
8394 if (component.Count () > max_cluster_count) oversized = true ;
8495 uint64_t component_size{0 };
8596 for (auto i : component) component_size += graph.FeeRate (i).size ;
8697 if (component_size > max_cluster_size) oversized = true ;
87- todo -= component;
8898 }
8999 }
90100 return *oversized;
@@ -287,8 +297,9 @@ FUZZ_TARGET(txgraph)
287297 FuzzedDataProvider provider (buffer.data (), buffer.size ());
288298
289299 /* * Internal test RNG, used only for decisions which would require significant amount of data
290- * to be read from the provider, without realistically impacting test sensitivity. */
291- InsecureRandomContext rng (0xdecade2009added + buffer.size ());
300+ * to be read from the provider, without realistically impacting test sensitivity, and for
301+ * specialized test cases that are hard to perform more generically. */
302+ InsecureRandomContext rng (provider.ConsumeIntegral <uint64_t >());
292303
293304 /* * Variable used whenever an empty TxGraph::Ref is needed. */
294305 TxGraph::Ref empty_ref;
@@ -830,6 +841,122 @@ FUZZ_TARGET(txgraph)
830841 // else.
831842 assert (top_sim.MatchesOversizedClusters (removed_set));
832843
844+ // Apply all removals to the simulation, and verify the result is no longer
845+ // oversized. Don't query the real graph for oversizedness; it is compared
846+ // against the simulation anyway later.
847+ for (auto simpos : removed_set) {
848+ top_sim.RemoveTransaction (top_sim.GetRef (simpos));
849+ }
850+ assert (!top_sim.IsOversized ());
851+ break ;
852+ } else if ((block_builders.empty () || sims.size () > 1 ) &&
853+ top_sim.GetTransactionCount () > max_cluster_count && !top_sim.IsOversized () && command-- == 0 ) {
854+ // Trim (special case which avoids apparent cycles in the implicit approximate
855+ // dependency graph constructed inside the Trim() implementation). This is worth
856+ // testing separately, because such cycles cannot occur in realistic scenarios,
857+ // but this is hard to replicate in general in this fuzz test.
858+
859+ // First, we need to have dependencies applied and linearizations fixed to avoid
860+ // circular dependencies in implied graph; trigger it via whatever means.
861+ real->CountDistinctClusters ({}, false );
862+
863+ // Gather the current clusters.
864+ auto clusters = top_sim.GetComponents ();
865+
866+ // Merge clusters randomly until at least one oversized one appears.
867+ bool made_oversized = false ;
868+ auto merges_left = clusters.size () - 1 ;
869+ while (merges_left > 0 ) {
870+ --merges_left;
871+ // Find positions of clusters in the clusters vector to merge together.
872+ auto par_cl = rng.randrange (clusters.size ());
873+ auto chl_cl = rng.randrange (clusters.size () - 1 );
874+ chl_cl += (chl_cl >= par_cl);
875+ Assume (chl_cl != par_cl);
876+ // Add between 1 and 3 dependencies between them. As all are in the same
877+ // direction (from the child cluster to parent cluster), no cycles are possible,
878+ // regardless of what internal topology Trim() uses as approximation within the
879+ // clusters.
880+ int num_deps = rng.randrange (3 ) + 1 ;
881+ for (int i = 0 ; i < num_deps; ++i) {
882+ // Find a parent transaction in the parent cluster.
883+ auto par_idx = rng.randrange (clusters[par_cl].Count ());
884+ SimTxGraph::Pos par_pos = 0 ;
885+ for (auto j : clusters[par_cl]) {
886+ if (par_idx == 0 ) {
887+ par_pos = j;
888+ break ;
889+ }
890+ --par_idx;
891+ }
892+ // Find a child transaction in the child cluster.
893+ auto chl_idx = rng.randrange (clusters[chl_cl].Count ());
894+ SimTxGraph::Pos chl_pos = 0 ;
895+ for (auto j : clusters[chl_cl]) {
896+ if (chl_idx == 0 ) {
897+ chl_pos = j;
898+ break ;
899+ }
900+ --chl_idx;
901+ }
902+ // Add dependency to both simulation and real TxGraph.
903+ auto par_ref = top_sim.GetRef (par_pos);
904+ auto chl_ref = top_sim.GetRef (chl_pos);
905+ top_sim.AddDependency (par_ref, chl_ref);
906+ real->AddDependency (*par_ref, *chl_ref);
907+ }
908+ // Compute the combined cluster.
909+ auto par_cluster = clusters[par_cl];
910+ auto chl_cluster = clusters[chl_cl];
911+ auto new_cluster = par_cluster | chl_cluster;
912+ // Remove the parent and child cluster from clusters.
913+ std::erase_if (clusters, [&](const auto & cl) noexcept { return cl == par_cluster || cl == chl_cluster; });
914+ // Add the combined cluster.
915+ clusters.push_back (new_cluster);
916+ // If this is the first merge that causes an oversized cluster to appear, pick
917+ // a random number of further merges to appear.
918+ if (!made_oversized) {
919+ made_oversized = new_cluster.Count () > max_cluster_count;
920+ if (!made_oversized) {
921+ FeeFrac total;
922+ for (auto i : new_cluster) total += top_sim.graph .FeeRate (i);
923+ if (uint32_t (total.size ) > max_cluster_size) made_oversized = true ;
924+ }
925+ if (made_oversized) merges_left = rng.randrange (clusters.size ());
926+ }
927+ }
928+
929+ // Determine an upper bound on how many transactions are removed.
930+ uint32_t max_removed = 0 ;
931+ for (auto & cluster : clusters) {
932+ // Gather all transaction sizes in the to-be-combined cluster.
933+ std::vector<uint32_t > sizes;
934+ for (auto i : cluster) sizes.push_back (top_sim.graph .FeeRate (i).size );
935+ auto sum_sizes = std::accumulate (sizes.begin (), sizes.end (), uint64_t {0 });
936+ // Sort from large to small.
937+ std::sort (sizes.begin (), sizes.end (), std::greater{});
938+ // In the worst case, only the smallest transactions are removed.
939+ while (sizes.size () > max_cluster_count || sum_sizes > max_cluster_size) {
940+ sum_sizes -= sizes.back ();
941+ sizes.pop_back ();
942+ ++max_removed;
943+ }
944+ }
945+
946+ // Invoke Trim now on the definitely-oversized txgraph.
947+ auto removed = real->Trim ();
948+ // Verify that the number of removals is within range.
949+ assert (removed.size () >= 1 );
950+ assert (removed.size () <= max_removed);
951+ // The removed set must contain all its own descendants.
952+ auto removed_set = top_sim.MakeSet (removed);
953+ for (auto simpos : removed_set) {
954+ assert (top_sim.graph .Descendants (simpos).IsSubsetOf (removed_set));
955+ }
956+ // Something from every oversized cluster should have been removed, and nothing
957+ // else.
958+ assert (top_sim.MatchesOversizedClusters (removed_set));
959+
833960 // Apply all removals to the simulation, and verify the result is no longer
834961 // oversized. Don't query the real graph for oversizedness; it is compared
835962 // against the simulation anyway later.
0 commit comments