Skip to content

Commit 023418a

Browse files
committed
Merge bitcoin/bitcoin#28530: tests, bug fix: DisconnectedBlockTransactions rewrite followups
9b3da70 [test] DisconnectedBlockTransactions::DynamicMemoryUsage (glozow) b2d0447 bugfix: correct DisconnectedBlockTransactions memory usage (stickies-v) f4254e2 assume duplicate transactions are not added to `iters_by_txid` (ismaelsadeeq) 29eb219 move only: move implementation code to disconnected_transactions.cpp (ismaelsadeeq) 81dfedd refactor: update `MAX_DISCONNECTED_TX_POOL` from kb to bytes (ismaelsadeeq) Pull request description: This PR is a follow-up to fix review comments and a bugfix from #28385 The PR - Updated `DisconnectedBlockTransactions`'s `MAX_DISCONNECTED_TX_POOL` from kb to bytes. - Moved `DisconnectedBlockTransactions` implementation code to `kernel/disconnected_transactions.cpp`. - `AddTransactionsFromBlock` now assume duplicate transactions are not passed by asserting after inserting each transaction to `iters_by_txid`. - Included a Bug fix: In the current master we are underestimating the memory usage of `DisconnectedBlockTransactions`. * When adding and subtracting `cachedInnerUsage` we call `RecursiveDynamicUsage` with `CTransaction` which invokes this [`RecursiveDynamicUsage(const CTransaction& tx)`](https://github.com/bitcoin/bitcoin/blob/6e721c923c87abdb8d99674093d08d8c17bc52c2/src/core_memusage.h#L32) version of `RecursiveDynamicUsage`, the output of that call only account for the memory usage of the inputs and outputs of the `CTransaction`, this omits the memory usage of the `CTransaction` object and the control block. * This PR fixes this bug by calling `RecursiveDynamicUsage` with `CTransactionRef` when adding and subtracting `cachedInnerUsage` which invokes [`RecursiveDynamicUsage(const std::shared_ptr<X>& p)`](https://github.com/bitcoin/bitcoin/blob/6e721c923c87abdb8d99674093d08d8c17bc52c2/src/core_memusage.h#L67) version of `RecursiveDynamicUsage` the output of the calculation accounts for the` CTransaction` object, the control blocks, inputs and outputs memory usage. * see [comment ](bitcoin/bitcoin#28385 (comment)) - Added test for DisconnectedBlockTransactions memory limit. ACKs for top commit: stickies-v: ACK 9b3da70 - nice work! BrandonOdiwuor: re ACK 9b3da70 glozow: ACK 9b3da70 Tree-SHA512: 69b9595d09f4d0209038f97081d790cea92ccf63efb94e9e372749979fcbe527f7f17a8e454720cedd12021be0c8e11cf99874625d3dafd9ec602b12dbeb4098
2 parents 023e8c2 + 9b3da70 commit 023418a

8 files changed

+204
-77
lines changed

src/Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ libbitcoin_node_a_SOURCES = \
404404
kernel/coinstats.cpp \
405405
kernel/context.cpp \
406406
kernel/cs_main.cpp \
407+
kernel/disconnected_transactions.cpp \
407408
kernel/mempool_persist.cpp \
408409
kernel/mempool_removal_reason.cpp \
409410
mapport.cpp \
@@ -944,6 +945,7 @@ libbitcoinkernel_la_SOURCES = \
944945
kernel/coinstats.cpp \
945946
kernel/context.cpp \
946947
kernel/cs_main.cpp \
948+
kernel/disconnected_transactions.cpp \
947949
kernel/mempool_persist.cpp \
948950
kernel/mempool_removal_reason.cpp \
949951
key.cpp \

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ BITCOIN_TESTS =\
9292
test/dbwrapper_tests.cpp \
9393
test/denialofservice_tests.cpp \
9494
test/descriptor_tests.cpp \
95+
test/disconnected_transactions.cpp \
9596
test/flatfile_tests.cpp \
9697
test/fs_tests.cpp \
9798
test/getarg_tests.cpp \

src/bench/disconnected_transactions.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ static ReorgTxns CreateBlocks(size_t num_not_shared)
7373

