Skip to content

Commit ac9fa6e

Browse files
committed
Merge bitcoin/bitcoin#28385: [refactor] rewrite DisconnectedBlockTransactions to not use boost
4313c77 make DisconnectedBlockTransactions responsible for its own memory management (glozow) cf5f1fa MOVEONLY: DisconnectedBlockTransactions to its own file (glozow) 2765d6f rewrite DisconnectedBlockTransactions as a list + map (glozow) 79ce9f0 add std::list to memusage (glozow) 59a35a7 [bench] DisconnectedBlockTransactions (glozow) 925bb72 [refactor] batch-add transactions to DisconnectedBlockTransactions (glozow) Pull request description: Motivation - I think it's preferable to use stdlib data structures instead of depending on boost if we can achieve the same thing. - Also see #28335 for further context/motivation. This PR simplifies that one. Things done in this PR: - Add a bench for `DisconnectedBlockTransactions` where we reorg and the new chain has {100%, 90%, 10%} of the same transactions. AFAIU in practice, it's usually close to 100%. - Rewrite `DisconnectedBlockTransactions` as a `std::list` + `unordered_map` instead of a boost multi index container. - On my machine, the bench suggests the performance is very similar. - Move `DisconnectedBlockTransactions` from txmempool.h to its own kernel/disconnected_transactions.h. This struct isn't used by txmempool and doesn't have much to do with txmempool. My guess is that it's been living there for convenience since the boost includes are there. ACKs for top commit: ismaelsadeeq: Tested ACK 4313c77 stickies-v: ACK 4313c77 TheCharlatan: ACK 4313c77 Tree-SHA512: 273c80866bf3acd39b2a039dc082b7719d2d82e0940e1eb6c402f1c0992e997256722b85c7e310c9811238a770cfbdeb122ea4babbc23835d17128f214a1ef9e
2 parents 719cb30 + 4313c77 commit ac9fa6e

9 files changed

+319
-129
lines changed

src/Makefile.am

+1
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ BITCOIN_CORE_H = \
186186
kernel/coinstats.h \
187187
kernel/context.h \
188188
kernel/cs_main.h \
189+
kernel/disconnected_transactions.h \
189190
kernel/mempool_entry.h \
190191
kernel/mempool_limits.h \
191192
kernel/mempool_options.h \

