coins: add PeekCoin()

Introduce a helper to look up a Coin through a stack of CCoinsViewCache layers without populating parent caches.

This is useful for ephemeral views (e.g. during ConnectBlock) that want to avoid polluting CoinsTip() when validating invalid blocks.

Co-authored-by: l0rinc <pap.lorinc@gmail.com>
Co-authored-by: Pieter Wuille <pieter@wuille.net>
Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
This commit is contained in:
Andrew Toth 2025-12-24 09:00:10 +02:00
parent 6750744eb3
commit 69b01af0eb
No known key found for this signature in database
GPG Key ID: 60007AFC8938B018
4 changed files with 48 additions and 1 deletions

View File

@ -15,6 +15,7 @@ TRACEPOINT_SEMAPHORE(utxocache, spent);
TRACEPOINT_SEMAPHORE(utxocache, uncache);
std::optional<Coin> CCoinsView::GetCoin(const COutPoint& outpoint) const { return std::nullopt; }
std::optional<Coin> CCoinsView::PeekCoin(const COutPoint& outpoint) const { return GetCoin(outpoint); }
uint256 CCoinsView::GetBestBlock() const { return uint256(); }
std::vector<uint256> CCoinsView::GetHeadBlocks() const { return std::vector<uint256>(); }
void CCoinsView::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock)
@ -31,6 +32,7 @@ bool CCoinsView::HaveCoin(const COutPoint &outpoint) const
CCoinsViewBacked::CCoinsViewBacked(CCoinsView *viewIn) : base(viewIn) { }
std::optional<Coin> CCoinsViewBacked::GetCoin(const COutPoint& outpoint) const { return base->GetCoin(outpoint); }
std::optional<Coin> CCoinsViewBacked::PeekCoin(const COutPoint& outpoint) const { return base->PeekCoin(outpoint); }
bool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); }
uint256 CCoinsViewBacked::GetBestBlock() const { return base->GetBestBlock(); }
std::vector<uint256> CCoinsViewBacked::GetHeadBlocks() const { return base->GetHeadBlocks(); }
@ -39,6 +41,14 @@ void CCoinsViewBacked::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& h
std::unique_ptr<CCoinsViewCursor> CCoinsViewBacked::Cursor() const { return base->Cursor(); }
size_t CCoinsViewBacked::EstimateSize() const { return base->EstimateSize(); }
std::optional<Coin> CCoinsViewCache::PeekCoin(const COutPoint& outpoint) const
{
if (auto it{cacheCoins.find(outpoint)}; it != cacheCoins.end()) {
return it->second.coin.IsSpent() ? std::nullopt : std::optional{it->second.coin};
}
return base->PeekCoin(outpoint);
}
CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) :
CCoinsViewBacked(baseIn), m_deterministic(deterministic),
cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
@ -393,3 +403,8 @@ bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint& outpoint) const
{
return ExecuteBackedWrapper<bool>([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks);
}
std::optional<Coin> CCoinsViewErrorCatcher::PeekCoin(const COutPoint& outpoint) const
{
return ExecuteBackedWrapper<std::optional<Coin>>([&]() { return CCoinsViewBacked::PeekCoin(outpoint); }, m_err_callbacks);
}

View File

@ -302,9 +302,15 @@ class CCoinsView
{
public:
//! Retrieve the Coin (unspent transaction output) for a given outpoint.
//! May populate the cache. Use PeekCoin() to perform a non-caching lookup.
virtual std::optional<Coin> GetCoin(const COutPoint& outpoint) const;
//! Retrieve the Coin (unspent transaction output) for a given outpoint, without caching results.
//! Does not populate the cache. Use GetCoin() to cache the result.
virtual std::optional<Coin> PeekCoin(const COutPoint& outpoint) const;
//! Just check whether a given outpoint is unspent.
//! May populate the cache. Use PeekCoin() to perform a non-caching lookup.
virtual bool HaveCoin(const COutPoint &outpoint) const;
//! Retrieve the block hash whose state this CCoinsView currently represents
@ -340,6 +346,7 @@ protected:
public:
CCoinsViewBacked(CCoinsView *viewIn);
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override;
std::optional<Coin> PeekCoin(const COutPoint& outpoint) const override;
bool HaveCoin(const COutPoint &outpoint) const override;
uint256 GetBestBlock() const override;
std::vector<uint256> GetHeadBlocks() const override;
@ -386,6 +393,7 @@ public:
// Standard CCoinsView methods
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override;
std::optional<Coin> PeekCoin(const COutPoint& outpoint) const override;
bool HaveCoin(const COutPoint &outpoint) const override;
uint256 GetBestBlock() const override;
void SetBestBlock(const uint256 &hashBlock);
@ -536,6 +544,7 @@ public:
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override;
bool HaveCoin(const COutPoint &outpoint) const override;
std::optional<Coin> PeekCoin(const COutPoint& outpoint) const override;
private:
/** A list of callbacks to execute upon leveldb read error. */

View File

@ -1159,4 +1159,25 @@ BOOST_AUTO_TEST_CASE(ccoins_reset_guard)
BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
}
BOOST_AUTO_TEST_CASE(ccoins_peekcoin)
{
CCoinsViewTest base{m_rng};
// Populate the base view with a coin.
const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()};
const Coin coin{CTxOut{m_rng.randrange(10), CScript{}}, 1, false};
{
CCoinsViewCache cache{&base};
cache.AddCoin(outpoint, Coin{coin}, /*possible_overwrite=*/false);
cache.Flush();
}
// Verify PeekCoin can read through the cache stack without mutating the intermediate cache.
CCoinsViewCacheTest main_cache{&base};
const auto fetched{main_cache.PeekCoin(outpoint)};
BOOST_CHECK(fetched.has_value());
BOOST_CHECK(*fetched == coin);
BOOST_CHECK(!main_cache.HaveCoinInCache(outpoint));
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -257,7 +257,9 @@ FUZZ_TARGET(coinscache_sim)
// Look up in simulation data.
auto sim = lookup(outpointidx);
// Look up in real caches.
auto realcoin = caches.back()->GetCoin(data.outpoints[outpointidx]);
auto realcoin = provider.ConsumeBool() ?
caches.back()->PeekCoin(data.outpoints[outpointidx]) :
caches.back()->GetCoin(data.outpoints[outpointidx]);
// Compare results.
if (!sim.has_value()) {
assert(!realcoin);