7474
static void Reorg(const ReorgTxns& reorg)
7575
{
76-
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_SIZE * 1000};
76+
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_BYTES};
7777
// Disconnect block
7878
const auto evicted = disconnectpool.AddTransactionsFromBlock(reorg.disconnected_txns);
7979
assert(evicted.empty());
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 <kernel/disconnected_transactions.h>
6+
7+
#include <assert.h>
8+
#include <core_memusage.h>
9+
#include <memusage.h>
10+
#include <primitives/transaction.h>
11+
#include <util/hasher.h>
12+
13+
#include <memory>
14+
#include <utility>
15+
16+
// It's almost certainly a logic bug if we don't clear out queuedTx before
17+
// destruction, as we add to it while disconnecting blocks, and then we
18+
// need to re-process remaining transactions to ensure mempool consistency.
19+
// For now, assert() that we've emptied out this object on destruction.
20+
// This assert() can always be removed if the reorg-processing code were
21+
// to be refactored such that this assumption is no longer true (for
22+
// instance if there was some other way we cleaned up the mempool after a
23+
// reorg, besides draining this object).
24+
DisconnectedBlockTransactions::~DisconnectedBlockTransactions()
25+
{
26+
assert(queuedTx.empty());
27+
assert(iters_by_txid.empty());
28+
assert(cachedInnerUsage == 0);
29+
}
30+
31+
std::vector<CTransactionRef> DisconnectedBlockTransactions::LimitMemoryUsage()
32+
{
33+
std::vector<CTransactionRef> evicted;
34+
35+
while (!queuedTx.empty() && DynamicMemoryUsage() > m_max_mem_usage) {
36+
evicted.emplace_back(queuedTx.front());
37+
cachedInnerUsage -= RecursiveDynamicUsage(queuedTx.front());
38+
iters_by_txid.erase(queuedTx.front()->GetHash());
39+
queuedTx.pop_front();
40+
}
41+
return evicted;
42+
}
43+
44+
size_t DisconnectedBlockTransactions::DynamicMemoryUsage() const
45+
{
46+
return cachedInnerUsage + memusage::DynamicUsage(iters_by_txid) + memusage::DynamicUsage(queuedTx);
47+
}
48+
49+
[[nodiscard]] std::vector<CTransactionRef> DisconnectedBlockTransactions::AddTransactionsFromBlock(const std::vector<CTransactionRef>& vtx)
50+
{
51+
iters_by_txid.reserve(iters_by_txid.size() + vtx.size());
52+
for (auto block_it = vtx.rbegin(); block_it != vtx.rend(); ++block_it) {
53+
auto it = queuedTx.insert(queuedTx.end(), *block_it);
54+
auto [_, inserted] = iters_by_txid.emplace((*block_it)->GetHash(), it);
55+
assert(inserted); // callers may never pass multiple transactions with the same txid
56+
cachedInnerUsage += RecursiveDynamicUsage(*block_it);
57+
}
58+
return LimitMemoryUsage();
59+
}
60+
61+
void DisconnectedBlockTransactions::removeForBlock(const std::vector<CTransactionRef>& vtx)
62+
{
63+
// Short-circuit in the common case of a block being added to the tip
64+
if (queuedTx.empty()) {
65+
return;
66+
}
67+
for (const auto& tx : vtx) {
68+
auto iter = iters_by_txid.find(tx->GetHash());
69+
if (iter != iters_by_txid.end()) {
70+
auto list_iter = iter->second;
71+
iters_by_txid.erase(iter);
72+
cachedInnerUsage -= RecursiveDynamicUsage(*list_iter);
73+
queuedTx.erase(list_iter);
74+
}
75+
}
76+
}
77+
78+
void DisconnectedBlockTransactions::clear()
79+
{
80+
cachedInnerUsage = 0;
81+
iters_by_txid.clear();
82+
queuedTx.clear();
83+
}
84+
85+
std::list<CTransactionRef> DisconnectedBlockTransactions::take()
86+
{
87+
std::list<CTransactionRef> ret = std::move(queuedTx);
88+
clear();
89+
return ret;
90+
}

src/kernel/disconnected_transactions.h

Lines changed: 12 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@
55
#ifndef BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H
66
#define BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H
77

8-
#include <core_memusage.h>
9-
#include <memusage.h>
108
#include <primitives/transaction.h>
119
#include <util/hasher.h>
1210

1311
#include <list>
1412
#include <unordered_map>
1513
#include <vector>
1614

