Skip to content

Commit d5918dc

Browse files
committed
clusterlin: randomize the SearchCandidateFinder search order
To make search non-deterministic, change the BFS logic from always picking the first queue item to randomly picking the first or second queue item.
1 parent 991ff9a commit d5918dc

File tree

3 files changed

+44
-13
lines changed

3 files changed

+44
-13
lines changed

src/bench/cluster_linearize.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ void BenchLinearizePerIterWorstCase(ClusterIndex ntx, benchmark::Bench& bench)
101101
{
102102
const auto depgraph = MakeHardGraph<SetType>(ntx);
103103
const auto iter_limit = std::min<uint64_t>(10000, uint64_t{1} << (ntx / 2 - 1));
104+
uint64_t rng_seed = 0;
104105
bench.batch(iter_limit).unit("iters").run([&] {
105-
SearchCandidateFinder finder(depgraph);
106+
SearchCandidateFinder finder(depgraph, rng_seed++);
106107
auto [candidate, iters_performed] = finder.FindCandidateSet(iter_limit, {});
107108
assert(iters_performed == iter_limit);
108109
});
@@ -122,8 +123,9 @@ template<typename SetType>
122123
void BenchLinearizeNoItersWorstCase(ClusterIndex ntx, benchmark::Bench& bench)
123124
{
124125
const auto depgraph = MakeLinearGraph<SetType>(ntx);
126+
uint64_t rng_seed = 0;
125127
bench.run([&] {
126-
Linearize(depgraph, /*max_iterations=*/0);
128+
Linearize(depgraph, /*max_iterations=*/0, rng_seed++);
127129
});
128130
}
129131

src/cluster_linearize.h

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <vector>
1313
#include <utility>
1414

15+
#include <random.h>
1516
#include <util/feefrac.h>
1617
#include <util/vecdeque.h>
1718

@@ -225,6 +226,13 @@ struct SetInfo
225226
return {transactions | txn, feerate + depgraph.FeeRate(txn - transactions)};
226227
}
227228

229+
/** Swap two SetInfo objects. */
230+
friend void swap(SetInfo& a, SetInfo& b) noexcept
231+
{
232+
swap(a.transactions, b.transactions);
233+
swap(a.feerate, b.feerate);
234+
}
235+
228236
/** Permit equality testing. */
229237
friend bool operator==(const SetInfo&, const SetInfo&) noexcept = default;
230238
};
@@ -356,6 +364,8 @@ class AncestorCandidateFinder
356364
template<typename SetType>
357365
class SearchCandidateFinder
358366
{
367+
/** Internal RNG. */
368+
InsecureRandomContext m_rng;
359369
/** Internal dependency graph for the cluster. */
360370
const DepGraph<SetType>& m_depgraph;
361371
/** Which transactions are left to do (sorted indices). */
@@ -365,10 +375,12 @@ class SearchCandidateFinder
365375
/** Construct a candidate finder for a graph.
366376
*
367377
* @param[in] depgraph Dependency graph for the to-be-linearized cluster.
378+
* @param[in] rng_seed A random seed to control the search order.
368379
*
369380
* Complexity: O(1).
370381
*/
371-
SearchCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND) noexcept :
382+
SearchCandidateFinder(const DepGraph<SetType>& depgraph LIFETIMEBOUND, uint64_t rng_seed) noexcept :
383+
m_rng(rng_seed),
372384
m_depgraph(depgraph),
373385
m_todo(SetType::Fill(depgraph.TxCount())) {}
374386

@@ -413,6 +425,13 @@ class SearchCandidateFinder
413425
/** Construct a new work item. */
414426
WorkItem(SetInfo<SetType>&& i, SetType&& u) noexcept :
415427
inc(std::move(i)), und(std::move(u)) {}
428+
429+
/** Swap two WorkItems. */
430+
void Swap(WorkItem& other) noexcept
431+
{
432+
swap(inc, other.inc);
433+
swap(und, other.und);
434+
}
416435
};
417436

