Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2bd8cb3
validation: collect block inputs in CoinsViewOverlay before ConnectBlock
andrewtoth Mar 7, 2026
01291b8
coins: filter same-block spends in StartFetching
andrewtoth May 1, 2026
34b22b4
consensus: add MIN_TXIN_SERIALIZED_SIZE and MAX_INPUTS_PER_BLOCK
andrewtoth Apr 30, 2026
9a06b8e
coins: add ready flag to InputToFetch
andrewtoth May 1, 2026
0c7c62c
coins: stop fetching before mutating base
andrewtoth Mar 7, 2026
169944a
validation: add -inputfetchthreads configuration option
andrewtoth Apr 14, 2026
f5f6679
coins: introduce thread pool in CoinsViewOverlay
andrewtoth Mar 8, 2026
a162b3a
coins: fetch inputs in parallel
andrewtoth May 2, 2026
4fba08c
doc: update CoinsViewOverlay docstring to describe parallel fetching
andrewtoth Mar 7, 2026
dfed718
test: add unit tests for CoinsViewOverlay::StartFetching
andrewtoth Mar 7, 2026
b37e032
fuzz: update harnesses to cover CoinsViewOverlay::StartFetching
andrewtoth Mar 7, 2026
6f8ceb2
fuzz: add coins_view_stacked fuzz harness to test concurrent leveldb …
andrewtoth Mar 13, 2026
401cec5
coins: allocate cached coins out of line
LarryRuane May 10, 2026
0a0561d
test: adapt coins cache tests to pointer entries
LarryRuane May 10, 2026
9a1a383
fuzz: adapt coins cache entry construction
LarryRuane May 10, 2026
5638322
coins: tighten pointer cache entry ownership
l0rinc May 10, 2026
fbcbc64
coins: clean pointer cache patch artifacts
l0rinc May 10, 2026
6a5095e
test: cover spend moveout cache usage
l0rinc May 10, 2026
06cee21
coins: canonicalize spent pointer entries
l0rinc May 10, 2026
55288fb
test: free temp map coins on BatchWrite exception
l0rinc May 10, 2026
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
168 changes: 107 additions & 61 deletions src/coins.cpp

Large diffs are not rendered by default.

306 changes: 281 additions & 25 deletions src/coins.h

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/consensus/consensus.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ static const int COINBASE_MATURITY = 100;

static const int WITNESS_SCALE_FACTOR = 4;

/** The minimum serialized size of a CTxIn even with an empty scriptSig
* (32 byte txid + 4 byte vout + 1 byte scriptSig length + 4 byte sequence) */
static constexpr uint32_t MIN_TXIN_SERIALIZED_SIZE{41};
/** The maximum number of possible inputs included in a block */
static constexpr uint32_t MAX_INPUTS_PER_BLOCK{(MAX_BLOCK_WEIGHT / WITNESS_SCALE_FACTOR) / MIN_TXIN_SERIALIZED_SIZE};

static const size_t MIN_TRANSACTION_WEIGHT = WITNESS_SCALE_FACTOR * 60; // 60 is the lower bound for the size of a valid serialized CTransaction
static const size_t MIN_SERIALIZABLE_TRANSACTION_WEIGHT = WITNESS_SCALE_FACTOR * 10; // 10 is the lower bound for the size of a serialized CTransaction

