Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ libbitcoin_node_a_SOURCES = \
kernel/coinstats.cpp \
kernel/context.cpp \
kernel/cs_main.cpp \
kernel/disconnected_transactions.cpp \
kernel/mempool_persist.cpp \
kernel/mempool_removal_reason.cpp \
mapport.cpp \
Expand Down Expand Up @@ -943,6 +944,7 @@ libbitcoinkernel_la_SOURCES = \
kernel/coinstats.cpp \
kernel/context.cpp \
kernel/cs_main.cpp \
kernel/disconnected_transactions.cpp \
kernel/mempool_persist.cpp \
kernel/mempool_removal_reason.cpp \
key.cpp \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ BITCOIN_TESTS =\
test/dbwrapper_tests.cpp \
test/denialofservice_tests.cpp \
test/descriptor_tests.cpp \
test/disconnected_transactions.cpp \
test/flatfile_tests.cpp \
test/fs_tests.cpp \
test/getarg_tests.cpp \
Expand Down
2 changes: 1 addition & 1 deletion src/bench/disconnected_transactions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ static ReorgTxns CreateBlocks(size_t num_not_shared)

static void Reorg(const ReorgTxns& reorg)
{
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_SIZE * 1000};
DisconnectedBlockTransactions disconnectpool{MAX_DISCONNECTED_TX_POOL_BYTES};
// Disconnect block
const auto evicted = disconnectpool.AddTransactionsFromBlock(reorg.disconnected_txns);
assert(evicted.empty());
Expand Down
90 changes: 90 additions & 0 deletions src/kernel/disconnected_transactions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) 2023 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <kernel/disconnected_transactions.h>

#include <assert.h>
#include <core_memusage.h>
#include <memusage.h>
#include <primitives/transaction.h>
#include <util/hasher.h>

#include <memory>
#include <utility>

// It's almost certainly a logic bug if we don't clear out queuedTx before
// destruction, as we add to it while disconnecting blocks, and then we
// need to re-process remaining transactions to ensure mempool consistency.
// For now, assert() that we've emptied out this object on destruction.
// This assert() can always be removed if the reorg-processing code were
// to be refactored such that this assumption is no longer true (for
// instance if there was some other way we cleaned up the mempool after a
// reorg, besides draining this object).
DisconnectedBlockTransactions::~DisconnectedBlockTransactions()
{
assert(queuedTx.empty());
assert(iters_by_txid.empty());
assert(cachedInnerUsage == 0);
}

