diff --git a/src/bench/cluster_linearize.cpp b/src/bench/cluster_linearize.cpp index cf94580ab20..bfab5c729bd 100644 --- a/src/bench/cluster_linearize.cpp +++ b/src/bench/cluster_linearize.cpp @@ -101,8 +101,9 @@ void BenchLinearizePerIterWorstCase(ClusterIndex ntx, benchmark::Bench& bench) { const auto depgraph = MakeHardGraph(ntx); const auto iter_limit = std::min(10000, uint64_t{1} << (ntx / 2 - 1)); + uint64_t rng_seed = 0; bench.batch(iter_limit).unit("iters").run([&] { - SearchCandidateFinder finder(depgraph); + SearchCandidateFinder finder(depgraph, rng_seed++); auto [candidate, iters_performed] = finder.FindCandidateSet(iter_limit, {}); assert(iters_performed == iter_limit); }); @@ -122,8 +123,9 @@ template void BenchLinearizeNoItersWorstCase(ClusterIndex ntx, benchmark::Bench& bench) { const auto depgraph = MakeLinearGraph(ntx); + uint64_t rng_seed = 0; bench.run([&] { - Linearize(depgraph, /*max_iterations=*/0); + Linearize(depgraph, /*max_iterations=*/0, rng_seed++); }); } diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 52880529f6f..f689e7e33a5 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -225,6 +226,13 @@ struct SetInfo return {transactions | txn, feerate + depgraph.FeeRate(txn - transactions)}; } + /** Swap two SetInfo objects. */ + friend void swap(SetInfo& a, SetInfo& b) noexcept + { + swap(a.transactions, b.transactions); + swap(a.feerate, b.feerate); + } + /** Permit equality testing. */ friend bool operator==(const SetInfo&, const SetInfo&) noexcept = default; }; @@ -356,6 +364,8 @@ public: template class SearchCandidateFinder { + /** Internal RNG. */ + InsecureRandomContext m_rng; /** Internal dependency graph for the cluster. */ const DepGraph& m_depgraph; /** Which transactions are left to do (sorted indices). */ @@ -365,10 +375,12 @@ public: /** Construct a candidate finder for a graph. * * @param[in] depgraph Dependency graph for the to-be-linearized cluster. + * @param[in] rng_seed A random seed to control the search order. * * Complexity: O(1). */ - SearchCandidateFinder(const DepGraph& depgraph LIFETIMEBOUND) noexcept : + SearchCandidateFinder(const DepGraph& depgraph LIFETIMEBOUND, uint64_t rng_seed) noexcept : + m_rng(rng_seed), m_depgraph(depgraph), m_todo(SetType::Fill(depgraph.TxCount())) {} @@ -413,6 +425,13 @@ public: /** Construct a new work item. */ WorkItem(SetInfo&& i, SetType&& u) noexcept : inc(std::move(i)), und(std::move(u)) {} + + /** Swap two WorkItems. */ + void Swap(WorkItem& other) noexcept + { + swap(inc, other.inc); + swap(und, other.und); + } }; /** The queue of work items. */ @@ -493,9 +512,14 @@ public: // (BFS) corresponds to always taking from the front, which potentially uses more memory // (up to exponential in the transaction count), but seems to work better in practice. // - // The approach here combines the two: use BFS until the queue grows too large, at which - // point we temporarily switch to DFS until the size shrinks again. + // The approach here combines the two: use BFS (plus random swapping) until the queue grows + // too large, at which point we temporarily switch to DFS until the size shrinks again. while (!queue.empty()) { + // Randomly swap the first two items to randomize the search order. + if (queue.size() > 1 && m_rng.randbool()) { + queue[0].Swap(queue[1]); + } + // Processing the first queue item, and then using DFS for everything it gives rise to, // may increase the queue size by the number of undecided elements in there, minus 1 // for the first queue item being removed. Thus, only when that pushes the queue over @@ -534,6 +558,9 @@ public: * * @param[in] depgraph Dependency graph of the cluster to be linearized. * @param[in] max_iterations Upper bound on the number of optimization steps that will be done. + * @param[in] rng_seed A random number seed to control search order. This prevents peers + * from predicting exactly which clusters would be hard for us to + * linearize. * @return A pair of: * - The resulting linearization. * - A boolean indicating whether the result is guaranteed to be @@ -542,7 +569,7 @@ public: * Complexity: O(N * min(max_iterations + N, 2^N)) where N=depgraph.TxCount(). */ template -std::pair, bool> Linearize(const DepGraph& depgraph, uint64_t max_iterations) noexcept +std::pair, bool> Linearize(const DepGraph& depgraph, uint64_t max_iterations, uint64_t rng_seed) noexcept { if (depgraph.TxCount() == 0) return {{}, true}; @@ -550,7 +577,7 @@ std::pair, bool> Linearize(const DepGraph& de std::vector linearization; AncestorCandidateFinder anc_finder(depgraph); - SearchCandidateFinder src_finder(depgraph); + SearchCandidateFinder src_finder(depgraph, rng_seed); linearization.reserve(depgraph.TxCount()); bool optimal = true; diff --git a/src/test/fuzz/cluster_linearize.cpp b/src/test/fuzz/cluster_linearize.cpp index e4d9a59b8d3..6157291364b 100644 --- a/src/test/fuzz/cluster_linearize.cpp +++ b/src/test/fuzz/cluster_linearize.cpp @@ -391,15 +391,16 @@ FUZZ_TARGET(clusterlin_search_finder) // and comparing with the results from SimpleCandidateFinder, ExhaustiveCandidateFinder, and // AncestorCandidateFinder. - // Retrieve a depgraph from the fuzz input. + // Retrieve an RNG seed and a depgraph from the fuzz input. SpanReader reader(buffer); DepGraph depgraph; + uint64_t rng_seed{0}; try { - reader >> Using(depgraph); + reader >> Using(depgraph) >> rng_seed; } catch (const std::ios_base::failure&) {} // Instantiate ALL the candidate finders. - SearchCandidateFinder src_finder(depgraph); + SearchCandidateFinder src_finder(depgraph, rng_seed); SimpleCandidateFinder smp_finder(depgraph); ExhaustiveCandidateFinder exh_finder(depgraph); AncestorCandidateFinder anc_finder(depgraph); @@ -487,17 +488,18 @@ FUZZ_TARGET(clusterlin_linearize) { // Verify the behavior of Linearize(). - // Retrieve an iteration count, and a depgraph from the fuzz input. + // Retrieve an RNG seed, an iteration count, and a depgraph from the fuzz input. SpanReader reader(buffer); DepGraph depgraph; + uint64_t rng_seed{0}; uint64_t iter_count{0}; try { - reader >> VARINT(iter_count) >> Using(depgraph); + reader >> VARINT(iter_count) >> Using(depgraph) >> rng_seed; } catch (const std::ios_base::failure&) {} // Invoke Linearize(). iter_count &= 0x7ffff; - auto [linearization, optimal] = Linearize(depgraph, iter_count); + auto [linearization, optimal] = Linearize(depgraph, iter_count, rng_seed); SanityCheck(depgraph, linearization); auto chunking = ChunkLinearization(depgraph, linearization);