418437
/** The queue of work items. */
@@ -493,9 +512,14 @@ class SearchCandidateFinder
493512
// (BFS) corresponds to always taking from the front, which potentially uses more memory
494513
// (up to exponential in the transaction count), but seems to work better in practice.
495514
//
496-
// The approach here combines the two: use BFS until the queue grows too large, at which
497-
// point we temporarily switch to DFS until the size shrinks again.
515+
// The approach here combines the two: use BFS (plus random swapping) until the queue grows
516+
// too large, at which point we temporarily switch to DFS until the size shrinks again.
498517
while (!queue.empty()) {
518+
// Randomly swap the first two items to randomize the search order.
519+
if (queue.size() > 1 && m_rng.randbool()) {
520+
queue[0].Swap(queue[1]);
521+
}
522+
499523
// Processing the first queue item, and then using DFS for everything it gives rise to,
500524
// may increase the queue size by the number of undecided elements in there, minus 1
501525
// for the first queue item being removed. Thus, only when that pushes the queue over
@@ -534,6 +558,9 @@ class SearchCandidateFinder
534558
*
535559
* @param[in] depgraph Dependency graph of the cluster to be linearized.
536560
* @param[in] max_iterations Upper bound on the number of optimization steps that will be done.
561+
* @param[in] rng_seed A random number seed to control search order. This prevents peers
562+
* from predicting exactly which clusters would be hard for us to
563+
* linearize.
537564
* @return A pair of:
538565
* - The resulting linearization.
539566
* - A boolean indicating whether the result is guaranteed to be
@@ -542,15 +569,15 @@ class SearchCandidateFinder
542569
* Complexity: O(N * min(max_iterations + N, 2^N)) where N=depgraph.TxCount().
543570
*/
544571
template<typename SetType>
545-
std::pair<std::vector<ClusterIndex>, bool> Linearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations) noexcept
572+
std::pair<std::vector<ClusterIndex>, bool> Linearize(const DepGraph<SetType>& depgraph, uint64_t max_iterations, uint64_t rng_seed) noexcept
546573
{
547574
if (depgraph.TxCount() == 0) return {{}, true};
548575

549576
uint64_t iterations_left = max_iterations;
550577
std::vector<ClusterIndex> linearization;
551578

552579
AncestorCandidateFinder anc_finder(depgraph);
553-
SearchCandidateFinder src_finder(depgraph);
580+
SearchCandidateFinder src_finder(depgraph, rng_seed);
554581
linearization.reserve(depgraph.TxCount());
555582
bool optimal = true;
556583

src/test/fuzz/cluster_linearize.cpp

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -391,15 +391,16 @@ FUZZ_TARGET(clusterlin_search_finder)
391391
// and comparing with the results from SimpleCandidateFinder, ExhaustiveCandidateFinder, and
392392
// AncestorCandidateFinder.
393393

394-
// Retrieve a depgraph from the fuzz input.
394+
// Retrieve an RNG seed and a depgraph from the fuzz input.
395395
SpanReader reader(buffer);
396396
DepGraph<TestBitSet> depgraph;
397+
uint64_t rng_seed{0};
397398
try {
398-
reader >> Using<DepGraphFormatter>(depgraph);
399+
reader >> Using<DepGraphFormatter>(depgraph) >> rng_seed;
399400
} catch (const std::ios_base::failure&) {}
400401

401402
// Instantiate ALL the candidate finders.
402-
SearchCandidateFinder src_finder(depgraph);
403+
SearchCandidateFinder src_finder(depgraph, rng_seed);
403404
SimpleCandidateFinder smp_finder(depgraph);
404405
ExhaustiveCandidateFinder exh_finder(depgraph);
405406
AncestorCandidateFinder anc_finder(depgraph);
@@ -487,17 +488,18 @@ FUZZ_TARGET(clusterlin_linearize)
487488
{
488489
// Verify the behavior of Linearize().
489490

490-
// Retrieve an iteration count, and a depgraph from the fuzz input.
491+
// Retrieve an RNG seed, an iteration count, and a depgraph from the fuzz input.
491492
SpanReader reader(buffer);
492493
DepGraph<TestBitSet> depgraph;
494+
uint64_t rng_seed{0};
493495
uint64_t iter_count{0};
494496
try {
495-
reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph);
497+
reader >> VARINT(iter_count) >> Using<DepGraphFormatter>(depgraph) >> rng_seed;
496498
} catch (const std::ios_base::failure&) {}
497499

498500
// Invoke Linearize().
499501
iter_count &= 0x7ffff;
500-
auto [linearization, optimal] = Linearize(depgraph, iter_count);
502+
auto [linearization, optimal] = Linearize(depgraph, iter_count, rng_seed);
501503
SanityCheck(depgraph, linearization);
502504
auto chunking = ChunkLinearization(depgraph, linearization);
503505

0 commit comments

Comments
 (0)