Skip to content

Commit 938e86f

Browse files
glozowsipa
andcommitted
txgraph: add unit test for TxGraph::Trim (tests)
Co-Authored-By: Pieter Wuille <[email protected]>
1 parent a04e205 commit 938e86f

File tree

2 files changed

+295
-0
lines changed

2 files changed

+295
-0
lines changed

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

src/test/txgraph_tests.cpp

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// Copyright (c) 2023-present The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or https://opensource.org/license/mit/.
4+
5+
#include <txgraph.h>
6+
7+
#include <random.h>
8+
9+
#include <boost/test/unit_test.hpp>
10+
11+
#include <memory>
12+
#include <vector>
13+
14+
BOOST_AUTO_TEST_SUITE(txgraph_tests)
15+
16+
BOOST_AUTO_TEST_CASE(txgraph_trim_zigzag)
17+
{
18+
// T T T T T T T T T T T T T T (50 T's)
19+
// \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ /
20+
// \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ /
21+
// B B B B B B B B B B B B B (49 B's)
22+
//
23+
/** The maximum cluster count used in this test. */
24+
static constexpr int MAX_CLUSTER_COUNT = 50;
25+
/** The number of "bottom" transactions, which are in the mempool already. */
26+
static constexpr int NUM_BOTTOM_TX = 49;
27+
/** The number of "top" transactions, which come from disconnected blocks. These are re-added
28+
* to the mempool and, while connecting them to the already-in-mempool transactions, we
29+
* discover the resulting cluster is oversized. */
30+
static constexpr int NUM_TOP_TX = 50;
31+
/** The total number of transactions in the test. */
32+
static constexpr int NUM_TOTAL_TX = NUM_BOTTOM_TX + NUM_TOP_TX;
33+
static_assert(NUM_TOTAL_TX > MAX_CLUSTER_COUNT);
34+
/** Set a very large cluster size limit so that only the count limit is triggered. */
35+
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
36+
37+
// Create a new graph for the test.
38+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
39+
40+
// Add all transactions and store their Refs.
41+
std::vector<TxGraph::Ref> refs;
42+
refs.reserve(NUM_TOTAL_TX);
43+
// First all bottom transactions: the i'th bottom transaction is at position i.
44+
for (unsigned int i = 0; i < NUM_BOTTOM_TX; ++i) {
45+
refs.push_back(graph->AddTransaction(FeePerWeight{200 - i, 100}));
46+
}
47+
// Then all top transactions: the i'th top transaction is at position NUM_BOTTOM_TX + i.
48+
for (unsigned int i = 0; i < NUM_TOP_TX; ++i) {
49+
refs.push_back(graph->AddTransaction(FeePerWeight{100 - i, 100}));
50+
}
51+
52+
// Create the zigzag dependency structure.
53+
// Each transaction in the bottom row depends on two adjacent transactions from the top row.
54+
graph->SanityCheck();
55+
for (unsigned int i = 0; i < NUM_BOTTOM_TX; ++i) {
56+
graph->AddDependency(/*parent=*/refs[NUM_BOTTOM_TX + i], /*child=*/refs[i]);
57+
graph->AddDependency(/*parent=*/refs[NUM_BOTTOM_TX + i + 1], /*child=*/refs[i]);
58+
}
59+
60+
// Check that the graph is now oversized. This also forces the graph to
61+
// group clusters and compute the oversized status.
62+
graph->SanityCheck();
63+
BOOST_CHECK_EQUAL(graph->GetTransactionCount(), NUM_TOTAL_TX);
64+
BOOST_CHECK(graph->IsOversized(/*main_only=*/false));
65+
66+
// Call Trim() to remove transactions and bring the cluster back within limits.
67+
auto removed_refs = graph->Trim();
68+
graph->SanityCheck();
69+
BOOST_CHECK(!graph->IsOversized(/*main_only=*/false));
70+
71+
BOOST_CHECK_EQUAL(removed_refs.size(), NUM_TOTAL_TX - MAX_CLUSTER_COUNT);
72+
BOOST_CHECK_EQUAL(graph->GetTransactionCount(), MAX_CLUSTER_COUNT);
73+
74+
// Only prefix of size max_cluster_count is left. That's the first half of the top and first half of the bottom.
75+
for (unsigned int i = 0; i < refs.size(); ++i) {
76+
const bool first_half = (i < (NUM_BOTTOM_TX / 2)) ||
77+
(i >= NUM_BOTTOM_TX && i < NUM_BOTTOM_TX + NUM_TOP_TX / 2 + 1);
78+
BOOST_CHECK_EQUAL(graph->Exists(refs[i]), first_half);
79+
}
80+
}
81+
82+
BOOST_AUTO_TEST_CASE(txgraph_trim_flower)
83+
{
84+
// We will build an oversized flower-shaped graph: all transactions are spent by 1 descendant.
85+
//
86+
// T T T T T T T T (100 T's)
87+
// | | | | | | | |
88+
// | | | | | | | |
89+
// \---+---+---+-+-+---+---+---/
90+
// |
91+
// B (1 B)
92+
//
93+
/** The maximum cluster count used in this test. */
94+
static constexpr int MAX_CLUSTER_COUNT = 50;
95+
/** The number of "top" transactions, which come from disconnected blocks. These are re-added
96+
* to the mempool and, connecting them to the already-in-mempool transactions, we discover the
97+
* resulting cluster is oversized. */
98+
static constexpr int NUM_TOP_TX = MAX_CLUSTER_COUNT * 2;
99+
/** The total number of transactions in this test. */
100+
static constexpr int NUM_TOTAL_TX = NUM_TOP_TX + 1;
101+
/** Set a very large cluster size limit so that only the count limit is triggered. */
102+
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
103+
104+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
105+
106+
// Add all transactions and store their Refs.
107+
std::vector<TxGraph::Ref> refs;
108+
refs.reserve(NUM_TOTAL_TX);
109+
110+
// Add all transactions. They are in individual clusters.
111+
refs.push_back(graph->AddTransaction({1, 100}));
112+
for (unsigned int i = 0; i < NUM_TOP_TX; ++i) {
113+
refs.push_back(graph->AddTransaction(FeePerWeight{500 + i, 100}));
114+
}
115+
graph->SanityCheck();
116+
117+
// The 0th transaction spends all the top transactions.
118+
for (unsigned int i = 1; i < NUM_TOTAL_TX; ++i) {
119+
graph->AddDependency(/*parent=*/refs[i], /*child=*/refs[0]);
120+
}
121+
graph->SanityCheck();
122+
123+
// Check that the graph is now oversized. This also forces the graph to
124+
// group clusters and compute the oversized status.
125+
BOOST_CHECK(graph->IsOversized(/*main_only=*/false));
126+
127+
// Call Trim() to remove transactions and bring the cluster back within limits.
128+
auto removed_refs = graph->Trim();
129+
graph->SanityCheck();
130+
BOOST_CHECK(!graph->IsOversized(/*main_only=*/false));
131+
132+
BOOST_CHECK_EQUAL(removed_refs.size(), NUM_TOTAL_TX - MAX_CLUSTER_COUNT);
133+
BOOST_CHECK_EQUAL(graph->GetTransactionCount(), MAX_CLUSTER_COUNT);
134+
135+
// Only prefix of size max_cluster_count (last max_cluster_count top transactions) is left.
136+
for (unsigned int i = 0; i < refs.size(); ++i) {
137+
const bool top_highest_feerate = i > (NUM_TOTAL_TX - MAX_CLUSTER_COUNT - 1);
138+
BOOST_CHECK_EQUAL(graph->Exists(refs[i]), top_highest_feerate);
139+
}
140+
}
141+
142+
BOOST_AUTO_TEST_CASE(txgraph_trim_huge)
143+
{
144+
// The from-block transactions consist of 1000 fully linear clusters, each with 64
145+
// transactions. The mempool contains 11 transactions that together merge all of these into
146+
// a single cluster.
147+
//
148+
// (1000 chains of 64 transactions, 64000 T's total)
149+
//
150+
// T T T T T T T T
151+
// | | | | | | | |
152+
// T T T T T T T T
153+
// | | | | | | | |
154+
// T T T T T T T T
155+
// | | | | | | | |
156+
// T T T T T T T T
157+
// (64 long) (64 long) (64 long) (64 long) (64 long) (64 long) (64 long) (64 long)
158+
// | | | | | | | |
159+
// | | / \ | / \ | | /
160+
// \----------+--------/ \--------+--------/ \--------+-----+----+--------/
161+
// | | |
162+
// B B B
163+
//
164+
// (11 B's, each attaching to up to 100 chains of 64 T's)
165+
//
166+
/** The maximum cluster count used in this test. */
167+
static constexpr int MAX_CLUSTER_COUNT = 64;
168+
/** The number of "top" (from-block) chains of transactions. */
169+
static constexpr int NUM_TOP_CHAINS = 1000;
170+
/** The number of transactions per top chain. */
171+
static constexpr int NUM_TX_PER_TOP_CHAIN = MAX_CLUSTER_COUNT;
172+
/** The (maximum) number of dependencies per bottom transaction. */
173+
static constexpr int NUM_DEPS_PER_BOTTOM_TX = 100;
174+
/** The number of bottom transactions that are expected to be created. */
175+
static constexpr int NUM_BOTTOM_TX = (NUM_TOP_CHAINS - 1 + (NUM_DEPS_PER_BOTTOM_TX - 2)) / (NUM_DEPS_PER_BOTTOM_TX - 1);
176+
/** The total number of transactions created in this test. */
177+
static constexpr int NUM_TOTAL_TX = NUM_TOP_CHAINS * NUM_TX_PER_TOP_CHAIN + NUM_BOTTOM_TX;
178+
/** Set a very large cluster size limit so that only the count limit is triggered. */
179+
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000 * 100;
180+
181+
/** Refs to all top transactions. */
182+
std::vector<TxGraph::Ref> top_refs;
183+
/** Refs to all bottom transactions. */
184+
std::vector<TxGraph::Ref> bottom_refs;
185+
/** Indexes into top_refs for some transaction of each component, in arbitrary order.
186+
* Initially these are the last transactions in each chains, but as bottom transactions are
187+
* added, entries will be removed when they get merged, and randomized. */
188+
std::vector<size_t> top_components;
189+
190+
FastRandomContext rng;
191+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
192+
193+
// Construct the top chains.
194+
for (int chain = 0; chain < NUM_TOP_CHAINS; ++chain) {
195+
for (int chaintx = 0; chaintx < NUM_TX_PER_TOP_CHAIN; ++chaintx) {
196+
// Use random fees, size 1.
197+
int64_t fee = rng.randbits<27>() + 100;
198+
FeePerWeight feerate{fee, 1};
199+
top_refs.push_back(graph->AddTransaction(feerate));
200+
// Add internal dependencies linked the chain transactions together.
201+
if (chaintx > 0) {
202+
graph->AddDependency(*(top_refs.rbegin()), *(top_refs.rbegin() + 1));
203+
}
204+
}
205+
// Remember the last transaction in each chain, to attach the bottom transactions to.
206+
top_components.push_back(top_refs.size() - 1);
207+
}
208+
graph->SanityCheck();
209+
210+
// Not oversized so far (just 1000 clusters of 64).
211+
BOOST_CHECK(!graph->IsOversized());
212+
213+
// Construct the bottom transactions, and dependencies to the top chains.
214+
while (top_components.size() > 1) {
215+
// Construct the transaction.
216+
int64_t fee = rng.randbits<27>() + 100;
217+
FeePerWeight feerate{fee, 1};
218+
auto bottom_tx = graph->AddTransaction(feerate);
219+
// Determine the number of dependencies this transaction will have.
220+
int deps = std::min<int>(NUM_DEPS_PER_BOTTOM_TX, top_components.size());
221+
for (int dep = 0; dep < deps; ++dep) {
222+
// Pick an transaction in top_components to attach to.
223+
auto idx = rng.randrange(top_components.size());
224+
// Add dependency.
225+
graph->AddDependency(/*parent=*/top_refs[top_components[idx]], /*child=*/bottom_tx);
226+
// Unless this is the last dependency being added, remove from top_components, as
227+
// the component will be merged with that one.
228+
if (dep < deps - 1) {
229+
// Move entry top the back.
230+
if (idx != top_components.size() - 1) std::swap(top_components.back(), top_components[idx]);
231+
// And pop it.
232+
top_components.pop_back();
233+
}
234+
}
235+
bottom_refs.push_back(std::move(bottom_tx));
236+
}
237+
graph->SanityCheck();
238+
239+
// Now we are oversized (one cluster of 64011).
240+
BOOST_CHECK(graph->IsOversized());
241+
const auto total_tx_count = graph->GetTransactionCount();
242+
BOOST_CHECK(total_tx_count == top_refs.size() + bottom_refs.size());
243+
BOOST_CHECK(total_tx_count == NUM_TOTAL_TX);
244+
245+
// Call Trim() to remove transactions and bring the cluster back within limits.
246+
auto removed_refs = graph->Trim();
247+
BOOST_CHECK(!graph->IsOversized());
248+
BOOST_CHECK(removed_refs.size() == total_tx_count - graph->GetTransactionCount());
249+
graph->SanityCheck();
250+
251+
// At least one original chain must survive.
252+
BOOST_CHECK(graph->GetTransactionCount() >= NUM_TX_PER_TOP_CHAIN);
253+
}
254+
255+
BOOST_AUTO_TEST_CASE(txgraph_trim_big_singletons)
256+
{
257+
// Mempool consists of 100 singleton clusters; there are no dependencies. Some are oversized. Trim() should remove all of the oversized ones.
258+
static constexpr int MAX_CLUSTER_COUNT = 64;
259+
static constexpr int32_t MAX_CLUSTER_SIZE = 100'000;
260+
static constexpr int NUM_TOTAL_TX = 100;
261+
262+
// Create a new graph for the test.
263+
auto graph = MakeTxGraph(MAX_CLUSTER_COUNT, MAX_CLUSTER_SIZE);
264+
265+
// Add all transactions and store their Refs.
266+
std::vector<TxGraph::Ref> refs;
267+
refs.reserve(NUM_TOTAL_TX);
268+
269+
// Add all transactions. They are in individual clusters.
270+
for (unsigned int i = 0; i < NUM_TOTAL_TX; ++i) {
271+
// The 88th transaction is oversized.
272+
// Every 20th transaction is oversized.
273+
const FeePerWeight feerate{500 + i, (i == 88 || i % 20 == 0) ? MAX_CLUSTER_SIZE + 1 : 100};
274+
refs.push_back(graph->AddTransaction(feerate));
275+
}
276+
graph->SanityCheck();
277+
278+
// Check that the graph is now oversized. This also forces the graph to
279+
// group clusters and compute the oversized status.
280+
BOOST_CHECK(graph->IsOversized(/*main_only=*/false));
281+
282+
// Call Trim() to remove transactions and bring the cluster back within limits.
283+
auto removed_refs = graph->Trim();
284+
graph->SanityCheck();
285+
BOOST_CHECK_EQUAL(graph->GetTransactionCount(), NUM_TOTAL_TX - 6);
286+
BOOST_CHECK(!graph->IsOversized(/*main_only=*/false));
287+
288+
// Check that all the oversized transactions were removed.
289+
for (unsigned int i = 0; i < refs.size(); ++i) {
290+
BOOST_CHECK_EQUAL(graph->Exists(refs[i]), i != 88 && i % 20 != 0);
291+
}
292+
}
293+
294+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)