From 760fb22dc370b0882bd345ff913f9337a9b6e4c1 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 7 Mar 2026 14:39:03 -0500 Subject: [PATCH] test: add unit tests for CoinsViewOverlay::StartFetching Co-authored-by: l0rinc --- src/test/coinsviewoverlay_tests.cpp | 138 +++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 3 deletions(-) diff --git a/src/test/coinsviewoverlay_tests.cpp b/src/test/coinsviewoverlay_tests.cpp index fc7edfe97cd..d6403752b0e 100644 --- a/src/test/coinsviewoverlay_tests.cpp +++ b/src/test/coinsviewoverlay_tests.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include 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 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 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 fetched_inputs; + std::unordered_set 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("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("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()