17-
/** Maximum kilobytes for transactions to store for processing during reorg */
18-
static const unsigned int MAX_DISCONNECTED_TX_POOL_SIZE = 20'000;
15+
/** Maximum bytes for transactions to store for processing during reorg */
16+
static const unsigned int MAX_DISCONNECTED_TX_POOL_BYTES{20'000'000};
1917
/**
2018
* DisconnectedBlockTransactions
2119
@@ -38,48 +36,23 @@ static const unsigned int MAX_DISCONNECTED_TX_POOL_SIZE = 20'000;
3836
*/
3937
class DisconnectedBlockTransactions {
4038
private:
41-
/** Cached dynamic memory usage for the CTransactions (memory for the shared pointers is
42-
* included in the container calculations). */
39+
/** Cached dynamic memory usage for the `CTransactionRef`s */
4340
uint64_t cachedInnerUsage = 0;
4441
const size_t m_max_mem_usage;
4542
std::list<CTransactionRef> queuedTx;
4643
using TxList = decltype(queuedTx);
4744
std::unordered_map<uint256, TxList::iterator, SaltedTxidHasher> iters_by_txid;
4845

4946
/** 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-
}
47+
std::vector<CTransactionRef> LimitMemoryUsage();
6248

6349
public:
64-
DisconnectedBlockTransactions(size_t max_mem_usage) : m_max_mem_usage{max_mem_usage} {}
50+
DisconnectedBlockTransactions(size_t max_mem_usage)
51+
: m_max_mem_usage{max_mem_usage} {}
6552

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-
}
53+
~DisconnectedBlockTransactions();
7954

80-
size_t DynamicMemoryUsage() const {
81-
return cachedInnerUsage + memusage::DynamicUsage(iters_by_txid) + memusage::DynamicUsage(queuedTx);
82-
}
55+
size_t DynamicMemoryUsage() const;
8356

8457
/** Add transactions from the block, iterating through vtx in reverse order. Callers should call
8558
* this function for blocks in descending order by block height.
@@ -88,50 +61,16 @@ class DisconnectedBlockTransactions {
8861
* corresponding entry in iters_by_txid.
8962
* @returns vector of transactions that were evicted for size-limiting.
9063
*/
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-
}
64+
[[nodiscard]] std::vector<CTransactionRef> AddTransactionsFromBlock(const std::vector<CTransactionRef>& vtx);
10165

