Bitcoin Core 28.0.0
P2P Digital Currency
Loading...
Searching...
No Matches
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
19using namespace cluster_linearize;
20
21namespace {
22
28template<typename SetType>
29class SimpleCandidateFinder
30{
32 const DepGraph<SetType>& m_depgraph;
34 SetType m_todo;
35
36public:
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
98template<typename SetType>
99class ExhaustiveCandidateFinder
100{
102 const DepGraph<SetType>& m_depgraph;
104 SetType m_todo;
105
106public:
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
150template<typename SetType>
151std::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
169template<typename SetType>
170SetType 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
187template<typename BS>
188std::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
226FUZZ_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
254FUZZ_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
281FUZZ_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
297FUZZ_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
372FUZZ_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
414FUZZ_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
463static constexpr auto MAX_SIMPLE_ITERATIONS = 300000;
464
465FUZZ_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
567FUZZ_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
684FUZZ_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
770FUZZ_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
813FUZZ_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
890FUZZ_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
933FUZZ_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 LIFETIMEBOUND
Definition attributes.h:16
int ret
T ConsumeIntegralInRange(T min, T max)
Minimal stream for reading from an existing byte array by Span.
Definition streams.h:101
Class encapsulating the state needed to find the best remaining ancestor set.
void MarkDone(SetType select) noexcept
Remove a set of transactions from the set of to-be-linearized ones.
SetInfo< SetType > FindCandidateSet() const noexcept
Find the best (highest-feerate, smallest among those in case of a tie) ancestor set among the remaini...
bool AllDone() const noexcept
Check whether any unlinearized transactions remain.
Data structure that holds a transaction graph's preprocessed data (fee, size, ancestors,...
void AppendTopo(std::vector< ClusterIndex > &list, const SetType &select) const noexcept
Append the entries of select to list in a topologically valid order.
void AddDependency(ClusterIndex parent, ClusterIndex child) noexcept
Modify this transaction graph, adding a dependency between a specified parent and child.
const FeeFrac & FeeRate(ClusterIndex i) const noexcept
Get the feerate of a given transaction i.
const SetType & Ancestors(ClusterIndex i) const noexcept
Get the ancestors of a given transaction i.
bool IsConnected(const SetType &subset) const noexcept
Determine if a subset is connected.
SetType FindConnectedComponent(const SetType &todo) const noexcept
Find some connected component within the subset "todo" of this graph.
auto TxCount() const noexcept
Get the number of transactions in the graph.
const SetType & Descendants(ClusterIndex i) const noexcept
Get the descendants of a given transaction i.
ClusterIndex AddTransaction(const FeeFrac &feefrac) noexcept
Add a new unconnected transaction to this transaction graph (at the end), and return its ClusterIndex...
Data structure encapsulating the chunking of a linearization, permitting removal of subsets.
ClusterIndex NumChunksLeft() const noexcept
Determine how many chunks remain in the linearization.
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 SetInfo< SetType > & GetChunk(ClusterIndex n) const noexcept
Access a chunk.
void MarkDone(SetType subset) noexcept
Remove some subset of transactions from the linearization.
Class encapsulating the state needed to perform search for good candidate sets.
bool AllDone() const noexcept
Check whether any unlinearized transactions remain.
void MarkDone(const SetType &done) noexcept
Remove a subset of transactions from the cluster being linearized.
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.
#define FUZZ_TARGET(...)
Definition fuzz.h:35
#define LIMITED_WHILE(condition, limit)
Can be used to limit a theoretically unbounded loop.
Definition fuzz.h:22
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.
uint32_t ClusterIndex
Data type to represent transaction indices in clusters.
std::vector< FeeFrac > ChunkLinearization(const DepGraph< SetType > &depgraph, Span< const ClusterIndex > linearization) noexcept
Compute the feerates of the chunks of linearization.
std::vector< std::pair< FeeFrac, SetType > > Cluster
Data type to represent cluster input.
void PostLinearize(const DepGraph< SetType > &depgraph, Span< ClusterIndex > linearization)
Improve a given 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.
const char * prefix
Definition rest.cpp:1007
#define VARINT(obj)
Definition serialize.h:498
static Wrapper< Formatter, T & > Using(T &&t)
Cause serialization/deserialization of an object to be done using a specified formatter class.
Definition serialize.h:495
Data structure storing a fee and size, ordered by increasing fee/size.
Definition feefrac.h:39
int64_t fee
Definition feefrac.h:63
bool IsEmpty() const noexcept
Check if this is empty (size and fee are 0).
Definition feefrac.h:76
A set of transactions together with their aggregate feerate.
FeeFrac feerate
Their combined fee and size.
SetType transactions
The transactions in the set.
static constexpr auto MAX_SIMPLE_ITERATIONS
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
assert(!tx.IsCoinBase())