From 5ce280074512f79a4b3851d8d453f337fa97be46 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Tue, 14 Oct 2025 15:34:42 -0400 Subject: [PATCH] clusterlin: randomize equal-feerate parts of linearization (privacy) This places equal-feerate chunks (with no dependencies between them) in random order in the linearization output, hiding information about DepGraph insertion order from the output. Likewise, it randomizes the order of transactions within chunks for the same reason. --- src/cluster_linearize.h | 36 ++++++++++++++++++++++++------------ src/txgraph.cpp | 5 +++-- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/cluster_linearize.h b/src/cluster_linearize.h index 6639244f35e..8b4d0d2278f 100644 --- a/src/cluster_linearize.h +++ b/src/cluster_linearize.h @@ -696,6 +696,12 @@ public: * - Inside the selected chunk (see above), among the dependencies whose top feerate is strictly * higher than its bottom feerate in the selected chunk, if any, a uniformly random dependency * is deactivated. + * + * - How to decide the exact output linearization: + * - When there are multiple equal-feerate chunks with no dependencies between them, output a + * uniformly random one among the ones with no missing dependent chunks first. + * - Within chunks, repeatedly pick a uniformly random transaction among those with no missing + * dependencies. */ template class SpanningForestState @@ -1178,8 +1184,8 @@ public: std::vector ret; ret.reserve(m_transaction_idxs.Count()); /** A heap with all chunks (by representative) that can currently be included, sorted by - * chunk feerate. */ - std::vector ready_chunks; + * chunk feerate and a random tie-breaker. */ + std::vector> ready_chunks; /** Information about chunks: * - The first value is only used for chunk representatives, and counts the number of * unmet dependencies this chunk has on other chunks (not including dependencies within @@ -1205,25 +1211,27 @@ public: } // Construct a heap with all chunks that have no out-of-chunk dependencies. /** Comparison function for the heap. */ - auto chunk_cmp_fn = [&](TxIdx a, TxIdx b) noexcept { - auto& chunk_a = m_tx_data[a]; - auto& chunk_b = m_tx_data[b]; - Assume(chunk_a.chunk_rep == a); - Assume(chunk_b.chunk_rep == b); + auto chunk_cmp_fn = [&](const std::pair& a, const std::pair& b) noexcept { + auto& chunk_a = m_tx_data[a.first]; + auto& chunk_b = m_tx_data[b.first]; + Assume(chunk_a.chunk_rep == a.first); + Assume(chunk_b.chunk_rep == b.first); // First sort by chunk feerate. if (chunk_a.chunk_setinfo.feerate != chunk_b.chunk_setinfo.feerate) { return chunk_a.chunk_setinfo.feerate < chunk_b.chunk_setinfo.feerate; } - // Tie-break by chunk representative. - return a < b; + // Tie-break randomly. + if (a.second != b.second) return a.second < b.second; + // Lastly, tie-break by chunk representative. + return a.first < b.first; }; for (TxIdx chunk_rep : chunk_reps) { - if (chunk_deps[chunk_rep].first == 0) ready_chunks.push_back(chunk_rep); + if (chunk_deps[chunk_rep].first == 0) ready_chunks.emplace_back(chunk_rep, m_rng.rand64()); } std::make_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); // Pop chunks off the heap, highest-feerate ones first. while (!ready_chunks.empty()) { - auto chunk_rep = ready_chunks.front(); + auto [chunk_rep, _rnd] = ready_chunks.front(); std::pop_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); ready_chunks.pop_back(); Assume(m_tx_data[chunk_rep].chunk_rep == chunk_rep); @@ -1239,6 +1247,10 @@ public: // Pick transactions from the ready queue, append them to linearization, and decrement // dependency counts. while (!ready_tx.empty()) { + // Move a random queue element to the back. + auto pos = m_rng.randrange(ready_tx.size()); + if (pos != ready_tx.size() - 1) std::swap(ready_tx.back(), ready_tx[pos]); + // Pop from the back. auto tx_idx = ready_tx.back(); Assume(chunk_txn[tx_idx]); ready_tx.pop_back(); @@ -1259,7 +1271,7 @@ public: Assume(chunk_deps[chl_data.chunk_rep].first > 0); if (--chunk_deps[chl_data.chunk_rep].first == 0) { // Child chunk has no dependencies left. Add it to the chunk heap. - ready_chunks.push_back(chl_data.chunk_rep); + ready_chunks.emplace_back(chl_data.chunk_rep, m_rng.rand64()); std::push_heap(ready_chunks.begin(), ready_chunks.end(), chunk_cmp_fn); } } diff --git a/src/txgraph.cpp b/src/txgraph.cpp index 2004705f647..3bc5cae6740 100644 --- a/src/txgraph.cpp +++ b/src/txgraph.cpp @@ -2091,8 +2091,9 @@ std::pair GenericClusterImpl::Relinearize(TxGraphImpl& graph, in // Invoke the actual linearization algorithm (passing in the existing one). uint64_t rng_seed = graph.m_rng.rand64(); auto [linearization, optimal, cost] = Linearize(m_depgraph, max_iters, rng_seed, m_linearization); - // Postlinearize to guarantee that the chunks of the resulting linearization are all connected. - // (SFL currently does not guarantee connected chunks even when optimal). + // Postlinearize to undo some of the non-determinism caused by randomizing the linearization. + // This also guarantees that all chunks are connected (which is not guaranteed by SFL + // currently, even when optimal). PostLinearize(m_depgraph, linearization); // Update the linearization. m_linearization = std::move(linearization);