src/Makefile.bench.include

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ bench_bench_bitcoin_SOURCES = \
2828
bench/data.cpp \
2929
bench/data.h \
3030
bench/descriptors.cpp \
31+
bench/disconnected_transactions.cpp \
3132
bench/duplicate_inputs.cpp \
3233
bench/ellswift.cpp \
3334
bench/examples.cpp \
+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (c) 2023 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 <kernel/disconnected_transactions.h>
7+
#include <primitives/block.h>
8+
#include <test/util/random.h>
9+
#include <test/util/setup_common.h>
10+
11+
constexpr size_t BLOCK_VTX_COUNT{4000};
12+
constexpr size_t BLOCK_VTX_COUNT_10PERCENT{400};
13+
14+
using BlockTxns = decltype(CBlock::vtx);
15+
16+
/** Reorg where 1 block is disconnected and 2 blocks are connected. */
17+
struct ReorgTxns {
18+
/** Disconnected block. */
19+
BlockTxns disconnected_txns;
20+
/** First connected block. */
21+
BlockTxns connected_txns_1;
22+
/** Second connected block, new chain tip. Has no overlap with disconnected_txns. */
23+
BlockTxns connected_txns_2;
24+
/** Transactions shared between disconnected_txns and connected_txns_1. */
25+
size_t num_shared;
26+
};
27+
28+
static BlockTxns CreateRandomTransactions(size_t num_txns)
29+
{
30+
// Ensure every transaction has a different txid by having each one spend the previous one.
31+
static uint256 prevout_hash{uint256::ZERO};
32+
33+
BlockTxns txns;
34+
txns.reserve(num_txns);
35+
// Simplest spk for every tx
36+
CScript spk = CScript() << OP_TRUE;
37+
for (uint32_t i = 0; i < num_txns; ++i) {
38+
CMutableTransaction tx;
39+
tx.vin.emplace_back(CTxIn{COutPoint{prevout_hash, 0}});
40+
tx.vout.emplace_back(CTxOut{CENT, spk});
41+
auto ptx{MakeTransactionRef(tx)};
42+
txns.emplace_back(ptx);
43+
prevout_hash = ptx->GetHash();
44+
}
45+
return txns;
46+
}
47+
48+
/** Creates blocks for a Reorg, each with BLOCK_VTX_COUNT transactions. Between the disconnected
49+
* block and the first connected block, there will be num_not_shared transactions that are
50+
* different, and all other transactions the exact same. The second connected block has all unique
51+
* transactions. This is to simulate a reorg in which all but num_not_shared transactions are
52+
* confirmed in the new chain. */
53+
static ReorgTxns CreateBlocks(size_t num_not_shared)
54+
{
55+
auto num_shared{BLOCK_VTX_COUNT - num_not_shared};
56+
const auto shared_txns{CreateRandomTransactions(/*num_txns=*/num_shared)};
57+
58+
// Create different sets of transactions...
59+
auto disconnected_block_txns{CreateRandomTransactions(/*num_txns=*/num_not_shared)};
60+
std::copy(shared_txns.begin(), shared_txns.end(), std::back_inserter(disconnected_block_txns));
61+
62+
auto connected_block_txns{CreateRandomTransactions(/*num_txns=*/num_not_shared)};
63+
std::copy(shared_txns.begin(), shared_txns.end(), std::back_inserter(connected_block_txns));
64+
65+
assert(disconnected_block_txns.size() == BLOCK_VTX_COUNT);
66+
assert(connected_block_txns.size() == BLOCK_VTX_COUNT);
67+
68+
return ReorgTxns{/*disconnected_txns=*/disconnected_block_txns,
69+
/*connected_txns_1=*/connected_block_txns,
70+
/*connected_txns_2=*/CreateRandomTransactions(BLOCK_VTX_COUNT),
71+
/*num_shared=*/num_shared};
72+
}
73+
74+
static void Reorg(const ReorgTxns& reorg)
75+
{
76+
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_SIZE * 1000};
77+
// Disconnect block
78+
const auto evicted = disconnectpool.AddTransactionsFromBlock(reorg.disconnected_txns);
79+
assert(evicted.empty());
80+
81+
// Connect first block
82+
disconnectpool.removeForBlock(reorg.connected_txns_1);
83+
// Connect new tip
84+
disconnectpool.removeForBlock(reorg.connected_txns_2);
85+
86+
// Sanity Check
87+
assert(disconnectpool.size() == BLOCK_VTX_COUNT - reorg.num_shared);
88+
89+
disconnectpool.clear();
90+
}
91+
92+
/** Add transactions from DisconnectedBlockTransactions, remove all but one (the disconnected
93+
* block's coinbase transaction) of them, and then pop from the front until empty. This is a reorg
94+
* in which all of the non-coinbase transactions in the disconnected chain also exist in the new
95+
* chain. */
96+
static void AddAndRemoveDisconnectedBlockTransactionsAll(benchmark::Bench& bench)
97+
{
98+
const auto chains{CreateBlocks(/*num_not_shared=*/1)};
99+
assert(chains.num_shared == BLOCK_VTX_COUNT - 1);
100+
101+
bench.minEpochIterations(10).run([&]() NO_THREAD_SAFETY_ANALYSIS {
102+
Reorg(chains);
103+
});
104+
}
105+
106+
/** Add transactions from DisconnectedBlockTransactions, remove 90% of them, and then pop from the front until empty. */
107+
static void AddAndRemoveDisconnectedBlockTransactions90(benchmark::Bench& bench)
108+
{
109+
const auto chains{CreateBlocks(/*num_not_shared=*/BLOCK_VTX_COUNT_10PERCENT)};
110+
assert(chains.num_shared == BLOCK_VTX_COUNT - BLOCK_VTX_COUNT_10PERCENT);
111+
112+
bench.minEpochIterations(10).run([&]() NO_THREAD_SAFETY_ANALYSIS {
113+
Reorg(chains);
114+
});
115+
}
116+
117+
/** Add transactions from DisconnectedBlockTransactions, remove 10% of them, and then pop from the front until empty. */
118+
static void AddAndRemoveDisconnectedBlockTransactions10(benchmark::Bench& bench)
119+
{
120+
const auto chains{CreateBlocks(/*num_not_shared=*/BLOCK_VTX_COUNT - BLOCK_VTX_COUNT_10PERCENT)};
121+
assert(chains.num_shared == BLOCK_VTX_COUNT_10PERCENT);
122+
123+
bench.minEpochIterations(10).run([&]() NO_THREAD_SAFETY_ANALYSIS {
124+
Reorg(chains);
125+
});
126+
}
127+
128+
BENCHMARK(AddAndRemoveDisconnectedBlockTransactionsAll, benchmark::PriorityLevel::HIGH);
129+
BENCHMARK(AddAndRemoveDisconnectedBlockTransactions90, benchmark::PriorityLevel::HIGH);
130+
BENCHMARK(AddAndRemoveDisconnectedBlockTransactions10, benchmark::PriorityLevel::HIGH);
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) 2023 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+
#ifndef BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H
6+
#define BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H
7+
8+
#include <core_memusage.h>
9+
#include <memusage.h>
10+
#include <primitives/transaction.h>
11+
#include <util/hasher.h>
12+
13+
#include <list>
14+
#include <unordered_map>
15+
#include <vector>
16+
17+
/** Maximum kilobytes for transactions to store for processing during reorg */
18+
static const unsigned int MAX_DISCONNECTED_TX_POOL_SIZE = 20'000;
19+
/**
20+
* DisconnectedBlockTransactions
21+
22+
* During the reorg, it's desirable to re-add previously confirmed transactions
23+
* to the mempool, so that anything not re-confirmed in the new chain is
24+
* available to be mined. However, it's more efficient to wait until the reorg
25+
* is complete and process all still-unconfirmed transactions at that time,
26+
* since we expect most confirmed transactions to (typically) still be
27+
* confirmed in the new chain, and re-accepting to the memory pool is expensive
28+
* (and therefore better to not do in the middle of reorg-processing).
29+
* Instead, store the disconnected transactions (in order!) as we go, remove any
30+
* that are included in blocks in the new chain, and then process the remaining
31+
* still-unconfirmed transactions at the end.
32+
*
33+
* Order of queuedTx:
34+
* The front of the list should be the most recently-confirmed transactions (transactions at the
35+
* end of vtx of blocks closer to the tip). If memory usage grows too large, we trim from the front
36+
* of the list. After trimming, transactions can be re-added to the mempool from the back of the
37+
* list to the front without running into missing inputs.
38+
*/
39+
class DisconnectedBlockTransactions {
40+
private:
41+
/** Cached dynamic memory usage for the CTransactions (memory for the shared pointers is
42+
* included in the container calculations). */
43+
uint64_t cachedInnerUsage = 0;
44+
const size_t m_max_mem_usage;
45+
std::list<CTransactionRef> queuedTx;
46+
using TxList = decltype(queuedTx);
47+
std::unordered_map<uint256, TxList::iterator, SaltedTxidHasher> iters_by_txid;
48+
49+
/** Trim the earliest-added entries until we are within memory bounds. */
50+
std::vector<CTransactionRef> LimitMemoryUsage()
51+
{
52+
std::vector<CTransactionRef> evicted;
53+
54+
while (!queuedTx.empty() && DynamicMemoryUsage() > m_max_mem_usage) {
55+
evicted.emplace_back(queuedTx.front());
56+
cachedInnerUsage -= RecursiveDynamicUsage(*queuedTx.front());
57+
iters_by_txid.erase(queuedTx.front()->GetHash());
58+
queuedTx.pop_front();
59+
}
60+
return evicted;
61+
}
62+
63+
public:
64+
DisconnectedBlockTransactions(size_t max_mem_usage) : m_max_mem_usage{max_mem_usage} {}
65+
66+
// It's almost certainly a logic bug if we don't clear out queuedTx before
67+
// destruction, as we add to it while disconnecting blocks, and then we
68+
// need to re-process remaining transactions to ensure mempool consistency.
69+
// For now, assert() that we've emptied out this object on destruction.
70+
// This assert() can always be removed if the reorg-processing code were
71+
// to be refactored such that this assumption is no longer true (for
72+
// instance if there was some other way we cleaned up the mempool after a
73+
// reorg, besides draining this object).
74+
~DisconnectedBlockTransactions() {
75+
assert(queuedTx.empty());
76+
assert(iters_by_txid.empty());
77+
assert(cachedInnerUsage == 0);
78+
}
79+
80+
size_t DynamicMemoryUsage() const {
81+
return cachedInnerUsage + memusage::DynamicUsage(iters_by_txid) + memusage::DynamicUsage(queuedTx);
82+
}
83+
84+
/** Add transactions from the block, iterating through vtx in reverse order. Callers should call
85+
* this function for blocks in descending order by block height.
86+
* We assume that callers never pass multiple transactions with the same txid, otherwise things
87+
* can go very wrong in removeForBlock due to queuedTx containing an item without a
88+
* corresponding entry in iters_by_txid.
89+
* @returns vector of transactions that were evicted for size-limiting.
90+
*/
91+
[[nodiscard]] std::vector<CTransactionRef> AddTransactionsFromBlock(const std::vector<CTransactionRef>& vtx)
92+
{
93+
iters_by_txid.reserve(iters_by_txid.size() + vtx.size());
94+
for (auto block_it = vtx.rbegin(); block_it != vtx.rend(); ++block_it) {
95+
auto it = queuedTx.insert(queuedTx.end(), *block_it);
96+
iters_by_txid.emplace((*block_it)->GetHash(), it);
97+
cachedInnerUsage += RecursiveDynamicUsage(**block_it);
98+
}
99+
return LimitMemoryUsage();
100+
}
101+
102+
/** Remove any entries that are in this block. */
103+
void removeForBlock(const std::vector<CTransactionRef>& vtx)
104+
{
105+
// Short-circuit in the common case of a block being added to the tip
106+
if (queuedTx.empty()) {
107+
return;
108+
}
109+
for (const auto& tx : vtx) {
110+
auto iter = iters_by_txid.find(tx->GetHash());
111+
if (iter != iters_by_txid.end()) {
112+
auto list_iter = iter->second;
113+
iters_by_txid.erase(iter);
114+
cachedInnerUsage -= RecursiveDynamicUsage(**list_iter);
115+
queuedTx.erase(list_iter);
116+
}
117+
}
118+
}
119+
120+
size_t size() const { return queuedTx.size(); }
121+
122+
void clear()
123+
{
124+
cachedInnerUsage = 0;
125+
iters_by_txid.clear();
126+
queuedTx.clear();
127+
}
128+
129+
/** Clear all data structures and return the list of transactions. */
130+
std::list<CTransactionRef> take()
131+
{
132+
std::list<CTransactionRef> ret = std::move(queuedTx);
133+
clear();
134+
return ret;
135+
}
136+
};
137+
#endif // BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H