10266
/** 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-
}
67+
void removeForBlock(const std::vector<CTransactionRef>& vtx);
11968

12069
size_t size() const { return queuedTx.size(); }
12170

122-
void clear()
123-
{
124-
cachedInnerUsage = 0;
125-
iters_by_txid.clear();
126-
queuedTx.clear();
127-
}
71+
void clear();
12872

12973
/** 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-
}
74+
std::list<CTransactionRef> take();
13675
};
13776
#endif // BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 <boost/test/unit_test.hpp>
6+
#include <core_memusage.h>
7+
#include <kernel/disconnected_transactions.h>
8+
#include <test/util/setup_common.h>
9+
10+
BOOST_FIXTURE_TEST_SUITE(disconnected_transactions, TestChain100Setup)
11+
12+
//! Tests that DisconnectedBlockTransactions limits its own memory properly
13+
BOOST_AUTO_TEST_CASE(disconnectpool_memory_limits)
14+
{
15+
// Use the coinbase transactions from TestChain100Setup. It doesn't matter whether these
16+
// transactions would realistically be in a block together, they just need distinct txids and
17+
// uniform size for this test to work.
18+
std::vector<CTransactionRef> block_vtx(m_coinbase_txns);
19+
BOOST_CHECK_EQUAL(block_vtx.size(), 100);
20+
21+
// Roughly estimate sizes to sanity check that DisconnectedBlockTransactions::DynamicMemoryUsage
22+
// is within an expected range.
23+
24+
// Overhead for the hashmap depends on number of buckets
25+
std::unordered_map<uint256, CTransaction*, SaltedTxidHasher> temp_map;
26+
temp_map.reserve(1);
27+
const size_t MAP_1{memusage::DynamicUsage(temp_map)};
28+
temp_map.reserve(100);
29+
const size_t MAP_100{memusage::DynamicUsage(temp_map)};
30+
31+
const size_t TX_USAGE{RecursiveDynamicUsage(block_vtx.front())};
32+
for (const auto& tx : block_vtx)
33+
BOOST_CHECK_EQUAL(RecursiveDynamicUsage(tx), TX_USAGE);
34+
35+
// Our overall formula is unordered map overhead + usage per entry.
36+
// Implementations may vary, but we're trying to guess the usage of data structures.
37+
const size_t ENTRY_USAGE_ESTIMATE{
38+
TX_USAGE
39+
// list entry: 2 pointers (next pointer and prev pointer) + element itself
40+
+ memusage::MallocUsage((2 * sizeof(void*)) + sizeof(decltype(block_vtx)::value_type))
41+
// unordered map: 1 pointer for the hashtable + key and value
42+
+ memusage::MallocUsage(sizeof(void*) + sizeof(decltype(temp_map)::key_type)
43+
+ sizeof(decltype(temp_map)::value_type))};
44+
45+
// DisconnectedBlockTransactions that's just big enough for 1 transaction.
46+
{
47+
DisconnectedBlockTransactions disconnectpool{MAP_1 + ENTRY_USAGE_ESTIMATE};
48+
// Add just 2 (and not all 100) transactions to keep the unordered map's hashtable overhead
49+
// to a minimum and avoid all (instead of all but 1) transactions getting evicted.
50+
std::vector<CTransactionRef> two_txns({block_vtx.at(0), block_vtx.at(1)});
51+
auto evicted_txns{disconnectpool.AddTransactionsFromBlock(two_txns)};
52+
BOOST_CHECK(disconnectpool.DynamicMemoryUsage() <= MAP_1 + ENTRY_USAGE_ESTIMATE);
53+
54+
// Only 1 transaction can be kept
55+
BOOST_CHECK_EQUAL(1, evicted_txns.size());
56+
// Transactions are added from back to front and eviction is FIFO.
57+
BOOST_CHECK_EQUAL(block_vtx.at(1), evicted_txns.front());
58+
59+
disconnectpool.clear();
60+
}
61+
62+
// DisconnectedBlockTransactions with a comfortable maximum memory usage so that nothing is evicted.
63+
// Record usage so we can check size limiting in the next test.
64+
size_t usage_full{0};
65+
{
66+
const size_t USAGE_100_OVERESTIMATE{MAP_100 + ENTRY_USAGE_ESTIMATE * 100};
67+
DisconnectedBlockTransactions disconnectpool{USAGE_100_OVERESTIMATE};
68+
auto evicted_txns{disconnectpool.AddTransactionsFromBlock(block_vtx)};
69+
BOOST_CHECK_EQUAL(evicted_txns.size(), 0);
70+
BOOST_CHECK(disconnectpool.DynamicMemoryUsage() <= USAGE_100_OVERESTIMATE);
71+
72+
usage_full = disconnectpool.DynamicMemoryUsage();
73+
74+
disconnectpool.clear();
75+
}
76+
77+
// DisconnectedBlockTransactions that's just a little too small for all of the transactions.
78+
{
79+
const size_t MAX_MEMUSAGE_99{usage_full - sizeof(void*)};
80+
DisconnectedBlockTransactions disconnectpool{MAX_MEMUSAGE_99};
81+
auto evicted_txns{disconnectpool.AddTransactionsFromBlock(block_vtx)};
82+
BOOST_CHECK(disconnectpool.DynamicMemoryUsage() <= MAX_MEMUSAGE_99);
83+
84+
// Only 1 transaction needed to be evicted
85+
BOOST_CHECK_EQUAL(1, evicted_txns.size());
86+
87+
// Transactions are added from back to front and eviction is FIFO.
88+
// The last transaction of block_vtx should be the first to be evicted.
89+
BOOST_CHECK_EQUAL(block_vtx.back(), evicted_txns.front());
90+
91+
disconnectpool.clear();
92+
}
93+
}
94+
95+
BOOST_AUTO_TEST_SUITE_END()

src/test/validation_chainstatemanager_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup)
579579
// it will initialize instead of attempting to complete validation.
580580
//
581581
// Note that this is not a realistic use of DisconnectTip().
582-
DisconnectedBlockTransactions unused_pool{MAX_DISCONNECTED_TX_POOL_SIZE * 1000};
582+
DisconnectedBlockTransactions unused_pool{MAX_DISCONNECTED_TX_POOL_BYTES};
583583
BlockValidationState unused_state;
584584
{
585585
LOCK2(::cs_main, bg_chainstate.MempoolMutex());

src/validation.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3062,7 +3062,7 @@ bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex*
30623062

30633063
// Disconnect active blocks which are no longer in the best chain.
30643064
bool fBlocksDisconnected = false;
3065-
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_SIZE * 1000};
3065+
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_BYTES};
30663066
while (m_chain.Tip() && m_chain.Tip() != pindexFork) {
30673067
if (!DisconnectTip(state, &disconnectpool)) {
30683068
// This is likely a fatal error, but keep the mempool consistent,
@@ -3420,7 +3420,7 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
34203420

34213421
// ActivateBestChain considers blocks already in m_chain
34223422
// unconditionally valid already, so force disconnect away from it.
3423-
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_SIZE * 1000};
3423+
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_BYTES};
34243424
bool ret = DisconnectTip(state, &disconnectpool);
34253425
// DisconnectTip will add transactions to disconnectpool.
34263426
// Adjust the mempool to be consistent with the new tip, adding

0 commit comments

Comments
 (0)