|
| 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