src/memusage.h

+16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include <cassert>
1313
#include <cstdlib>
14+
#include <list>
1415
#include <map>
1516
#include <memory>
1617
#include <set>
@@ -148,6 +149,21 @@ static inline size_t DynamicUsage(const std::shared_ptr<X>& p)
148149
return p ? MallocUsage(sizeof(X)) + MallocUsage(sizeof(stl_shared_counter)) : 0;
149150
}
150151

152+
template<typename X>
153+
struct list_node
154+
{
155+
private:
156+
void* ptr_next;
157+
void* ptr_prev;
158+
X x;
159+
};
160+
161+
template<typename X>
162+
static inline size_t DynamicUsage(const std::list<X>& l)
163+
{
164+
return MallocUsage(sizeof(list_node<X>)) * l.size();
165+
}
166+
151167
template<typename X>
152168
struct unordered_node : private X
153169
{

src/test/validation_chainstatemanager_tests.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55
#include <chainparams.h>
66
#include <consensus/validation.h>
7+
#include <kernel/disconnected_transactions.h>
78
#include <node/kernel_notifications.h>
89
#include <node/utxo_snapshot.h>
910
#include <random.h>
@@ -536,7 +537,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup)
536537
// it will initialize instead of attempting to complete validation.
537538
//
538539
// Note that this is not a realistic use of DisconnectTip().
539-
DisconnectedBlockTransactions unused_pool;
540+
DisconnectedBlockTransactions unused_pool{MAX_DISCONNECTED_TX_POOL_SIZE * 1000};
540541
BlockValidationState unused_state;
541542
{
542543
LOCK2(::cs_main, bg_chainstate.MempoolMutex());

0 commit comments

Comments
 (0)