std::vector<CTransactionRef> DisconnectedBlockTransactions::LimitMemoryUsage()
{
std::vector<CTransactionRef> evicted;

while (!queuedTx.empty() && DynamicMemoryUsage() > m_max_mem_usage) {
evicted.emplace_back(queuedTx.front());
cachedInnerUsage -= RecursiveDynamicUsage(queuedTx.front());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reference for b2d0447

Imagining DynamicMemoryUsage for a DisconnectedBlockTransactions with 1 transaction (ignoring the iters_by_txid part since that doesn't change),

before

DynamicUsage(queuedTx) + cachedInnerUsage
=MallocUsage(sizeof(list_node<CTransactionRef>) + cachedInnerUsage
=MallocUsage(sizeof(4 pointers) + RecursiveDynamicUsage(CTransaction)
=MallocUsage(sizeof(4 pointers) + DynamicUsage(vin) + DynamicUsage(vout) + sum([RecursiveDynamicUsage(input) for input in vin]) + sum([RecursiveDynamicUsage(output) for output in vout])

after

DynamicUsage(queuedTx) + cachedInnerUsage
=MallocUsage(sizeof(list_node<CTransactionRef>) + cachedInnerUsage
=MallocUsage(sizeof(4 pointers) + RecursiveDynamicUsage(CTransactionRef)
=MallocUsage(sizeof(4 pointers) + DynamicUsage(CTransactionRef) + RecursiveDynamicUsage(CTransaction)
=MallocUsage(sizeof(4 pointers) + MallocUsage(sizeof(CTransaction)) + MallocUsage(sizeof(stl_shared_counter)) + RecursiveDynamicUsage(CTransaction)
=MallocUsage(sizeof(4 pointers) + MallocUsage(sizeof(CTransaction)) + MallocUsage(sizeof(stl_shared_counter)) + DynamicUsage(vin) + DynamicUsage(vout) + sum([RecursiveDynamicUsage(input) for input in vin]) + sum([RecursiveDynamicUsage(output) for output in vout])

So we account for

  • each list node, which is 2 pointers + a shared pointer object (2 pointers)
  • each shared pointer's dynamically allocated memory i.e. the CTransaction object + its always dynamically allocated stuff (the vectors) + the control block

iters_by_txid.erase(queuedTx.front()->GetHash());
queuedTx.pop_front();
}
return evicted;
}

size_t DisconnectedBlockTransactions::DynamicMemoryUsage() const
{
return cachedInnerUsage + memusage::DynamicUsage(iters_by_txid) + memusage::DynamicUsage(queuedTx);
}

[[nodiscard]] std::vector<CTransactionRef> DisconnectedBlockTransactions::AddTransactionsFromBlock(const std::vector<CTransactionRef>& vtx)
{
iters_by_txid.reserve(iters_by_txid.size() + vtx.size());
for (auto block_it = vtx.rbegin(); block_it != vtx.rend(); ++block_it) {
auto it = queuedTx.insert(queuedTx.end(), *block_it);
auto [_, inserted] = iters_by_txid.emplace((*block_it)->GetHash(), it);
assert(inserted); // callers may never pass multiple transactions with the same txid
cachedInnerUsage += RecursiveDynamicUsage(*block_it);
}
return LimitMemoryUsage();
}

void DisconnectedBlockTransactions::removeForBlock(const std::vector<CTransactionRef>& vtx)
{
// Short-circuit in the common case of a block being added to the tip
if (queuedTx.empty()) {
return;
}
for (const auto& tx : vtx) {
auto iter = iters_by_txid.find(tx->GetHash());
if (iter != iters_by_txid.end()) {
auto list_iter = iter->second;
iters_by_txid.erase(iter);
cachedInnerUsage -= RecursiveDynamicUsage(*list_iter);
queuedTx.erase(list_iter);
}
}
}

void DisconnectedBlockTransactions::clear()
{
cachedInnerUsage = 0;
iters_by_txid.clear();
queuedTx.clear();
}

std::list<CTransactionRef> DisconnectedBlockTransactions::take()
{
std::list<CTransactionRef> ret = std::move(queuedTx);
clear();
return ret;
}
85 changes: 12 additions & 73 deletions src/kernel/disconnected_transactions.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@
#ifndef BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H
#define BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H

#include <core_memusage.h>
#include <memusage.h>
#include <primitives/transaction.h>
#include <util/hasher.h>

#include <list>
#include <unordered_map>
#include <vector>

/** Maximum kilobytes for transactions to store for processing during reorg */
static const unsigned int MAX_DISCONNECTED_TX_POOL_SIZE = 20'000;
/** Maximum bytes for transactions to store for processing during reorg */
static const unsigned int MAX_DISCONNECTED_TX_POOL_BYTES{20'000'000};
/**
* DisconnectedBlockTransactions

Expand All @@ -38,48 +36,23 @@ static const unsigned int MAX_DISCONNECTED_TX_POOL_SIZE = 20'000;
*/
class DisconnectedBlockTransactions {
private:
/** Cached dynamic memory usage for the CTransactions (memory for the shared pointers is
* included in the container calculations). */
/** Cached dynamic memory usage for the `CTransactionRef`s */
uint64_t cachedInnerUsage = 0;
const size_t m_max_mem_usage;
std::list<CTransactionRef> queuedTx;
using TxList = decltype(queuedTx);
std::unordered_map<uint256, TxList::iterator, SaltedTxidHasher> iters_by_txid;

/** Trim the earliest-added entries until we are within memory bounds. */
std::vector<CTransactionRef> LimitMemoryUsage()
{
std::vector<CTransactionRef> evicted;

while (!queuedTx.empty() && DynamicMemoryUsage() > m_max_mem_usage) {
evicted.emplace_back(queuedTx.front());
cachedInnerUsage -= RecursiveDynamicUsage(*queuedTx.front());
iters_by_txid.erase(queuedTx.front()->GetHash());
queuedTx.pop_front();
}
return evicted;
}
std::vector<CTransactionRef> LimitMemoryUsage();

public:
DisconnectedBlockTransactions(size_t max_mem_usage) : m_max_mem_usage{max_mem_usage} {}
DisconnectedBlockTransactions(size_t max_mem_usage)
: m_max_mem_usage{max_mem_usage} {}

// It's almost certainly a logic bug if we don't clear out queuedTx before
// destruction, as we add to it while disconnecting blocks, and then we
// need to re-process remaining transactions to ensure mempool consistency.
// For now, assert() that we've emptied out this object on destruction.
// This assert() can always be removed if the reorg-processing code were
// to be refactored such that this assumption is no longer true (for
// instance if there was some other way we cleaned up the mempool after a
// reorg, besides draining this object).
~DisconnectedBlockTransactions() {
assert(queuedTx.empty());
assert(iters_by_txid.empty());
assert(cachedInnerUsage == 0);
}
~DisconnectedBlockTransactions();

size_t DynamicMemoryUsage() const {
return cachedInnerUsage + memusage::DynamicUsage(iters_by_txid) + memusage::DynamicUsage(queuedTx);
}
size_t DynamicMemoryUsage() const;

/** Add transactions from the block, iterating through vtx in reverse order. Callers should call
* this function for blocks in descending order by block height.
Expand All @@ -88,50 +61,16 @@ class DisconnectedBlockTransactions {
* corresponding entry in iters_by_txid.
* @returns vector of transactions that were evicted for size-limiting.
*/
[[nodiscard]] std::vector<CTransactionRef> AddTransactionsFromBlock(const std::vector<CTransactionRef>& vtx)
{
iters_by_txid.reserve(iters_by_txid.size() + vtx.size());
for (auto block_it = vtx.rbegin(); block_it != vtx.rend(); ++block_it) {
auto it = queuedTx.insert(queuedTx.end(), *block_it);
iters_by_txid.emplace((*block_it)->GetHash(), it);
cachedInnerUsage += RecursiveDynamicUsage(**block_it);
}
return LimitMemoryUsage();
}
[[nodiscard]] std::vector<CTransactionRef> AddTransactionsFromBlock(const std::vector<CTransactionRef>& vtx);

/** Remove any entries that are in this block. */
void removeForBlock(const std::vector<CTransactionRef>& vtx)
{
// Short-circuit in the common case of a block being added to the tip
if (queuedTx.empty()) {
return;
}
for (const auto& tx : vtx) {
auto iter = iters_by_txid.find(tx->GetHash());
if (iter != iters_by_txid.end()) {
auto list_iter = iter->second;
iters_by_txid.erase(iter);
cachedInnerUsage -= RecursiveDynamicUsage(**list_iter);
queuedTx.erase(list_iter);
}
}
}
void removeForBlock(const std::vector<CTransactionRef>& vtx);

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

void clear()
{
cachedInnerUsage = 0;
iters_by_txid.clear();
queuedTx.clear();
}
void clear();

/** Clear all data structures and return the list of transactions. */
std::list<CTransactionRef> take()
{
std::list<CTransactionRef> ret = std::move(queuedTx);
clear();
return ret;
}
std::list<CTransactionRef> take();
};
#endif // BITCOIN_KERNEL_DISCONNECTED_TRANSACTIONS_H
95 changes: 95 additions & 0 deletions src/test/disconnected_transactions.cpp
Copy link
Contributor

@stickies-v stickies-v Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for other reviewers: these are the memory allocation numbers I'm getting on my machine:

(lldb) p MAP_1
(const size_t) 32
(lldb) p MAP_100
(const size_t) 832
(lldb) p TX_USAGE
(const size_t) 640
(lldb) p ENTRY_USAGE_OVERESTIMATE
(const size_t) 896

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) 2023 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
#include <boost/test/unit_test.hpp>
#include <core_memusage.h>
#include <kernel/disconnected_transactions.h>
#include <test/util/setup_common.h>

BOOST_FIXTURE_TEST_SUITE(disconnected_transactions, TestChain100Setup)

//! Tests that DisconnectedBlockTransactions limits its own memory properly
BOOST_AUTO_TEST_CASE(disconnectpool_memory_limits)
{
// Use the coinbase transactions from TestChain100Setup. It doesn't matter whether these
// transactions would realistically be in a block together, they just need distinct txids and
// uniform size for this test to work.
std::vector<CTransactionRef> block_vtx(m_coinbase_txns);
BOOST_CHECK_EQUAL(block_vtx.size(), 100);

// Roughly estimate sizes to sanity check that DisconnectedBlockTransactions::DynamicMemoryUsage
// is within an expected range.

// Overhead for the hashmap depends on number of buckets
std::unordered_map<uint256, CTransaction*, SaltedTxidHasher> temp_map;
temp_map.reserve(1);
const size_t MAP_1{memusage::DynamicUsage(temp_map)};
temp_map.reserve(100);
const size_t MAP_100{memusage::DynamicUsage(temp_map)};

const size_t TX_USAGE{RecursiveDynamicUsage(block_vtx.front())};
for (const auto& tx : block_vtx)
BOOST_CHECK_EQUAL(RecursiveDynamicUsage(tx), TX_USAGE);

// Our overall formula is unordered map overhead + usage per entry.
// Implementations may vary, but we're trying to guess the usage of data structures.
const size_t ENTRY_USAGE_ESTIMATE{
TX_USAGE
// list entry: 2 pointers (next pointer and prev pointer) + element itself
+ memusage::MallocUsage((2 * sizeof(void*)) + sizeof(decltype(block_vtx)::value_type))
// unordered map: 1 pointer for the hashtable + key and value
+ memusage::MallocUsage(sizeof(void*) + sizeof(decltype(temp_map)::key_type)
+ sizeof(decltype(temp_map)::value_type))};

// DisconnectedBlockTransactions that's just big enough for 1 transaction.
{
DisconnectedBlockTransactions disconnectpool{MAP_1 + ENTRY_USAGE_ESTIMATE};
// Add just 2 (and not all 100) transactions to keep the unordered map's hashtable overhead
// to a minimum and avoid all (instead of all but 1) transactions getting evicted.
std::vector<CTransactionRef> two_txns({block_vtx.at(0), block_vtx.at(1)});
auto evicted_txns{disconnectpool.AddTransactionsFromBlock(two_txns)};
BOOST_CHECK(disconnectpool.DynamicMemoryUsage() <= MAP_1 + ENTRY_USAGE_ESTIMATE);

// Only 1 transaction can be kept
BOOST_CHECK_EQUAL(1, evicted_txns.size());
// Transactions are added from back to front and eviction is FIFO.
BOOST_CHECK_EQUAL(block_vtx.at(1), evicted_txns.front());

disconnectpool.clear();
}

// DisconnectedBlockTransactions with a comfortable maximum memory usage so that nothing is evicted.
// Record usage so we can check size limiting in the next test.
size_t usage_full{0};
{
const size_t USAGE_100_OVERESTIMATE{MAP_100 + ENTRY_USAGE_ESTIMATE * 100};
DisconnectedBlockTransactions disconnectpool{USAGE_100_OVERESTIMATE};
auto evicted_txns{disconnectpool.AddTransactionsFromBlock(block_vtx)};
BOOST_CHECK_EQUAL(evicted_txns.size(), 0);
BOOST_CHECK(disconnectpool.DynamicMemoryUsage() <= USAGE_100_OVERESTIMATE);

usage_full = disconnectpool.DynamicMemoryUsage();

disconnectpool.clear();
}

// DisconnectedBlockTransactions that's just a little too small for all of the transactions.
{
const size_t MAX_MEMUSAGE_99{usage_full - sizeof(void*)};
DisconnectedBlockTransactions disconnectpool{MAX_MEMUSAGE_99};
auto evicted_txns{disconnectpool.AddTransactionsFromBlock(block_vtx)};
BOOST_CHECK(disconnectpool.DynamicMemoryUsage() <= MAX_MEMUSAGE_99);

// Only 1 transaction needed to be evicted
BOOST_CHECK_EQUAL(1, evicted_txns.size());

// Transactions are added from back to front and eviction is FIFO.
// The last transaction of block_vtx should be the first to be evicted.
BOOST_CHECK_EQUAL(block_vtx.back(), evicted_txns.front());

disconnectpool.clear();
}
}

BOOST_AUTO_TEST_SUITE_END()
2 changes: 1 addition & 1 deletion src/test/validation_chainstatemanager_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup)
// it will initialize instead of attempting to complete validation.
//
// Note that this is not a realistic use of DisconnectTip().
DisconnectedBlockTransactions unused_pool{MAX_DISCONNECTED_TX_POOL_SIZE * 1000};
DisconnectedBlockTransactions unused_pool{MAX_DISCONNECTED_TX_POOL_BYTES};
BlockValidationState unused_state;
{
LOCK2(::cs_main, bg_chainstate.MempoolMutex());
Expand Down
4 changes: 2 additions & 2 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3058,7 +3058,7 @@ bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex*

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

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