@@ -69,22 +69,32 @@ struct SimTxGraph
69
69
SimTxGraph (SimTxGraph&&) noexcept = default ;
70
70
SimTxGraph& operator =(SimTxGraph&&) noexcept = default ;
71
71
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
+
72
86
/* * Check whether this graph is oversized (contains a connected component whose number of
73
87
* transactions exceeds max_cluster_count. */
74
88
bool IsOversized ()
75
89
{
76
90
if (!oversized.has_value ()) {
77
91
// Only recompute when oversized isn't already known.
78
92
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 ()) {
83
94
if (component.Count () > max_cluster_count) oversized = true ;
84
95
uint64_t component_size{0 };
85
96
for (auto i : component) component_size += graph.FeeRate (i).size ;
86
97
if (component_size > max_cluster_size) oversized = true ;
87
- todo -= component;
88
98
}
89
99
}
90
100
return *oversized;
@@ -287,8 +297,9 @@ FUZZ_TARGET(txgraph)
287
297
FuzzedDataProvider provider (buffer.data (), buffer.size ());
288
298
289
299
/* * 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 >());
292
303
293
304
/* * Variable used whenever an empty TxGraph::Ref is needed. */
294
305
TxGraph::Ref empty_ref;
@@ -830,6 +841,122 @@ FUZZ_TARGET(txgraph)
830
841
// else.
831
842
assert (top_sim.MatchesOversizedClusters (removed_set));
832
843
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
+
833
960
// Apply all removals to the simulation, and verify the result is no longer
834
961
// oversized. Don't query the real graph for oversizedness; it is compared
835
962
// against the simulation anyway later.
0 commit comments