test: add unit tests for CoinsViewOverlay::StartFetching
What changed, and why it matters
This commit only adds and updates unit tests for an existing Bitcoin Core component called CoinsViewOverlay. It does not change any production code that runs on the live Bitcoin network, so it cannot directly introduce a security vulnerability or fix one in shipped software. The changes make the test block creation more realistic by allowing some transactions to spend outputs from earlier transactions in the same block, and they add tests for edge cases like out-of-order coin access, disabled worker threads, and reusable overlay state.
No security action required. Treat as normal test-only code review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies src/test/coinsviewoverlay_tests.cpp. It enhances CreateBlock() so every 20th transaction spends a previous transaction’s output within the same block, and updates PopulateView/CheckCache to avoid treating such intra-block spends as separate inputs. It adds calls to view.StartFetching(block) in existing test cases and introduces four new BOOST_AUTO_TEST_CASEs: access_non_input_coins, fetch_out_of_order_input_uses_normal_lookup, fetch_state_is_reusable_after_teardown, and two tests under a new suite for disabled/interrupted thread pools. No consensus, networking, wallet, or script code is changed.
Changed components
src/test/coinsviewoverlay_tests.cppInspect captured patch +135 / −3
diff --git a/src/test/coinsviewoverlay_tests.cpp b/src/test/coinsviewoverlay_tests.cpp
index fc7edfe9..d6403752 100644
--- a/src/test/coinsviewoverlay_tests.cpp
+++ b/src/test/coinsviewoverlay_tests.cpp
@@ -19,6 +19,8 @@
#include <cstring>
#include <memory>
#include <ranges>
+#include <unordered_set>
+#include <vector>
namespace {
@@ -37,10 +39,13 @@ CBlock CreateBlock() noexcept
coinbase.vin.emplace_back();
block.vtx.push_back(MakeTransactionRef(coinbase));
+ Txid prevhash{Txid::FromUint256(uint256{1})};
+
for (const auto i : std::views::iota(1, NUM_TXS)) {
CMutableTransaction tx;
- Txid txid{Txid::FromUint256(uint256(i))};
+ const Txid txid{i % 20 == 0 ? prevhash : Txid::FromUint256(uint256(i))};
tx.vin.emplace_back(txid, 0);
+ prevhash = tx.GetHash();
block.vtx.push_back(MakeTransactionRef(tx));
}
@@ -52,12 +57,16 @@ void PopulateView(const CBlock& block, CCoinsView& view, bool spent = false)
CCoinsViewCache cache{&view};
cache.SetBestBlock(uint256::ONE);
+ std::unordered_set<Txid, SaltedTxidHasher> txids{};
+ txids.reserve(block.vtx.size() - 1);
for (const auto& tx : block.vtx | std::views::drop(1)) {
for (const auto& in : tx->vin) {
+ if (txids.contains(in.prevout.hash)) continue;
Coin coin{};
if (!spent) coin.out.nValue = 1;
cache.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin));
}
+ txids.emplace(tx->GetHash());
}
cache.Flush();
@@ -66,6 +75,8 @@ void PopulateView(const CBlock& block, CCoinsView& view, bool spent = false)
void CheckCache(const CBlock& block, const CCoinsViewCache& cache)
{
uint32_t counter{0};
+ std::unordered_set<Txid, SaltedTxidHasher> txids{};
+ txids.reserve(block.vtx.size() - 1);
for (const auto& tx : block.vtx) {
if (tx->IsCoinBase()) {
@@ -76,9 +87,11 @@ void CheckCache(const CBlock& block, const CCoinsViewCache& cache)
const auto& first{cache.AccessCoin(outpoint)};
const auto& second{cache.AccessCoin(outpoint)};
BOOST_CHECK_EQUAL(&first, &second);
- ++counter;
- BOOST_CHECK(cache.HaveCoinInCache(outpoint));
+ const auto have{cache.HaveCoinInCache(outpoint)};
+ BOOST_CHECK_NE(txids.contains(outpoint.hash), have);
+ counter += have;
}
+ txids.emplace(tx->GetHash());
}
}
BOOST_CHECK_EQUAL(cache.GetCacheSize(), counter);
@@ -95,6 +108,7 @@ BOOST_AUTO_TEST_CASE(fetch_inputs_from_db)
PopulateView(block, db);
CCoinsViewCache main_cache{&db};
CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()};
+ const auto reset_guard{view.StartFetching(block)};
const auto& outpoint{block.vtx[1]->vin[0].prevout};
BOOST_CHECK(view.HaveCoin(outpoint));
@@ -122,6 +136,7 @@ BOOST_AUTO_TEST_CASE(fetch_inputs_from_cache)
CCoinsViewCache main_cache{&db};
PopulateView(block, main_cache);
CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()};
+ const auto reset_guard{view.StartFetching(block)};
CheckCache(block, view);
const auto& outpoint{block.vtx[1]->vin[0].prevout};
@@ -142,6 +157,7 @@ BOOST_AUTO_TEST_CASE(fetch_no_double_spend)
// Add all inputs as spent already in cache
PopulateView(block, main_cache, /*spent=*/true);
CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()};
+ const auto reset_guard{view.StartFetching(block)};
for (const auto& tx : block.vtx) {
for (const auto& in : tx->vin) {
const auto& c{view.AccessCoin(in.prevout)};
@@ -160,6 +176,7 @@ BOOST_AUTO_TEST_CASE(fetch_no_inputs)
CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
CCoinsViewCache main_cache{&db};
CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()};
+ const auto reset_guard{view.StartFetching(block)};
for (const auto& tx : block.vtx) {
for (const auto& in : tx->vin) {
const auto& c{view.AccessCoin(in.prevout)};
@@ -171,5 +188,120 @@ BOOST_AUTO_TEST_CASE(fetch_no_inputs)
BOOST_CHECK_EQUAL(view.GetCacheSize(), 0);
}
+// Access coins that are not block inputs
+BOOST_AUTO_TEST_CASE(access_non_input_coins)
+{
+ CBlock block;
+ CMutableTransaction coinbase;
+ coinbase.vin.emplace_back();
+ block.vtx.push_back(MakeTransactionRef(coinbase));
+ CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ CCoinsViewCache main_cache{&db};
+ Coin coin{};
+ coin.out.nValue = 1;
+ const COutPoint outpoint{Txid::FromUint256(uint256::ZERO), 0};
+ main_cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, std::move(coin));
+
+ CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()};
+ const auto reset_guard{view.StartFetching(block)};
+
+ // Non-input fallback hit.
+ BOOST_CHECK(!view.AccessCoin(outpoint).IsSpent());
+
+ // Non-input fallback miss.
+ const COutPoint missing_outpoint{Txid::FromUint256(uint256::ONE), 0};
+ BOOST_CHECK(view.AccessCoin(missing_outpoint).IsSpent());
+ BOOST_CHECK(!view.HaveCoinInCache(missing_outpoint));
+}
+
+// Access a fetched input out of order (i.e. not the next one in m_inputs).
+// FetchCoinFromBase must fall back to base->PeekCoin, and the coin must still
+// be inserted into the cache.
+BOOST_AUTO_TEST_CASE(fetch_out_of_order_input_uses_normal_lookup)
+{
+ const auto block{CreateBlock()};
+ CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ CCoinsViewCache main_cache{&db};
+ PopulateView(block, main_cache);
+
+ std::vector<COutPoint> fetched_inputs;
+ std::unordered_set<Txid, SaltedTxidHasher> txids;
+ txids.reserve(block.vtx.size() - 1);
+ for (const auto& tx : block.vtx | std::views::drop(1)) {
+ for (const auto& input : tx->vin) {
+ if (!txids.contains(input.prevout.hash)) fetched_inputs.push_back(input.prevout);
+ }
+ txids.emplace(tx->GetHash());
+ }
+ BOOST_REQUIRE_GE(fetched_inputs.size(), 2U);
+
+ CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()};
+ const auto reset_guard{view.StartFetching(block)};
+
+ const auto& out_of_order_input{fetched_inputs[1]};
+ BOOST_CHECK(!view.HaveCoinInCache(out_of_order_input));
+ BOOST_CHECK(!view.AccessCoin(out_of_order_input).IsSpent());
+ BOOST_CHECK(view.HaveCoinInCache(out_of_order_input));
+
+ CheckCache(block, view);
+}
+
+// The ResetGuard returned by StartFetching must clear all per-block state when
+// it goes out of scope, so the overlay can be reused for a subsequent block.
+// Flush must also clear all per-block state to be reused.
+BOOST_AUTO_TEST_CASE(fetch_state_is_reusable_after_teardown)
+{
+ const auto block{CreateBlock()};
+ CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ CCoinsViewCache main_cache{&db};
+ PopulateView(block, main_cache);
+ CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()};
+
+ for (const bool use_flush : {false, true, false}) {
+ {
+ const auto reset_guard{view.StartFetching(block)};
+ CheckCache(block, view);
+ BOOST_CHECK_GT(view.GetCacheSize(), 0U);
+ if (use_flush) {
+ view.SetBestBlock(uint256::ONE);
+ view.Flush();
+ }
+ }
+ BOOST_CHECK_EQUAL(view.GetCacheSize(), 0U);
+ }
+}
+
BOOST_AUTO_TEST_SUITE_END()
+BOOST_AUTO_TEST_SUITE(coinsviewoverlay_tests_noworkers)
+
+// Test that disabled input fetching falls back to normal cache lookups via base->PeekCoin.
+BOOST_AUTO_TEST_CASE(fetch_unstarted_thread_pool)
+{
+ const auto block{CreateBlock()};
+ CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ CCoinsViewCache main_cache{&db};
+ PopulateView(block, main_cache);
+ auto thread_pool{std::make_shared<ThreadPool>("fetch_none")};
+ CoinsViewOverlay view{&main_cache, thread_pool};
+ const auto reset_guard{view.StartFetching(block)};
+ CheckCache(block, view);
+}
+
+// Test that an interrupted thread pool falls back to normal cache lookups via base->PeekCoin.
+BOOST_AUTO_TEST_CASE(fetch_interrupted_thread_pool_uses_normal_lookup)
+{
+ const auto block{CreateBlock()};
+ CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ CCoinsViewCache main_cache{&db};
+ PopulateView(block, main_cache);
+
+ auto thread_pool{std::make_shared<ThreadPool>("fetch_intr")};
+ thread_pool->Start(DEFAULT_PREVOUTFETCH_THREADS);
+ thread_pool->Interrupt();
+ CoinsViewOverlay view{&main_cache, thread_pool};
+ const auto reset_guard{view.StartFetching(block)};
+ CheckCache(block, view);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.