Expand Down
1 change: 1 addition & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
argsman.AddArg("-minimumchainwork=<hex>", strprintf("Minimum work assumed to exist on a valid chain in hex (default: %s, testnet3: %s, testnet4: %s, signet: %s)", defaultChainParams->GetConsensus().nMinimumChainWork.GetHex(), testnetChainParams->GetConsensus().nMinimumChainWork.GetHex(), testnet4ChainParams->GetConsensus().nMinimumChainWork.GetHex(), signetChainParams->GetConsensus().nMinimumChainWork.GetHex()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS);
argsman.AddArg("-par=<n>", strprintf("Set the number of script verification threads (0 = auto, up to %d, <0 = leave that many cores free, default: %d)",
MAX_SCRIPTCHECK_THREADS, DEFAULT_SCRIPTCHECK_THREADS), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-inputfetchthreads=<n>", strprintf("Set the number of input fetch threads (0 disables, up to %d, default: %d). Negative values are rejected.", MAX_INPUTFETCH_THREADS, DEFAULT_INPUTFETCH_THREADS), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-persistmempool", strprintf("Whether to save the mempool on shutdown and load on restart (default: %u)", DEFAULT_PERSIST_MEMPOOL), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-persistmempoolv1",
strprintf("Whether a mempool.dat file created by -persistmempool or the savemempool RPC will be written in the legacy format "
Expand Down
2 changes: 2 additions & 0 deletions src/kernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ add_library(bitcoinkernel
../uint256.cpp
../util/chaintype.cpp
../util/check.cpp
../util/exception.cpp
../util/expected.cpp
../util/feefrac.cpp
../util/fs.cpp
Expand All @@ -70,6 +71,7 @@ add_library(bitcoinkernel
../util/rbf.cpp
../util/signalinterrupt.cpp
../util/syserror.cpp
../util/thread.cpp
../util/threadnames.cpp
../util/time.cpp
../util/tokenpipe.cpp
Expand Down
3 changes: 3 additions & 0 deletions src/kernel/chainstatemanager_opts.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class CChainParams;
class ValidationSignals;

static constexpr auto DEFAULT_MAX_TIP_AGE{24h};
static constexpr int32_t DEFAULT_INPUTFETCH_THREADS{4};

namespace kernel {

Expand All @@ -46,6 +47,8 @@ struct ChainstateManagerOpts {
ValidationSignals* signals{nullptr};
//! Number of script check worker threads. Zero means no parallel verification.
int worker_threads_num{0};
//! Number of input fetch worker threads. Zero means no parallel fetching.
int32_t inputfetch_threads_num{DEFAULT_INPUTFETCH_THREADS};
size_t script_execution_cache_bytes{DEFAULT_SCRIPT_EXECUTION_CACHE_BYTES};
size_t signature_cache_bytes{DEFAULT_SIGNATURE_CACHE_BYTES};
};
Expand Down
7 changes: 7 additions & 0 deletions src/node/chainstatemanager_args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ util::Result<void> ApplyArgsManOptions(const ArgsManager& args, ChainstateManage
// Subtract 1 because the main thread counts towards the par threads.
opts.worker_threads_num = script_threads - 1;

if (auto value{args.GetIntArg("-inputfetchthreads")}) {
if (*value < 0) {
return util::Error{Untranslated(strprintf("-inputfetchthreads must be non-negative (got %d). Use 0 to disable input fetching.", *value))};
}
opts.inputfetch_threads_num = static_cast<int32_t>(std::min<int64_t>(*value, MAX_INPUTFETCH_THREADS));
}

if (auto max_size = args.GetIntArg("-maxsigcachesize")) {
// 1. When supplied with a max_size of 0, both the signature cache and
// script execution cache create the minimum possible cache (2
Expand Down
79 changes: 68 additions & 11 deletions src/test/coins_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ class CCoinsViewTest : public CoinsViewEmpty
for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)){
if (it->second.IsDirty()) {
// Same optimization used in CCoinsViewDB is to only write dirty entries.
map_[it->first] = it->second.coin;
if (it->second.coin.IsSpent() && m_rng.randrange(3) == 0) {
map_[it->first] = it->second.coin ? *it->second.coin : Coin{};
if (it->second.IsSpent() && m_rng.randrange(3) == 0) {
// Randomly delete empty entries on write.
map_.erase(it->first);
}
Expand All @@ -84,7 +84,7 @@ class CCoinsViewCacheTest : public CCoinsViewCache
size_t ret = memusage::DynamicUsage(cacheCoins);
size_t count = 0;
for (const auto& entry : cacheCoins) {
ret += entry.second.coin.DynamicMemoryUsage();
if (entry.second.coin) ret += entry.second.coin->DynamicMemoryUsage();
++count;
}
BOOST_CHECK_EQUAL(GetCacheSize(), count);
Expand All @@ -96,6 +96,7 @@ class CCoinsViewCacheTest : public CCoinsViewCache

CCoinsMap& map() const { return cacheCoins; }
CoinsCachePair& sentinel() const { return m_sentinel; }
CCoinsMapMemoryResource& resource() { return m_cache_coins_memory_resource; }
size_t& usage() const { return cachedCoinsUsage; }
size_t& dirty() const { return m_dirty_count; }
};
Expand Down Expand Up @@ -631,22 +632,46 @@ static void SetCoinsValue(const CAmount value, Coin& coin)
}
}

static size_t InsertCoinsMapEntry(CCoinsMap& map, CoinsCachePair& sentinel, const CoinEntry& cache_coin)
static void FreeCoin(CCoinsMapMemoryResource& resource, CCoinsCacheEntry& entry)
{
if (!entry.coin) return;
entry.coin->~Coin();
resource.Deallocate(entry.coin, sizeof(Coin), alignof(Coin));
entry.coin = nullptr;
}

// Add an (empty) coin to a cache entry. Unlike in the production code, a cache
// entry can point to a spent (empty) coin.
static void AddCoin(CCoinsMapMemoryResource& resource, CCoinsCacheEntry& entry)
{
if (entry.coin) FreeCoin(resource, entry);
assert(!entry.coin);
entry.coin = static_cast<Coin*>(resource.Allocate(sizeof(Coin), alignof(Coin)));
new (entry.coin) Coin();
}

static void FreeAllCoins(CCoinsMap& map, CCoinsMapMemoryResource& resource)
{
for (auto& entry : map) if (entry.second.coin) FreeCoin(resource, entry.second);
}

static size_t InsertCoinsMapEntry(CCoinsMap& map, CoinsCachePair& sentinel, CCoinsMapMemoryResource& resource, const CoinEntry& cache_coin)
{
CCoinsCacheEntry entry;
SetCoinsValue(cache_coin.value, entry.coin);
AddCoin(resource, entry);
SetCoinsValue(cache_coin.value, *entry.coin);
auto [iter, inserted] = map.emplace(OUTPOINT, std::move(entry));
assert(inserted);
if (cache_coin.IsDirty()) CCoinsCacheEntry::SetDirty(*iter, sentinel);
if (cache_coin.IsFresh()) CCoinsCacheEntry::SetFresh(*iter, sentinel);
return iter->second.coin.DynamicMemoryUsage();
return iter->second.coin->DynamicMemoryUsage();
}

static MaybeCoin GetCoinsMapEntry(const CCoinsMap& map, const COutPoint& outp = OUTPOINT)
{
if (auto it{map.find(outp)}; it != map.end()) {
return CoinEntry{
it->second.coin.IsSpent() ? SPENT : it->second.coin.out.nValue,
(!it->second.coin || it->second.coin->IsSpent()) ? SPENT : it->second.coin->out.nValue,
CoinEntry::ToState(it->second.IsDirty(), it->second.IsFresh())};
}
return MISSING;
Expand All @@ -658,11 +683,17 @@ static void WriteCoinsViewEntry(CCoinsView& view, const MaybeCoin& cache_coin)
sentinel.second.SelfRef(sentinel);
CCoinsMapMemoryResource resource;
CCoinsMap map{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &resource};
if (cache_coin) InsertCoinsMapEntry(map, sentinel, *cache_coin);
if (cache_coin) InsertCoinsMapEntry(map, sentinel, resource, *cache_coin);
size_t dirty_count{cache_coin && cache_coin->IsDirty()};
auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, map, /*will_erase=*/true)};
view.BatchWrite(cursor, {});
auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, map, resource, /*will_clear=*/true)};
try {
view.BatchWrite(cursor, {});
} catch (...) {
FreeAllCoins(map, resource);
throw;
}
BOOST_CHECK_EQUAL(dirty_count, 0U);
FreeAllCoins(map, resource);
}

class SingleEntryCacheTest
Expand All @@ -673,11 +704,16 @@ class SingleEntryCacheTest
auto base_cache_coin{base_value == ABSENT ? MISSING : CoinEntry{base_value, CoinEntry::State::DIRTY}};
WriteCoinsViewEntry(base, base_cache_coin);
if (cache_coin) {
cache.usage() += InsertCoinsMapEntry(cache.map(), cache.sentinel(), *cache_coin);
cache.usage() += InsertCoinsMapEntry(cache.map(), cache.sentinel(), cache.resource(), *cache_coin);
cache.dirty() += cache_coin->IsDirty();
}
}

~SingleEntryCacheTest() noexcept
{
FreeAllCoins(cache.map(), cache.resource());
}

CCoinsViewCacheTest base{&CoinsViewEmpty::Get()};
CCoinsViewCacheTest cache{&base};
};
Expand Down Expand Up @@ -1119,6 +1155,27 @@ BOOST_AUTO_TEST_CASE(ccoins_emplace_duplicate_keeps_usage_balanced)
BOOST_CHECK(cache.AccessCoin(outpoint) == coin1);
}

BOOST_AUTO_TEST_CASE(ccoins_spend_moveout_keeps_usage_balanced)
{
CCoinsViewCacheTest base{&CoinsViewEmpty::Get()};
CCoinsViewCacheTest cache{&base};

const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()};
const Coin coin{CTxOut{m_rng.randrange(10), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 1)}, 1, false};

base.AddCoin(outpoint, Coin{coin}, /*possible_overwrite=*/false);
base.SelfTest();

BOOST_CHECK(cache.HaveCoin(outpoint));
cache.SelfTest();

Coin moveout;
BOOST_CHECK(cache.SpendCoin(outpoint, &moveout));
BOOST_CHECK(moveout == coin);
BOOST_CHECK_EQUAL(GetCoinsMapEntry(cache.map(), outpoint), SPENT_DIRTY);
cache.SelfTest();
}

BOOST_AUTO_TEST_CASE(ccoins_reset_guard)
{
CCoinsViewTest root{m_rng};
Expand Down
Loading
Loading