coins: add explicit `CoinsViewEmpty` noop backend
What changed, and why it matters
This commit is a code cleanup and build-fix change. It introduces a single, shared 'empty' coins view object and replaces many places that created their own throwaway empty view. There is no security bug being fixed or introduced here; it only makes the code more consistent and avoids a compiler warning in shared-library builds.
No security action required. Treat as normal refactoring/build-fix commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change adds a CoinsViewEmpty singleton (explicit no-op CCoinsView implementation) with an out-of-line singleton accessor in coins.cpp to prevent -Wunique-object-duplication warnings. It then replaces anonymous CCoinsView stack instances used as dummy backends with CoinsViewEmpty::Get(). CCoinsViewTest and CoinsViewBottom now inherit from CoinsViewEmpty. The behavior of the no-op backend remains identical (default GetCoin, GetBestBlock, Cursor, etc., and now EstimateSize() returns 0 instead of being overridden).
Changed components
src/coins.hsrc/coins.cppsrc/validation.cppsrc/node/psbt.cppsrc/rpc/rawtransaction.cppsrc/bitcoin-tx.cppsrc/bench/ccoins_caching.cppsrc/test/coins_tests.cppsrc/test/fuzz/coins_view.cppsrc/test/fuzz/coinscache_sim.cppsrc/test/fuzz/transaction.cppsrc/test/script_p2sh_tests.cppsrc/test/sigopcount_tests.cppsrc/test/transaction_tests.cppsrc/test/util/setup_common.cppInspect captured patch +54 / −63
diff --git a/src/bench/ccoins_caching.cpp b/src/bench/ccoins_caching.cpp
index 82735b32..12ea0760 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<CMutableTransaction> 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 d4702a9c..4a891ad8 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 87bbdcbf..e5deef34 100644
--- a/src/coins.cpp
+++ b/src/coins.cpp
@@ -41,6 +41,12 @@ void CCoinsViewBacked::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& b
std::unique_ptr<CCoinsViewCursor> CCoinsViewBacked::Cursor() const { return base->Cursor(); }
size_t CCoinsViewBacked::EstimateSize() const { return base->EstimateSize(); }
+CoinsViewEmpty& CoinsViewEmpty::Get()
+{
+ static CoinsViewEmpty instance;
+ return instance;
+}
+
std::optional<Coin> 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 68d46938..876c5ed8 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 faedf0b6..9d79b42f 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 cae9bb6b..74652c17 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 f8de7f5d..2a180f25 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<COutPoint, Coin> map_;
public:
- CCoinsViewTest(FastRandomContext& rng) : m_rng{rng} {}
+ explicit CCoinsViewTest(FastRandomContext& rng) : m_rng{rng} {}
std::optional<Coin> 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 5af5996f..c11581d2 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<CCoinsViewDB*>(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 c8534e4f..23828132 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<COutPoint, Coin> m_data;
@@ -153,11 +153,6 @@ public:
return std::nullopt;
}
- uint256 GetBestBlock() const final { return {}; }
- std::vector<uint256> GetHeadBlocks() const final { return {}; }
- std::unique_ptr<CCoinsViewCursor> 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 a8b2d2a2..299db8c1 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 04a03ca4..f80090aa 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 d3fec0f9..a463f43e 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 2bb9e8a4..19c416c2 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<CMutableTransaction> 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<CMutableTransaction> 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 8b746193..5c50553e 100644
--- a/src/test/util/setup_common.cpp
+++ b/src/test/util/setup_common.cpp
@@ -465,8 +465,7 @@ std::pair<CMutableTransaction, CAmount> 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 2b0de23d..e15afd6c 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());
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.