Skip to content

Commit 87ab691

Browse files
committed
Merge bitcoin/bitcoin#31553: cluster mempool: add TxGraph reorg functionality
1632fc1 txgraph: Track multiple potential would-be clusters in Trim (improvement) (Pieter Wuille) 4608df3 txgraph: add Trim benchmark (benchmark) (Pieter Wuille) 9c436ff txgraph: add fuzz test scenario that avoids cycles inside Trim() (tests) (Pieter Wuille) 938e86f txgraph: add unit test for TxGraph::Trim (tests) (glozow) a04e205 txgraph: Add ability to trim oversized clusters (feature) (Pieter Wuille) eabcd0e txgraph: remove unnecessary m_group_oversized (simplification) (Greg Sanders) 19b14e6 txgraph: Permit transactions that exceed cluster size limit (feature) (Pieter Wuille) c4287b9 txgraph: Add ability to configure maximum cluster size/weight (feature) (Pieter Wuille) Pull request description: Part of cluster mempool (#30289). During reorganisations, it is possible that dependencies get added which would result in clusters that violate policy limits (cluster count, cluster weight), when linking the new from-block transactions to the old from-mempool transactions. Unlike RBF scenarios, we cannot simply reject the changes when they are due to received blocks. To accommodate this, add a `TxGraph::Trim()`, which removes some subset of transactions (including descendants) in order to make all resulting clusters satisfy the limits. Conceptually, the way this is done is by defining a rudimentary linearization for the entire would-be too-large cluster, iterating it from beginning to end, and reasoning about the counts and weights of the clusters that would be reached using transactions up to that point. If a transaction is encountered whose addition would violate the limit, it is removed, together with all its descendants. This rudimentary linearization is like a merge sort of the chunks of the clusters being combined, but respecting topology. More specifically, it is continuously picking the highest-chunk-feerate remaining transaction among those which have no unmet dependencies left. For efficiency, this rudimentary linearization is computed lazily, by putting all viable transactions in a heap, sorted by chunk feerate, and adding new transactions to it as they become viable. The `Trim()` function is rather unusual compared to the `TxGraph` functionality added in previous PRs, in that `Trim()` makes it own decisions about what the resulting graph contents will be, without good specification of how it makes that decision - it is just a best-effort attempt (which is improved in the last commit). All other `TxGraph` mutators are simply to inform the graph about changes the calling mempool code decided on; this one lets the decision be made by txgraph. As part of this, the "oversized" property is expanded to also encompass a configurable cluster weight limit (in addition to cluster count limit). ACKs for top commit: instagibbs: reACK 1632fc1 glozow: reACK 1632fc1 via range-diff ismaelsadeeq: reACK 1632fc1 🛰️ Tree-SHA512: ccacb54be8ad622bd2717905fc9b7e42aea4b07f824de1924da9237027a97a9a2f1b862bc6a791cbd2e1a01897ad2c7c73c398a2d5ccbce90bfbeac0bcebc9ce
2 parents d33c111 + 1632fc1 commit 87ab691

File tree

7 files changed

+1061
-60
lines changed

7 files changed

+1061
-60
lines changed

src/bench/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ add_executable(bench_bitcoin
4848
sign_transaction.cpp
4949
streams_findbyte.cpp
5050
strencodings.cpp
51+
txgraph.cpp
5152
util_time.cpp
5253
verify_script.cpp
5354
xor.cpp

src/bench/txgraph.cpp

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 <bench/bench.h>
6+
#include <random.h>
7+
#include <txgraph.h>
8+
#include <util/feefrac.h>
9+
10+
#include <cassert>
11+
#include <cstdint>
12+
13+
namespace {
14+
15+
void BenchTxGraphTrim(benchmark::Bench& bench)
16+
{
17+
// The from-block transactions consist of 1000 fully linear clusters, each with 64
18+
// transactions. The mempool contains 11 transactions that together merge all of these into
19+
// a single cluster.
20+
//
21+
// (1000 chains of 64 transactions, 64000 T's total)
22+
//
23+
// T T T T T T T T
24+
// | | | | | | | |
25+
// T T T T T T T T
26+
// | | | | | | | |
27+
// T T T T T T T T
28+
// | | | | | | | |
29+
// T T T T T T T T
30+
// (64 long) (64 long) (64 long) (64 long) (64 long) (64 long) (64 long) (64 long)
31+
// | | | | | | | |
32+
// | | / \ | / \ | | /
33+
// \----------+--------/ \--------+--------/ \--------+-----+----+--------/
34+
// | | |
35+
// B B B
36+
//
37+
// (11 B's, each attaching to up to 100 chains of 64 T's)
38+
//
39+
/** The maximum cluster count used in this test. */
40+
static constexpr int MAX_CLUSTER_COUNT = 64;
41+
/** The number of "top" (from-block) chains of transactions. */
42+
static constexpr int NUM_TOP_CHAINS = 1000;
43+
/** The number of transactions per top chain. */
44+
static constexpr int NUM_TX_PER_TOP_CHAIN = MAX_CLUSTER_COUNT;
45+
/** The (maximum) number of dependencies per bottom transaction. */
46+
static constexpr int NUM_DEPS_PER_BOTTOM_TX = 100;
47+
/** Set a very large cluster size limit so that only the count limit is triggered. */
48+
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
49+
50+
/** Refs to all top transactions. */
51+
std::vector<TxGraph::Ref> top_refs;
52+
/** Refs to all bottom transactions. */
53+
std::vector<TxGraph::Ref> bottom_refs;
54+
/** Indexes into top_refs for some transaction of each component, in arbitrary order.
55+
* Initially these are the last transactions in each chains, but as bottom transactions are
56+
* added, entries will be removed when they get merged, and randomized. */
57+
std::vector<size_t> top_components;
58+
59+
InsecureRandomContext rng(11);
60+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
61+
62+
// Construct the top chains.
63+
for (int chain = 0; chain < NUM_TOP_CHAINS; ++chain) {
64+
for (int chaintx = 0; chaintx < NUM_TX_PER_TOP_CHAIN; ++chaintx) {
65+
int64_t fee = rng.randbits<27>() + 100;
66+
FeePerWeight feerate{fee, 1};
67+
top_refs.push_back(graph->AddTransaction(feerate));
68+
// Add internal dependencies linking the chain transactions together.
69+
if (chaintx > 0) {
70+
graph->AddDependency(*(top_refs.rbegin()), *(top_refs.rbegin() + 1));
71+
}
72+
}
73+
// Remember the last transaction in each chain, to attach the bottom transactions to.
74+
top_components.push_back(top_refs.size() - 1);
75+
}
76+
77+
// Make the graph linearize all clusters acceptably.
78+
graph->GetBlockBuilder();
79+
80+
// Construct the bottom transactions, and dependencies to the top chains.
81+
while (top_components.size() > 1) {
82+
// Construct the transaction.
83+
int64_t fee = rng.randbits<27>() + 100;
84+
FeePerWeight feerate{fee, 1};
85+
auto bottom_tx = graph->AddTransaction(feerate);
86+
// Determine the number of dependencies this transaction will have.
87+
int deps = std::min<int>(NUM_DEPS_PER_BOTTOM_TX, top_components.size());
88+
for (int dep = 0; dep < deps; ++dep) {
89+
// Pick an transaction in top_components to attach to.
90+
auto idx = rng.randrange(top_components.size());
91+
// Add dependency.
92+
graph->AddDependency(/*parent=*/top_refs[top_components[idx]], /*child=*/bottom_tx);
93+
// Unless this is the last dependency being added, remove from top_components, as
94+
// the component will be merged with that one.
95+
if (dep < deps - 1) {
96+
// Move entry top the back.
97+
if (idx != top_components.size() - 1) std::swap(top_components.back(), top_components[idx]);
98+
// And pop it.
99+
top_components.pop_back();
100+
}
101+
}
102+
bottom_refs.push_back(std::move(bottom_tx));
103+
}
104+
105+
// Run the benchmark exactly once. Running it multiple times would require the setup to be
106+
// redone, which takes a very non-negligible time compared to the trimming itself.
107+
bench.epochIterations(1).epochs(1).run([&] {
108+
// Call Trim() to remove transactions and bring the cluster back within limits.
109+
graph->Trim();
110+
// And relinearize everything that remains acceptably.
111+
graph->GetBlockBuilder();
112+
});
113+
114+
assert(!graph->IsOversized());
115+
// At least 99% of chains must survive.
116+
assert(graph->GetTransactionCount() >= (NUM_TOP_CHAINS * NUM_TX_PER_TOP_CHAIN * 99) / 100);
117+
}
118+
119+
} // namespace
120+
121+
static void TxGraphTrim(benchmark::Bench& bench) { BenchTxGraphTrim(bench); }
122+
123+
BENCHMARK(TxGraphTrim, benchmark::PriorityLevel::HIGH);

src/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ add_executable(test_bitcoin
106106
transaction_tests.cpp
107107
translation_tests.cpp
108108
txdownload_tests.cpp
109+
txgraph_tests.cpp
109110
txindex_tests.cpp
110111
txpackage_tests.cpp
111112
txreconciliation_tests.cpp

0 commit comments

Comments
 (0)