From b637566c8d08d4007c9413dbbb8171d20e3cfad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 15 Feb 2026 15:16:27 +0100 Subject: [PATCH] coins: add explicit `CoinsViewEmpty` noop backend Introduce `CoinsViewEmpty` as an explicit no-op `CCoinsView` implementation, and define its singleton accessor out of line in `coins.cpp` to avoid `-Wunique-object-duplication` in shared-library builds.` Use it at call sites that intentionally want a no-op backend instead of constructing anonymous placeholder views. `CCoinsViewTest` and `CoinsViewBottom` now inherit defaults from `CoinsViewEmpty` (e.g. the unused `EstimateSize()`, which now returns 0). Co-authored-by: Ryan Ofsky --- src/bench/ccoins_caching.cpp | 3 +-- src/bitcoin-tx.cpp | 3 +-- src/coins.cpp | 6 ++++++ src/coins.h | 12 ++++++++++++ src/node/psbt.cpp | 3 +-- src/rpc/rawtransaction.cpp | 5 ++--- src/test/coins_tests.cpp | 13 +++++-------- src/test/fuzz/coins_view.cpp | 18 ++++++++---------- src/test/fuzz/coinscache_sim.cpp | 7 +------ src/test/fuzz/transaction.cpp | 3 +-- src/test/script_p2sh_tests.cpp | 3 +-- src/test/sigopcount_tests.cpp | 3 +-- src/test/transaction_tests.cpp | 15 +++++---------- src/test/util/setup_common.cpp | 3 +-- src/validation.cpp | 20 ++++++++------------ 15 files changed, 54 insertions(+), 63 deletions(-) diff --git a/src/bench/ccoins_caching.cpp b/src/bench/ccoins_caching.cpp index 82735b3286e..12ea0760801 100644 --- a/src/bench/ccoins_caching.cpp +++ b/src/bench/ccoins_caching.cpp @@ -26,8 +26,7 @@ static void CCoinsCaching(benchmark::Bench& bench) ECC_Context ecc_context{}; FillableSigningProvider keystore; - CCoinsView coinsDummy; - CCoinsViewCache coins(&coinsDummy); + CCoinsViewCache coins{&CoinsViewEmpty::Get()}; std::vector dummyTransactions = SetupDummyInputs(keystore, coins, {11 * COIN, 50 * COIN, 21 * COIN, 22 * COIN}); diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp index d4702a9cf0f..4a891ad8f18 100644 --- a/src/bitcoin-tx.cpp +++ b/src/bitcoin-tx.cpp @@ -586,8 +586,7 @@ static void MutateTxSign(CMutableTransaction& tx, const std::string& flagStr) // starts as a clone of the raw tx: CMutableTransaction mergedTx{tx}; const CMutableTransaction txv{tx}; - CCoinsView viewDummy; - CCoinsViewCache view(&viewDummy); + CCoinsViewCache view{&CoinsViewEmpty::Get()}; if (!registers.contains("privatekeys")) throw std::runtime_error("privatekeys register variable must be set."); diff --git a/src/coins.cpp b/src/coins.cpp index 87bbdcbfa9e..e5deef34348 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -41,6 +41,12 @@ void CCoinsViewBacked::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& b std::unique_ptr CCoinsViewBacked::Cursor() const { return base->Cursor(); } size_t CCoinsViewBacked::EstimateSize() const { return base->EstimateSize(); } +CoinsViewEmpty& CoinsViewEmpty::Get() +{ + static CoinsViewEmpty instance; + return instance; +} + std::optional CCoinsViewCache::PeekCoin(const COutPoint& outpoint) const { if (auto it{cacheCoins.find(outpoint)}; it != cacheCoins.end()) { diff --git a/src/coins.h b/src/coins.h index 68d469383e1..876c5ed8b20 100644 --- a/src/coins.h +++ b/src/coins.h @@ -342,6 +342,18 @@ public: virtual size_t EstimateSize() const { return 0; } }; +/** Noop coins view. */ +class CoinsViewEmpty : public CCoinsView +{ +protected: + CoinsViewEmpty() = default; + +public: + static CoinsViewEmpty& Get(); + + CoinsViewEmpty(const CoinsViewEmpty&) = delete; + CoinsViewEmpty& operator=(const CoinsViewEmpty&) = delete; +}; /** CCoinsView backed by another CCoinsView */ class CCoinsViewBacked : public CCoinsView diff --git a/src/node/psbt.cpp b/src/node/psbt.cpp index faedf0b6aab..9d79b42f29a 100644 --- a/src/node/psbt.cpp +++ b/src/node/psbt.cpp @@ -116,8 +116,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx) // Estimate the size CMutableTransaction mtx(*psbtx.tx); - CCoinsView view_dummy; - CCoinsViewCache view(&view_dummy); + CCoinsViewCache view{&CoinsViewEmpty::Get()}; bool success = true; for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) { diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index cae9bb6b7a0..74652c17421 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -623,8 +623,7 @@ static RPCMethod combinerawtransaction() CMutableTransaction mergedTx(txVariants[0]); // Fetch previous transactions (inputs): - CCoinsView viewDummy; - CCoinsViewCache view(&viewDummy); + CCoinsViewCache view{&CoinsViewEmpty::Get()}; { NodeContext& node = EnsureAnyNodeContext(request.context); const CTxMemPool& mempool = EnsureMemPool(node); @@ -638,7 +637,7 @@ static RPCMethod combinerawtransaction() view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail. } - view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long + view.SetBackend(CoinsViewEmpty::Get()); // switch back to avoid locking mempool for too long } // Use CTransaction for the constant parts of the diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index f8de7f5d3af..2a180f25936 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -38,14 +38,14 @@ bool operator==(const Coin &a, const Coin &b) { a.out == b.out; } -class CCoinsViewTest : public CCoinsView +class CCoinsViewTest : public CoinsViewEmpty { FastRandomContext& m_rng; uint256 hashBestBlock_; std::map map_; public: - CCoinsViewTest(FastRandomContext& rng) : m_rng{rng} {} + explicit CCoinsViewTest(FastRandomContext& rng) : m_rng{rng} {} std::optional GetCoin(const COutPoint& outpoint) const override { @@ -677,8 +677,7 @@ public: } } - CCoinsView root; - CCoinsViewCacheTest base{&root}; + CCoinsViewCacheTest base{&CoinsViewEmpty::Get()}; CCoinsViewCacheTest cache{&base}; }; @@ -1087,8 +1086,7 @@ BOOST_AUTO_TEST_CASE(coins_resource_is_used) BOOST_AUTO_TEST_CASE(ccoins_addcoin_exception_keeps_usage_balanced) { - CCoinsView root; - CCoinsViewCacheTest cache{&root}; + CCoinsViewCacheTest cache{&CoinsViewEmpty::Get()}; const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()}; @@ -1105,8 +1103,7 @@ BOOST_AUTO_TEST_CASE(ccoins_addcoin_exception_keeps_usage_balanced) BOOST_AUTO_TEST_CASE(ccoins_emplace_duplicate_keeps_usage_balanced) { - CCoinsView root; - CCoinsViewCacheTest cache{&root}; + CCoinsViewCacheTest cache{&CoinsViewEmpty::Get()}; const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()}; diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index 5af5996fa6a..c11581d2d3c 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -90,11 +90,11 @@ void initialize_coins_view() static const auto testing_setup = MakeNoLogFileContext<>(); } -void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& coins_view_cache, CCoinsView* backend_coins_view, bool is_db) +void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& coins_view_cache, CCoinsView* backend_coins_view) { + const bool is_db{dynamic_cast(backend_coins_view) != nullptr}; bool good_data{true}; auto* original_backend{backend_coins_view}; - CCoinsView coins_view_empty{}; if (is_db) coins_view_cache.SetBestBlock(uint256::ONE); COutPoint random_out_point; @@ -158,7 +158,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co // Reset() clears the best block; db backends require a non-null hash. if (is_db) coins_view_cache.SetBestBlock(uint256::ONE); } - backend_coins_view = use_original_backend ? original_backend : &coins_view_empty; + backend_coins_view = use_original_backend ? original_backend : &CoinsViewEmpty::Get(); coins_view_cache.SetBackend(*backend_coins_view); }, [&] { @@ -350,9 +350,8 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co FUZZ_TARGET(coins_view, .init = initialize_coins_view) { FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; - CCoinsView backend_coins_view; - CCoinsViewCache coins_view_cache{&backend_coins_view, /*deterministic=*/true}; - TestCoinsView(fuzzed_data_provider, coins_view_cache, &backend_coins_view, /*is_db=*/false); + CCoinsViewCache coins_view_cache{&CoinsViewEmpty::Get(), /*deterministic=*/true}; + TestCoinsView(fuzzed_data_provider, coins_view_cache, &CoinsViewEmpty::Get()); } FUZZ_TARGET(coins_view_db, .init = initialize_coins_view) @@ -365,7 +364,7 @@ FUZZ_TARGET(coins_view_db, .init = initialize_coins_view) }; CCoinsViewDB backend_coins_view{std::move(db_params), CoinsViewOptions{}}; CCoinsViewCache coins_view_cache{&backend_coins_view, /*deterministic=*/true}; - TestCoinsView(fuzzed_data_provider, coins_view_cache, &backend_coins_view, /*is_db=*/true); + TestCoinsView(fuzzed_data_provider, coins_view_cache, &backend_coins_view); } // Creates a CoinsViewOverlay and a MutationGuardCoinsViewCache as the base. @@ -375,8 +374,7 @@ FUZZ_TARGET(coins_view_db, .init = initialize_coins_view) FUZZ_TARGET(coins_view_overlay, .init = initialize_coins_view) { FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; - CCoinsView backend_base_coins_view; - MutationGuardCoinsViewCache backend_cache{&backend_base_coins_view, /*deterministic=*/true}; + MutationGuardCoinsViewCache backend_cache{&CoinsViewEmpty::Get(), /*deterministic=*/true}; CoinsViewOverlay coins_view_cache{&backend_cache, /*deterministic=*/true}; - TestCoinsView(fuzzed_data_provider, coins_view_cache, &backend_cache, /*is_db=*/false); + TestCoinsView(fuzzed_data_provider, coins_view_cache, &backend_cache); } diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp index c8534e4f60c..238281321f5 100644 --- a/src/test/fuzz/coinscache_sim.cpp +++ b/src/test/fuzz/coinscache_sim.cpp @@ -139,7 +139,7 @@ struct CacheLevel * * The initial state consists of the empty UTXO set. */ -class CoinsViewBottom final : public CCoinsView +class CoinsViewBottom final : public CoinsViewEmpty { std::map m_data; @@ -153,11 +153,6 @@ public: return std::nullopt; } - uint256 GetBestBlock() const final { return {}; } - std::vector GetHeadBlocks() const final { return {}; } - std::unique_ptr Cursor() const final { return {}; } - size_t EstimateSize() const final { return m_data.size(); } - void BatchWrite(CoinsViewCacheCursor& cursor, const uint256&) final { for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) { diff --git a/src/test/fuzz/transaction.cpp b/src/test/fuzz/transaction.cpp index a8b2d2a207d..299db8c10c5 100644 --- a/src/test/fuzz/transaction.cpp +++ b/src/test/fuzz/transaction.cpp @@ -86,8 +86,7 @@ FUZZ_TARGET(transaction, .init = initialize_transaction) (void)RecursiveDynamicUsage(tx); (void)SignalsOptInRBF(tx); - CCoinsView coins_view; - const CCoinsViewCache coins_view_cache(&coins_view); + const CCoinsViewCache coins_view_cache{&CoinsViewEmpty::Get()}; (void)ValidateInputsStandardness(tx, coins_view_cache); (void)IsWitnessStandard(tx, coins_view_cache); diff --git a/src/test/script_p2sh_tests.cpp b/src/test/script_p2sh_tests.cpp index 04a03ca4970..f80090aa2b1 100644 --- a/src/test/script_p2sh_tests.cpp +++ b/src/test/script_p2sh_tests.cpp @@ -277,8 +277,7 @@ BOOST_AUTO_TEST_CASE(switchover) BOOST_AUTO_TEST_CASE(ValidateInputsStandardness) { - CCoinsView coinsDummy; - CCoinsViewCache coins(&coinsDummy); + CCoinsViewCache coins{&CoinsViewEmpty::Get()}; FillableSigningProvider keystore; CKey key[6]; for (int i = 0; i < 6; i++) diff --git a/src/test/sigopcount_tests.cpp b/src/test/sigopcount_tests.cpp index d3fec0f9357..a463f43ea88 100644 --- a/src/test/sigopcount_tests.cpp +++ b/src/test/sigopcount_tests.cpp @@ -116,8 +116,7 @@ BOOST_AUTO_TEST_CASE(GetTxSigOpCost) CMutableTransaction spendingTx; // Create utxo set - CCoinsView coinsDummy; - CCoinsViewCache coins(&coinsDummy); + CCoinsViewCache coins{&CoinsViewEmpty::Get()}; // Create key CKey key = GenerateRandomKey(); CPubKey pubkey = key.GetPubKey(); diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index 2bb9e8a475e..19c416c225f 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -392,8 +392,7 @@ BOOST_AUTO_TEST_CASE(basic_transaction_tests) BOOST_AUTO_TEST_CASE(test_Get) { FillableSigningProvider keystore; - CCoinsView coinsDummy; - CCoinsViewCache coins(&coinsDummy); + CCoinsViewCache coins{&CoinsViewEmpty::Get()}; std::vector dummyTransactions = SetupDummyInputs(keystore, coins, {11*CENT, 50*CENT, 21*CENT, 22*CENT}); @@ -749,8 +748,7 @@ BOOST_AUTO_TEST_CASE(test_witness) BOOST_AUTO_TEST_CASE(test_IsStandard) { FillableSigningProvider keystore; - CCoinsView coinsDummy; - CCoinsViewCache coins(&coinsDummy); + CCoinsViewCache coins{&CoinsViewEmpty::Get()}; std::vector dummyTransactions = SetupDummyInputs(keystore, coins, {11*CENT, 50*CENT, 21*CENT, 22*CENT}); @@ -1022,8 +1020,7 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops) { - CCoinsView coins_dummy; - CCoinsViewCache coins(&coins_dummy); + CCoinsViewCache coins{&CoinsViewEmpty::Get()}; CKey key; key.MakeNewKey(true); @@ -1132,8 +1129,7 @@ BOOST_AUTO_TEST_CASE(max_standard_legacy_sigops) BOOST_AUTO_TEST_CASE(checktxinputs_invalid_transactions_test) { auto check_invalid{[](CAmount input_value, CAmount output_value, bool coinbase, int spend_height, TxValidationResult expected_result, std::string_view expected_reason) { - CCoinsView coins_dummy; - CCoinsViewCache inputs(&coins_dummy); + CCoinsViewCache inputs{&CoinsViewEmpty::Get()}; const COutPoint prevout{Txid::FromUint256(uint256::ONE), 0}; inputs.AddCoin(prevout, Coin{{input_value, CScript() << OP_TRUE}, /*nHeightIn=*/1, coinbase}, /*possible_overwrite=*/false); @@ -1181,8 +1177,7 @@ BOOST_AUTO_TEST_CASE(getvalueout_out_of_range_throws) /** Sanity check the return value of SpendsNonAnchorWitnessProg for various output types. */ BOOST_AUTO_TEST_CASE(spends_witness_prog) { - CCoinsView coins_dummy; - CCoinsViewCache coins(&coins_dummy); + CCoinsViewCache coins{&CoinsViewEmpty::Get()}; CKey key; key.MakeNewKey(true); const CPubKey pubkey{key.GetPubKey()}; diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 8b746193f93..5c50553e9f4 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -465,8 +465,7 @@ std::pair TestChain100Setup::CreateValidTransactio keystore.AddKey(input_signing_key); } // - Populate a CoinsViewCache with the unspent output - CCoinsView coins_view; - CCoinsViewCache coins_cache(&coins_view); + CCoinsViewCache coins_cache{&CoinsViewEmpty::Get()}; for (const auto& input_transaction : input_transactions) { AddCoins(coins_cache, *input_transaction.get(), input_height); } diff --git a/src/validation.cpp b/src/validation.cpp index 2b0de23db3b..e15afd6c1f9 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -437,7 +437,7 @@ class MemPoolAccept public: explicit MemPoolAccept(CTxMemPool& mempool, Chainstate& active_chainstate) : m_pool(mempool), - m_view(&m_dummy), + m_view(&CoinsViewEmpty::Get()), m_viewmempool(&active_chainstate.CoinsTip(), m_pool), m_active_chainstate(active_chainstate) { @@ -737,10 +737,6 @@ private: /** When m_view is connected to m_viewmempool as its backend, it can pull coins from the mempool and from the UTXO * set. This is also where temporary coins are stored. */ CCoinsViewMemPool m_viewmempool; - /** When m_view is connected to m_dummy, it can no longer look up coins from the mempool or UTXO set (meaning no disk - * operations happen), but can still return coins it accessed previously. Useful for keeping track of which coins - * were pulled from disk. */ - CCoinsView m_dummy; Chainstate& m_active_chainstate; @@ -867,14 +863,14 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws) } } - // This is const, but calls into the back end CoinsViews. The CCoinsViewDB at the bottom of the - // hierarchy brings the best block into scope. See CCoinsViewDB::GetBestBlock(). - m_view.GetBestBlock(); + // This is const, but calls into `CCoinsViewCache::GetBestBlock()` to refresh + // the cached best block through `m_viewmempool` after caching inputs. + (void)m_view.GetBestBlock(); - // we have all inputs cached now, so switch back to dummy (to protect - // against bugs where we pull more inputs from disk that miss being added - // to coins_to_uncache) - m_view.SetBackend(m_dummy); + // All required inputs are cached now, so switch m_view to the empty backend. + // This keeps already-fetched cache entries for later checks and prevents new + // backend lookups (which would avoid coins_to_uncache tracking). + m_view.SetBackend(CoinsViewEmpty::Get()); assert(m_active_chainstate.m_blockman.LookupBlockIndex(m_view.GetBestBlock()) == m_active_chainstate.m_chain.Tip());