Bitcoin Core  28.1.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 <serialize.h>
7 #include <streams.h>
8 #include <test/fuzz/fuzz.h>
11 #include <util/bitset.h>
12 #include <util/feefrac.h>
13 
14 #include <algorithm>
15 #include <stdint.h>
16 #include <vector>
17 #include <utility>
18 
19 using namespace cluster_linearize;
20 
21 namespace {
22 
28 template<typename SetType>
29 class SimpleCandidateFinder
30 {
32  const DepGraph<SetType>& m_depgraph;
34  SetType m_todo;
35 
36 public:
38  SimpleCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept :
39  m_depgraph(depgraph), m_todo{SetType::Fill(depgraph.TxCount())} {}
40 
42  void MarkDone(SetType select) noexcept { m_todo -= select; }
43 
45  bool AllDone() const noexcept { return m_todo.None(); }
46 
53  std::pair<SetInfo<SetType>, uint64_t> FindCandidateSet(uint64_t max_iterations) const noexcept
54  {
55  uint64_t iterations_left = max_iterations;
56  // Queue of work units. Each consists of:
57  // - inc: set of transactions definitely included
58  // - und: set of transactions that can be added to inc still
59  std::vector<std::pair<SetType, SetType>> queue;
60  // Initially we have just one queue element, with the entire graph in und.
61  queue.emplace_back(SetType{}, m_todo);
62  // Best solution so far.
63  SetInfo best(m_depgraph, m_todo);
64  // Process the queue.
65  while (!queue.empty() && iterations_left) {
66  --iterations_left;
67  // Pop top element of the queue.
68  auto [inc, und] = queue.back();
69  queue.pop_back();
70  // Look for a transaction to consider adding/removing.
71  bool inc_none = inc.None();
72  for (auto split : und) {
73  // If inc is empty, consider any split transaction. Otherwise only consider
74  // transactions that share ancestry with inc so far (which means only connected
75  // sets will be considered).
76  if (inc_none || inc.Overlaps(m_depgraph.Ancestors(split))) {
77  // Add a queue entry with split included.
78  SetInfo new_inc(m_depgraph, inc | (m_todo & m_depgraph.Ancestors(split)));
79  queue.emplace_back(new_inc.transactions, und - new_inc.transactions);
80  // Add a queue entry with split excluded.
81  queue.emplace_back(inc, und - m_depgraph.Descendants(split));
82  // Update statistics to account for the candidate new_inc.
83  if (new_inc.feerate > best.feerate) best = new_inc;
84  break;
85  }
86  }
87  }
88  return {std::move(best), max_iterations - iterations_left};
89  }
90 };
91 
98 template<typename SetType>
99 class ExhaustiveCandidateFinder
100 {
102  const DepGraph<SetType>& m_depgraph;
104  SetType m_todo;
105 
106 public:
108  ExhaustiveCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept :
109  m_depgraph(depgraph), m_todo{SetType::Fill(depgraph.TxCount())} {}
110 
112  void MarkDone(SetType select) noexcept { m_todo -= select; }
113 
115  bool AllDone() const noexcept { return m_todo.None(); }
116 
121  SetInfo<SetType> FindCandidateSet() const noexcept
122  {
123  // Best solution so far.
124  SetInfo<SetType> best{m_todo, m_depgraph.FeeRate(m_todo)};
125  // The number of combinations to try.
126  uint64_t limit = (uint64_t{1} << m_todo.Count()) - 1;
127  // Try the transitive closure of every non-empty subset of m_todo.
128  for (uint64_t x = 1; x < limit; ++x) {
129  // If bit number b is set in x, then the remaining ancestors of the b'th remaining
130  // transaction in m_todo are included.
131  SetType txn;
132  auto x_shifted{x};
133  for (auto i : m_todo) {
134  if (x_shifted & 1) txn |= m_depgraph.Ancestors(i);
135  x_shifted >>= 1;
136  }
137  SetInfo cur(m_depgraph, txn & m_todo);
138  if (cur.feerate > best.feerate) best = cur;
139  }
140  return best;
141  }
142 };
143 
150 template<typename SetType>
151 std::pair<std::vector<ClusterIndex>, bool> SimpleLinearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations)
152 {
153  std::vector<ClusterIndex> linearization;
154  SimpleCandidateFinder finder(depgraph);
155  SetType todo = SetType::Fill(depgraph.TxCount());
156  bool optimal = true;
157  while (todo.Any()) {
158  auto [candidate, iterations_done] = finder.FindCandidateSet(max_iterations);
159  if (iterations_done == max_iterations) optimal = false;
160  depgraph.AppendTopo(linearization, candidate.transactions);
161  todo -= candidate.transactions;
162  finder.MarkDone(candidate.transactions);
163  max_iterations -= iterations_done;
164  }
165  return {std::move(linearization), optimal};
166 }
167 
169 template<typename SetType>
170 SetType ReadTopologicalSet(const DepGraph<SetType>& depgraph, const SetType& todo, SpanReader& reader)
171 {
172  uint64_t mask{0};
173  try {
174  reader >> VARINT(mask);
175  } catch(const std::ios_base::failure&) {}
176  SetType ret;
177  for (auto i : todo) {
178  if (!ret[i]) {
179  if (mask & 1) ret |= depgraph.Ancestors(i);
180  mask >>= 1;
181  }
182  }
183  return ret & todo;
184 }
185 
187 template<typename BS>
188 std::vector<ClusterIndex> ReadLinearization(const DepGraph<BS>& depgraph, SpanReader& reader)
189 {
190  std::vector<ClusterIndex> linearization;
191  TestBitSet todo = TestBitSet::Fill(depgraph.TxCount());
192  // In every iteration one topologically-valid transaction is appended to linearization.
193  while (todo.Any()) {
194  // Compute the set of transactions with no not-yet-included ancestors.
195  TestBitSet potential_next;
196  for (auto j : todo) {
197  if ((depgraph.Ancestors(j) & todo) == TestBitSet::Singleton(j)) {
198  potential_next.Set(j);
199  }
200  }
201  // There must always be one (otherwise there is a cycle in the graph).
202  assert(potential_next.Any());
203  // Read a number from reader, and interpret it as index into potential_next.
204  uint64_t idx{0};
205  try {
206  reader >> VARINT(idx);
207  } catch (const std::ios_base::failure&) {}
208  idx %= potential_next.Count();
209  // Find out which transaction that corresponds to.
210  for (auto j : potential_next) {
211  if (idx == 0) {
212  // When found, add it to linearization and remove it from todo.
213  linearization.push_back(j);
214  assert(todo[j]);
215  todo.Reset(j);
216  break;
217  }
218  --idx;
219  }
220  }
221  return linearization;
222 }
223 
224 } // namespace
225 
226 FUZZ_TARGET(clusterlin_add_dependency)
227 {
228  // Verify that computing a DepGraph from a cluster, or building it step by step using AddDependency
229  // have the same effect.
230 
231  // Construct a cluster of a certain length, with no dependencies.
232  FuzzedDataProvider provider(buffer.data(), buffer.size());
233  auto num_tx = provider.ConsumeIntegralInRange<ClusterIndex>(2, 32);
234  Cluster<TestBitSet> cluster(num_tx, std::pair{FeeFrac{0, 1}, TestBitSet{}});
235  // Construct the corresponding DepGraph object (also no dependencies).
236  DepGraph depgraph(cluster);
237  SanityCheck(depgraph);
238  // Read (parent, child) pairs, and add them to the cluster and depgraph.
239  LIMITED_WHILE(provider.remaining_bytes() > 0, TestBitSet::Size() * TestBitSet::Size()) {
240  auto parent = provider.ConsumeIntegralInRange<ClusterIndex>(0, num_tx - 1);
241  auto child = provider.ConsumeIntegralInRange<ClusterIndex>(0, num_tx - 2);
242  child += (child >= parent);
243  cluster[child].second.Set(parent);
244  depgraph.AddDependency(parent, child);
245  assert(depgraph.Ancestors(child)[parent]);
246  assert(depgraph.Descendants(parent)[child]);
247  }
248  // Sanity check the result.
249  SanityCheck(depgraph);
250  // Verify that the resulting DepGraph matches one recomputed from the cluster.
251  assert(DepGraph(cluster) == depgraph);
252 }
253 
254 FUZZ_TARGET(clusterlin_cluster_serialization)
255 {
256  // Verify that any graph of transactions has its ancestry correctly computed by DepGraph, and
257  // if it is a DAG, that it can be serialized as a DepGraph in a way that roundtrips. This
258  // guarantees that any acyclic cluster has a corresponding DepGraph serialization.
259 
260  FuzzedDataProvider provider(buffer.data(), buffer.size());
261 
262  // Construct a cluster in a naive way (using a FuzzedDataProvider-based serialization).
263  Cluster<TestBitSet> cluster;
264  auto num_tx = provider.ConsumeIntegralInRange<ClusterIndex>(1, 32);
265  cluster.resize(num_tx);
266  for (ClusterIndex i = 0; i < num_tx; ++i) {
267  cluster[i].first.size = provider.ConsumeIntegralInRange<int32_t>(1, 0x3fffff);
268  cluster[i].first.fee = provider.ConsumeIntegralInRange<int64_t>(-0x8000000000000, 0x7ffffffffffff);
269  for (ClusterIndex j = 0; j < num_tx; ++j) {
270  if (i == j) continue;
271  if (provider.ConsumeBool()) cluster[i].second.Set(j);
272  }
273  }
274 
275  // Construct dependency graph, and verify it matches the cluster (which includes a round-trip
276  // check for the serialization).
277  DepGraph depgraph(cluster);
278  VerifyDepGraphFromCluster(cluster, depgraph);
279 }
280 
281 FUZZ_TARGET(clusterlin_depgraph_serialization)
282 {
283  // Verify that any deserialized depgraph is acyclic and roundtrips to an identical depgraph.
284 
285  // Construct a graph by deserializing.
286  SpanReader reader(buffer);
287  DepGraph<TestBitSet> depgraph;
288  try {
289  reader >> Using<DepGraphFormatter>(depgraph);
290  } catch (const std::ios_base::failure&) {}
291  SanityCheck(depgraph);
292 
293  // Verify the graph is a DAG.
294  assert(IsAcyclic(depgraph));
295 }
296 
297 FUZZ_TARGET(clusterlin_components)
298 {
299  // Verify the behavior of DepGraphs's FindConnectedComponent and IsConnected functions.
300 
301  // Construct a depgraph.
302  SpanReader reader(buffer);
303  DepGraph<TestBitSet> depgraph;
304  try {
305  reader >> Using<DepGraphFormatter>(depgraph);
306  } catch (const std::ios_base::failure&) {}
307 
308  TestBitSet todo = TestBitSet::Fill(depgraph.TxCount());
309  while (todo.Any()) {
310  // Find a connected component inside todo.
311  auto component = depgraph.FindConnectedComponent(todo);
312 
313  // The component must be a subset of todo and non-empty.
314  assert(component.IsSubsetOf(todo));
315  assert(component.Any());
316 
317  // If todo is the entire graph, and the entire graph is connected, then the component must
318  // be the entire graph.
319  if (todo == TestBitSet::Fill(depgraph.TxCount())) {
320  assert((component == todo) == depgraph.IsConnected());
321  }
322 
323  // If subset is connected, then component must match subset.
324  assert((component == todo) == depgraph.IsConnected(todo));
325 
326  // The component cannot have any ancestors or descendants outside of component but in todo.
327  for (auto i : component) {
328  assert((depgraph.Ancestors(i) & todo).IsSubsetOf(component));
329  assert((depgraph.Descendants(i) & todo).IsSubsetOf(component));
330  }
331 
332  // Starting from any component element, we must be able to reach every element.
333  for (auto i : component) {
334  // Start with just i as reachable.
335  TestBitSet reachable = TestBitSet::Singleton(i);
336  // Add in-todo descendants and ancestors to reachable until it does not change anymore.
337  while (true) {
338  TestBitSet new_reachable = reachable;
339  for (auto j : new_reachable) {
340  new_reachable |= depgraph.Ancestors(j) & todo;
341  new_reachable |= depgraph.Descendants(j) & todo;
342  }
343  if (new_reachable == reachable) break;
344  reachable = new_reachable;
345  }
346  // Verify that the result is the entire component.
347  assert(component == reachable);
348  }
349 
350  // Construct an arbitrary subset of todo.
351  uint64_t subset_bits{0};
352  try {
353  reader >> VARINT(subset_bits);
354  } catch (const std::ios_base::failure&) {}
355  TestBitSet subset;
356  for (ClusterIndex i = 0; i < depgraph.TxCount(); ++i) {
357  if (todo[i]) {
358  if (subset_bits & 1) subset.Set(i);
359  subset_bits >>= 1;
360  }
361  }
362  // Which must be non-empty.
363  if (subset.None()) subset = TestBitSet::Singleton(todo.First());
364  // Remove it from todo.
365  todo -= subset;
366  }
367 
368  // No components can be found in an empty subset.
369  assert(depgraph.FindConnectedComponent(todo).None());
370 }
371 
372 FUZZ_TARGET(clusterlin_chunking)
373 {
374  // Verify the correctness of the ChunkLinearization function.
375 
376  // Construct a graph by deserializing.
377  SpanReader reader(buffer);
378  DepGraph<TestBitSet> depgraph;
379  try {
380  reader >> Using<DepGraphFormatter>(depgraph);
381  } catch (const std::ios_base::failure&) {}
382 
383  // Read a valid linearization for depgraph.
384  auto linearization = ReadLinearization(depgraph, reader);
385 
386  // Invoke the chunking function.
387  auto chunking = ChunkLinearization(depgraph, linearization);
388 
389  // Verify that chunk feerates are monotonically non-increasing.
390  for (size_t i = 1; i < chunking.size(); ++i) {
391  assert(!(chunking[i] >> chunking[i - 1]));
392  }
393 
394  // Naively recompute the chunks (each is the highest-feerate prefix of what remains).
395  auto todo = TestBitSet::Fill(depgraph.TxCount());
396  for (const auto& chunk_feerate : chunking) {
397  assert(todo.Any());
398  SetInfo<TestBitSet> accumulator, best;
399  for (ClusterIndex idx : linearization) {
400  if (todo[idx]) {
401  accumulator |= SetInfo(depgraph, idx);
402  if (best.feerate.IsEmpty() || accumulator.feerate >> best.feerate) {
403  best = accumulator;
404  }
405  }
406  }
407  assert(chunk_feerate == best.feerate);
408  assert(best.transactions.IsSubsetOf(todo));
409  todo -= best.transactions;
410  }
411  assert(todo.None());
412 }
413 
414 FUZZ_TARGET(clusterlin_ancestor_finder)
415 {
416  // Verify that AncestorCandidateFinder works as expected.
417 
418  // Retrieve a depgraph from the fuzz input.
419  SpanReader reader(buffer);
420  DepGraph<TestBitSet> depgraph;
421  try {
422  reader >> Using<DepGraphFormatter>(depgraph);
423  } catch (const std::ios_base::failure&) {}
424 
425  AncestorCandidateFinder anc_finder(depgraph);
426  auto todo = TestBitSet::Fill(depgraph.TxCount());
427  while (todo.Any()) {
428  // Call the ancestor finder's FindCandidateSet for what remains of the graph.
429  assert(!anc_finder.AllDone());
430  auto best_anc = anc_finder.FindCandidateSet();
431  // Sanity check the result.
432  assert(best_anc.transactions.Any());
433  assert(best_anc.transactions.IsSubsetOf(todo));
434  assert(depgraph.FeeRate(best_anc.transactions) == best_anc.feerate);
435  assert(depgraph.IsConnected(best_anc.transactions));
436  // Check that it is topologically valid.
437  for (auto i : best_anc.transactions) {
438  assert((depgraph.Ancestors(i) & todo).IsSubsetOf(best_anc.transactions));
439  }
440 
441  // Compute all remaining ancestor sets.
442  std::optional<SetInfo<TestBitSet>> real_best_anc;
443  for (auto i : todo) {
444  SetInfo info(depgraph, todo & depgraph.Ancestors(i));
445  if (!real_best_anc.has_value() || info.feerate > real_best_anc->feerate) {
446  real_best_anc = info;
447  }
448  }
449  // The set returned by anc_finder must equal the real best ancestor sets.
450  assert(real_best_anc.has_value());
451  assert(*real_best_anc == best_anc);
452 
453  // Find a topologically valid subset of transactions to remove from the graph.
454  auto del_set = ReadTopologicalSet(depgraph, todo, reader);
455  // If we did not find anything, use best_anc itself, because we should remove something.
456  if (del_set.None()) del_set = best_anc.transactions;
457  todo -= del_set;
458  anc_finder.MarkDone(del_set);
459  }
460  assert(anc_finder.AllDone());
461 }
462 
463 static constexpr auto MAX_SIMPLE_ITERATIONS = 300000;
464 
465 FUZZ_TARGET(clusterlin_search_finder)
466 {
467  // Verify that SearchCandidateFinder works as expected by sanity checking the results
468  // and comparing with the results from SimpleCandidateFinder, ExhaustiveCandidateFinder, and
469  // AncestorCandidateFinder.
470 
471  // Retrieve an RNG seed and a depgraph from the fuzz input.
472  SpanReader reader(buffer);
473  DepGraph<TestBitSet> depgraph;
474  uint64_t rng_seed{0};
475  try {
476  reader >> Using<DepGraphFormatter>(depgraph) >> rng_seed;
477  } catch (const std::ios_base::failure&) {}
478 
479  // Instantiate ALL the candidate finders.
480  SearchCandidateFinder src_finder(depgraph, rng_seed);
481  SimpleCandidateFinder smp_finder(depgraph);
482  ExhaustiveCandidateFinder exh_finder(depgraph);
483  AncestorCandidateFinder anc_finder(depgraph);
484 
485  auto todo = TestBitSet::Fill(depgraph.TxCount());
486  while (todo.Any()) {
487  assert(!src_finder.AllDone());
488  assert(!smp_finder.AllDone());
489  assert(!exh_finder.AllDone());
490  assert(!anc_finder.AllDone());
491 
492  // For each iteration, read an iteration count limit from the fuzz input.
493  uint64_t max_iterations = 1;
494  try {
495  reader >> VARINT(max_iterations);
496  } catch (const std::ios_base::failure&) {}
497  max_iterations &= 0xfffff;
498 
499  // Read an initial subset from the fuzz input.
500  SetInfo init_best(depgraph, ReadTopologicalSet(depgraph, todo, reader));
501 
502  // Call the search finder's FindCandidateSet for what remains of the graph.
503  auto [found, iterations_done] = src_finder.FindCandidateSet(max_iterations, init_best);
504 
505  // Sanity check the result.
506  assert(iterations_done <= max_iterations);
507  assert(found.transactions.Any());
508  assert(found.transactions.IsSubsetOf(todo));
509  assert(depgraph.FeeRate(found.transactions) == found.feerate);
510  if (!init_best.feerate.IsEmpty()) assert(found.feerate >= init_best.feerate);
511  // Check that it is topologically valid.
512  for (auto i : found.transactions) {
513  assert(found.transactions.IsSupersetOf(depgraph.Ancestors(i) & todo));
514  }
515 
516  // At most 2^N-1 iterations can be required: the number of non-empty subsets a graph with N
517  // transactions has.
518  assert(iterations_done <= ((uint64_t{1} << todo.Count()) - 1));
519 
520  // Perform quality checks only if SearchCandidateFinder claims an optimal result.
521  if (iterations_done < max_iterations) {
522  // Optimal sets are always connected.
523  assert(depgraph.IsConnected(found.transactions));
524 
525  // Compare with SimpleCandidateFinder.
526  auto [simple, simple_iters] = smp_finder.FindCandidateSet(MAX_SIMPLE_ITERATIONS);
527  assert(found.feerate >= simple.feerate);
528  if (simple_iters < MAX_SIMPLE_ITERATIONS) {
529  assert(found.feerate == simple.feerate);
530  }
531 
532  // Compare with AncestorCandidateFinder;
533  auto anc = anc_finder.FindCandidateSet();
534  assert(found.feerate >= anc.feerate);
535 
536  // Compare with ExhaustiveCandidateFinder. This quickly gets computationally expensive
537  // for large clusters (O(2^n)), so only do it for sufficiently small ones.
538  if (todo.Count() <= 12) {
539  auto exhaustive = exh_finder.FindCandidateSet();
540  assert(exhaustive.feerate == found.feerate);
541  // Also compare ExhaustiveCandidateFinder with SimpleCandidateFinder (this is
542  // primarily a test for SimpleCandidateFinder's correctness).
543  assert(exhaustive.feerate >= simple.feerate);
544  if (simple_iters < MAX_SIMPLE_ITERATIONS) {
545  assert(exhaustive.feerate == simple.feerate);
546  }
547  }
548  }
549 
550  // Find a topologically valid subset of transactions to remove from the graph.
551  auto del_set = ReadTopologicalSet(depgraph, todo, reader);
552  // If we did not find anything, use found itself, because we should remove something.
553  if (del_set.None()) del_set = found.transactions;
554  todo -= del_set;
555  src_finder.MarkDone(del_set);
556  smp_finder.MarkDone(del_set);
557  exh_finder.MarkDone(del_set);
558  anc_finder.MarkDone(del_set);
559  }
560 
561  assert(src_finder.AllDone());
562  assert(smp_finder.AllDone());
563  assert(exh_finder.AllDone());
564  assert(anc_finder.AllDone());
565 }
566 
567 FUZZ_TARGET(clusterlin_linearization_chunking)
568 {
569  // Verify the behavior of LinearizationChunking.
570 
571  // Retrieve a depgraph from the fuzz input.
572  SpanReader reader(buffer);
573  DepGraph<TestBitSet> depgraph;
574  try {
575  reader >> Using<DepGraphFormatter>(depgraph);
576  } catch (const std::ios_base::failure&) {}
577 
578  // Retrieve a topologically-valid subset of depgraph.
579  auto todo = TestBitSet::Fill(depgraph.TxCount());
580  auto subset = SetInfo(depgraph, ReadTopologicalSet(depgraph, todo, reader));
581 
582  // Retrieve a valid linearization for depgraph.
583  auto linearization = ReadLinearization(depgraph, reader);
584 
585  // Construct a LinearizationChunking object, initially for the whole linearization.
586  LinearizationChunking chunking(depgraph, linearization);
587 
588  // Incrementally remove transactions from the chunking object, and check various properties at
589  // every step.
590  while (todo.Any()) {
591  assert(chunking.NumChunksLeft() > 0);
592 
593  // Construct linearization with just todo.
594  std::vector<ClusterIndex> linearization_left;
595  for (auto i : linearization) {
596  if (todo[i]) linearization_left.push_back(i);
597  }
598 
599  // Compute the chunking for linearization_left.
600  auto chunking_left = ChunkLinearization(depgraph, linearization_left);
601 
602  // Verify that it matches the feerates of the chunks of chunking.
603  assert(chunking.NumChunksLeft() == chunking_left.size());
604  for (ClusterIndex i = 0; i < chunking.NumChunksLeft(); ++i) {
605  assert(chunking.GetChunk(i).feerate == chunking_left[i]);
606  }
607 
608  // Check consistency of chunking.
609  TestBitSet combined;
610  for (ClusterIndex i = 0; i < chunking.NumChunksLeft(); ++i) {
611  const auto& chunk_info = chunking.GetChunk(i);
612  // Chunks must be non-empty.
613  assert(chunk_info.transactions.Any());
614  // Chunk feerates must be monotonically non-increasing.
615  if (i > 0) assert(!(chunk_info.feerate >> chunking.GetChunk(i - 1).feerate));
616  // Chunks must be a subset of what is left of the linearization.
617  assert(chunk_info.transactions.IsSubsetOf(todo));
618  // Chunks' claimed feerates must match their transactions' aggregate feerate.
619  assert(depgraph.FeeRate(chunk_info.transactions) == chunk_info.feerate);
620  // Chunks must be the highest-feerate remaining prefix.
621  SetInfo<TestBitSet> accumulator, best;
622  for (auto j : linearization) {
623  if (todo[j] && !combined[j]) {
624  accumulator |= SetInfo(depgraph, j);
625  if (best.feerate.IsEmpty() || accumulator.feerate > best.feerate) {
626  best = accumulator;
627  }
628  }
629  }
630  assert(best.transactions == chunk_info.transactions);
631  assert(best.feerate == chunk_info.feerate);
632  // Chunks cannot overlap.
633  assert(!chunk_info.transactions.Overlaps(combined));
634  combined |= chunk_info.transactions;
635  // Chunks must be topological.
636  for (auto idx : chunk_info.transactions) {
637  assert((depgraph.Ancestors(idx) & todo).IsSubsetOf(combined));
638  }
639  }
640  assert(combined == todo);
641 
642  // Verify the expected properties of LinearizationChunking::IntersectPrefixes:
643  auto intersect = chunking.IntersectPrefixes(subset);
644  // - Intersecting again doesn't change the result.
645  assert(chunking.IntersectPrefixes(intersect) == intersect);
646  // - The intersection is topological.
647  TestBitSet intersect_anc;
648  for (auto idx : intersect.transactions) {
649  intersect_anc |= (depgraph.Ancestors(idx) & todo);
650  }
651  assert(intersect.transactions == intersect_anc);
652  // - The claimed intersection feerate matches its transactions.
653  assert(intersect.feerate == depgraph.FeeRate(intersect.transactions));
654  // - The intersection may only be empty if its input is empty.
655  assert(intersect.transactions.Any() == subset.transactions.Any());
656  // - The intersection feerate must be as high as the input.
657  assert(intersect.feerate >= subset.feerate);
658  // - No non-empty intersection between the intersection and a prefix of the chunks of the
659  // remainder of the linearization may be better than the intersection.
660  TestBitSet prefix;
661  for (ClusterIndex i = 0; i < chunking.NumChunksLeft(); ++i) {
662  prefix |= chunking.GetChunk(i).transactions;
663  auto reintersect = SetInfo(depgraph, prefix & intersect.transactions);
664  if (!reintersect.feerate.IsEmpty()) {
665  assert(reintersect.feerate <= intersect.feerate);
666  }
667  }
668 
669  // Find a subset to remove from linearization.
670  auto done = ReadTopologicalSet(depgraph, todo, reader);
671  if (done.None()) {
672  // We need to remove a non-empty subset, so fall back to the unlinearized ancestors of
673  // the first transaction in todo if done is empty.
674  done = depgraph.Ancestors(todo.First()) & todo;
675  }
676  todo -= done;
677  chunking.MarkDone(done);
678  subset = SetInfo(depgraph, subset.transactions - done);
679  }
680 
681  assert(chunking.NumChunksLeft() == 0);
682 }
683 
684 FUZZ_TARGET(clusterlin_linearize)
685 {
686  // Verify the behavior of Linearize().
687 
688  // Retrieve an RNG seed, an iteration count, and a depgraph from the fuzz input.
689  SpanReader reader(buffer);
690  DepGraph<TestBitSet> depgraph;
691  uint64_t rng_seed{0};
692  uint64_t iter_count{0};
693  try {
694  reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph) >> rng_seed;
695  } catch (const std::ios_base::failure&) {}
696 
697  // Optionally construct an old linearization for it.
698  std::vector<ClusterIndex> old_linearization;
699  {
700  uint8_t have_old_linearization{0};
701  try {
702  reader >> have_old_linearization;
703  } catch(const std::ios_base::failure&) {}
704  if (have_old_linearization & 1) {
705  old_linearization = ReadLinearization(depgraph, reader);
706  SanityCheck(depgraph, old_linearization);
707  }
708  }
709 
710  // Invoke Linearize().
711  iter_count &= 0x7ffff;
712  auto [linearization, optimal] = Linearize(depgraph, iter_count, rng_seed, old_linearization);
713  SanityCheck(depgraph, linearization);
714  auto chunking = ChunkLinearization(depgraph, linearization);
715 
716  // Linearization must always be as good as the old one, if provided.
717  if (!old_linearization.empty()) {
718  auto old_chunking = ChunkLinearization(depgraph, old_linearization);
719  auto cmp = CompareChunks(chunking, old_chunking);
720  assert(cmp >= 0);
721  }
722 
723  // If the iteration count is sufficiently high, an optimal linearization must be found.
724  // Each linearization step can use up to 2^k iterations, with steps k=1..n. That sum is
725  // 2 * (2^n - 1)
726  const uint64_t n = depgraph.TxCount();
727  if (n <= 18 && iter_count > 2U * ((uint64_t{1} << n) - 1U)) {
728  assert(optimal);
729  }
730 
731  // If Linearize claims optimal result, run quality tests.
732  if (optimal) {
733  // It must be as good as SimpleLinearize.
734  auto [simple_linearization, simple_optimal] = SimpleLinearize(depgraph, MAX_SIMPLE_ITERATIONS);
735  SanityCheck(depgraph, simple_linearization);
736  auto simple_chunking = ChunkLinearization(depgraph, simple_linearization);
737  auto cmp = CompareChunks(chunking, simple_chunking);
738  assert(cmp >= 0);
739  // If SimpleLinearize finds the optimal result too, they must be equal (if not,
740  // SimpleLinearize is broken).
741  if (simple_optimal) assert(cmp == 0);
742 
743  // Only for very small clusters, test every topologically-valid permutation.
744  if (depgraph.TxCount() <= 7) {
745  std::vector<ClusterIndex> perm_linearization(depgraph.TxCount());
746  for (ClusterIndex i = 0; i < depgraph.TxCount(); ++i) perm_linearization[i] = i;
747  // Iterate over all valid permutations.
748  do {
749  // Determine whether perm_linearization is topological.
750  TestBitSet perm_done;
751  bool perm_is_topo{true};
752  for (auto i : perm_linearization) {
753  perm_done.Set(i);
754  if (!depgraph.Ancestors(i).IsSubsetOf(perm_done)) {
755  perm_is_topo = false;
756  break;
757  }
758  }
759  // If so, verify that the obtained linearization is as good as the permutation.
760  if (perm_is_topo) {
761  auto perm_chunking = ChunkLinearization(depgraph, perm_linearization);
762  auto cmp = CompareChunks(chunking, perm_chunking);
763  assert(cmp >= 0);
764  }
765  } while(std::next_permutation(perm_linearization.begin(), perm_linearization.end()));
766  }
767  }
768 }
769 
770 FUZZ_TARGET(clusterlin_postlinearize)
771 {
772  // Verify expected properties of PostLinearize() on arbitrary linearizations.
773 
774  // Retrieve a depgraph from the fuzz input.
775  SpanReader reader(buffer);
776  DepGraph<TestBitSet> depgraph;
777  try {
778  reader >> Using<DepGraphFormatter>(depgraph);
779  } catch (const std::ios_base::failure&) {}
780 
781  // Retrieve a linearization from the fuzz input.
782  std::vector<ClusterIndex> linearization;
783  linearization = ReadLinearization(depgraph, reader);
784  SanityCheck(depgraph, linearization);
785 
786  // Produce a post-processed version.
787  auto post_linearization = linearization;
788  PostLinearize(depgraph, post_linearization);
789  SanityCheck(depgraph, post_linearization);
790 
791  // Compare diagrams: post-linearization cannot worsen anywhere.
792  auto chunking = ChunkLinearization(depgraph, linearization);
793  auto post_chunking = ChunkLinearization(depgraph, post_linearization);
794  auto cmp = CompareChunks(post_chunking, chunking);
795  assert(cmp >= 0);
796 
797  // Run again, things can keep improving (and never get worse)
798  auto post_post_linearization = post_linearization;
799  PostLinearize(depgraph, post_post_linearization);
800  SanityCheck(depgraph, post_post_linearization);
801  auto post_post_chunking = ChunkLinearization(depgraph, post_post_linearization);
802  cmp = CompareChunks(post_post_chunking, post_chunking);
803  assert(cmp >= 0);
804 
805  // The chunks that come out of postlinearizing are always connected.
806  LinearizationChunking linchunking(depgraph, post_linearization);
807  while (linchunking.NumChunksLeft()) {
808  assert(depgraph.IsConnected(linchunking.GetChunk(0).transactions));
809  linchunking.MarkDone(linchunking.GetChunk(0).transactions);
810  }
811 }
812 
813 FUZZ_TARGET(clusterlin_postlinearize_tree)
814 {
815  // Verify expected properties of PostLinearize() on linearizations of graphs that form either
816  // an upright or reverse tree structure.
817 
818  // Construct a direction, RNG seed, and an arbitrary graph from the fuzz input.
819  SpanReader reader(buffer);
820  uint64_t rng_seed{0};
821  DepGraph<TestBitSet> depgraph_gen;
822  uint8_t direction{0};
823  try {
824  reader >> direction >> rng_seed >> Using<DepGraphFormatter>(depgraph_gen);
825  } catch (const std::ios_base::failure&) {}
826 
827  // Now construct a new graph, copying the nodes, but leaving only the first parent (even
828  // direction) or the first child (odd direction).
829  DepGraph<TestBitSet> depgraph_tree;
830  for (ClusterIndex i = 0; i < depgraph_gen.TxCount(); ++i) {
831  depgraph_tree.AddTransaction(depgraph_gen.FeeRate(i));
832  }
833  if (direction & 1) {
834  for (ClusterIndex i = 0; i < depgraph_gen.TxCount(); ++i) {
835  auto children = depgraph_gen.Descendants(i) - TestBitSet::Singleton(i);
836  // Remove descendants that are children of other descendants.
837  for (auto j : children) {
838  if (!children[j]) continue;
839  children -= depgraph_gen.Descendants(j);
840  children.Set(j);
841  }
842  if (children.Any()) depgraph_tree.AddDependency(i, children.First());
843  }
844  } else {
845  for (ClusterIndex i = 0; i < depgraph_gen.TxCount(); ++i) {
846  auto parents = depgraph_gen.Ancestors(i) - TestBitSet::Singleton(i);
847  // Remove ancestors that are parents of other ancestors.
848  for (auto j : parents) {
849  if (!parents[j]) continue;
850  parents -= depgraph_gen.Ancestors(j);
851  parents.Set(j);
852  }
853  if (parents.Any()) depgraph_tree.AddDependency(parents.First(), i);
854  }
855  }
856 
857  // Retrieve a linearization from the fuzz input.
858  std::vector<ClusterIndex> linearization;
859  linearization = ReadLinearization(depgraph_tree, reader);
860  SanityCheck(depgraph_tree, linearization);
861 
862  // Produce a postlinearized version.
863  auto post_linearization = linearization;
864  PostLinearize(depgraph_tree, post_linearization);
865  SanityCheck(depgraph_tree, post_linearization);
866 
867  // Compare diagrams.
868  auto chunking = ChunkLinearization(depgraph_tree, linearization);
869  auto post_chunking = ChunkLinearization(depgraph_tree, post_linearization);
870  auto cmp = CompareChunks(post_chunking, chunking);
871  assert(cmp >= 0);
872 
873  // Verify that post-linearizing again does not change the diagram. The result must be identical
874  // as post_linearization ought to be optimal already with a tree-structured graph.
875  auto post_post_linearization = post_linearization;
876  PostLinearize(depgraph_tree, post_linearization);
877  SanityCheck(depgraph_tree, post_linearization);
878  auto post_post_chunking = ChunkLinearization(depgraph_tree, post_post_linearization);
879  auto cmp_post = CompareChunks(post_post_chunking, post_chunking);
880  assert(cmp_post == 0);
881 
882  // Try to find an even better linearization directly. This must not change the diagram for the
883  // same reason.
884  auto [opt_linearization, _optimal] = Linearize(depgraph_tree, 100000, rng_seed, post_linearization);
885  auto opt_chunking = ChunkLinearization(depgraph_tree, opt_linearization);
886  auto cmp_opt = CompareChunks(opt_chunking, post_chunking);
887  assert(cmp_opt == 0);
888 }
889 
890 FUZZ_TARGET(clusterlin_postlinearize_moved_leaf)
891 {
892  // Verify that taking an existing linearization, and moving a leaf to the back, potentially
893  // increasing its fee, and then post-linearizing, results in something as good as the
894  // original. This guarantees that in an RBF that replaces a transaction with one of the same
895  // size but higher fee, applying the "remove conflicts, append new transaction, postlinearize"
896  // process will never worsen linearization quality.
897 
898  // Construct an arbitrary graph and a fee from the fuzz input.
899  SpanReader reader(buffer);
900  DepGraph<TestBitSet> depgraph;
901  int32_t fee_inc{0};
902  try {
903  uint64_t fee_inc_code;
904  reader >> Using<DepGraphFormatter>(depgraph) >> VARINT(fee_inc_code);
905  fee_inc = fee_inc_code & 0x3ffff;
906  } catch (const std::ios_base::failure&) {}
907  if (depgraph.TxCount() == 0) return;
908 
909  // Retrieve two linearizations from the fuzz input.
910  auto lin = ReadLinearization(depgraph, reader);
911  auto lin_leaf = ReadLinearization(depgraph, reader);
912 
913  // Construct a linearization identical to lin, but with the tail end of lin_leaf moved to the
914  // back.
915  std::vector<ClusterIndex> lin_moved;
916  for (auto i : lin) {
917  if (i != lin_leaf.back()) lin_moved.push_back(i);
918  }
919  lin_moved.push_back(lin_leaf.back());
920 
921  // Postlinearize lin_moved.
922  PostLinearize(depgraph, lin_moved);
923  SanityCheck(depgraph, lin_moved);
924 
925  // Compare diagrams (applying the fee delta after computing the old one).
926  auto old_chunking = ChunkLinearization(depgraph, lin);
927  depgraph.FeeRate(lin_leaf.back()).fee += fee_inc;
928  auto new_chunking = ChunkLinearization(depgraph, lin_moved);
929  auto cmp = CompareChunks(new_chunking, old_chunking);
930  assert(cmp >= 0);
931 }
932 
933 FUZZ_TARGET(clusterlin_merge)
934 {
935  // Construct an arbitrary graph from the fuzz input.
936  SpanReader reader(buffer);
937  DepGraph<TestBitSet> depgraph;
938  try {
939  reader >> Using<DepGraphFormatter>(depgraph);
940  } catch (const std::ios_base::failure&) {}
941 
942  // Retrieve two linearizations from the fuzz input.
943  auto lin1 = ReadLinearization(depgraph, reader);
944  auto lin2 = ReadLinearization(depgraph, reader);
945 
946  // Merge the two.
947  auto lin_merged = MergeLinearizations(depgraph, lin1, lin2);
948 
949  // Compute chunkings and compare.
950  auto chunking1 = ChunkLinearization(depgraph, lin1);
951  auto chunking2 = ChunkLinearization(depgraph, lin2);
952  auto chunking_merged = ChunkLinearization(depgraph, lin_merged);
953  auto cmp1 = CompareChunks(chunking_merged, chunking1);
954  assert(cmp1 >= 0);
955  auto cmp2 = CompareChunks(chunking_merged, chunking2);
956  assert(cmp2 >= 0);
957 }
#define VARINT(obj)
Definition: serialize.h:498
A set of transactions together with their aggregate feerate.
int ret
Data structure encapsulating the chunking of a linearization, permitting removal of subsets...
ClusterIndex AddTransaction(const FeeFrac &feefrac) noexcept
Add a new unconnected transaction to this transaction graph (at the end), and return its ClusterIndex...
assert(!tx.IsCoinBase())
SetType FindConnectedComponent(const SetType &todo) const noexcept
Find some connected component within the subset "todo" of this graph.
bool AllDone() const noexcept
Check whether any unlinearized transactions remain.
FeeFrac feerate
Their combined fee and size.
uint32_t ClusterIndex
Data type to represent transaction indices in clusters.
const char * prefix
Definition: rest.cpp:1007
static constexpr auto MAX_SIMPLE_ITERATIONS
SetInfo< SetType > IntersectPrefixes(const SetInfo< SetType > &subset) const noexcept
Find the shortest intersection between subset and the prefixes of remaining chunks of the linearizati...
const SetType & Descendants(ClusterIndex i) const noexcept
Get the descendants of a given transaction i.
const SetInfo< SetType > & GetChunk(ClusterIndex n) const noexcept
Access a chunk.
#define LIMITED_WHILE(condition, limit)
Can be used to limit a theoretically unbounded loop.
Definition: fuzz.h:22
void MarkDone(SetType select) noexcept
Remove a set of transactions from the set of to-be-linearized ones.
Minimal stream for reading from an existing byte array by Span.
Definition: streams.h:100
bool AllDone() const noexcept
Check whether any unlinearized transactions remain.
#define LIFETIMEBOUND
Definition: attributes.h:16
void MarkDone(SetType subset) noexcept
Remove some subset of transactions from the linearization.
bool IsConnected(const SetType &subset) const noexcept
Determine if a subset is connected.
Class encapsulating the state needed to find the best remaining ancestor set.
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.
bool IsEmpty() const noexcept
Check if this is empty (size and fee are 0).
Definition: feefrac.h:76
FUZZ_TARGET(clusterlin_add_dependency)
std::partial_ordering CompareChunks(Span< const FeeFrac > chunks0, Span< const FeeFrac > chunks1)
Compare the feerate diagrams implied by the provided sorted chunks data.
Definition: feefrac.cpp:10
Data structure storing a fee and size, ordered by increasing fee/size.
Definition: feefrac.h:38
void AddDependency(ClusterIndex parent, ClusterIndex child) noexcept
Modify this transaction graph, adding a dependency between a specified parent and child...
Data structure that holds a transaction graph&#39;s preprocessed data (fee, size, ancestors, descendants).
std::pair< std::vector< ClusterIndex >, bool > Linearize(const DepGraph< SetType > &depgraph, uint64_t max_iterations, uint64_t rng_seed, Span< const ClusterIndex > old_linearization={}) noexcept
Find or improve a linearization for a cluster.
SetInfo< SetType > FindCandidateSet() const noexcept
Find the best (highest-feerate, smallest among those in case of a tie) ancestor set among the remaini...
static std::vector< std::string > split(const std::string &str, const std::string &delims=" \)
Definition: subprocess.h:303
ClusterIndex NumChunksLeft() const noexcept
Determine how many chunks remain in the linearization.
std::vector< ClusterIndex > MergeLinearizations(const DepGraph< SetType > &depgraph, Span< const ClusterIndex > lin1, Span< const ClusterIndex > lin2)
Merge two linearizations for the same cluster into one that is as good as both.
void PostLinearize(const DepGraph< SetType > &depgraph, Span< ClusterIndex > linearization)
Improve a given linearization.
const FeeFrac & FeeRate(ClusterIndex i) const noexcept
Get the feerate of a given transaction i.
T ConsumeIntegralInRange(T min, T max)
std::vector< std::pair< FeeFrac, SetType > > Cluster
Data type to represent cluster input.
std::vector< FeeFrac > ChunkLinearization(const DepGraph< SetType > &depgraph, Span< const ClusterIndex > linearization) noexcept
Compute the feerates of the chunks of linearization.
const SetType & Ancestors(ClusterIndex i) const noexcept
Get the ancestors of a given transaction i.
auto TxCount() const noexcept
Get the number of transactions in the graph.
void MarkDone(const SetType &done) noexcept
Remove a subset of transactions from the cluster being linearized.
Class encapsulating the state needed to perform search for good candidate sets.
void AppendTopo(std::vector< ClusterIndex > &list, const SetType &select) const noexcept
Append the entries of select to list in a topologically valid order.