fuzz: keep `coinscache_sim` backend free of spent coins
What changed, and why it matters
This is a change to a Bitcoin Core fuzz test, not to the live Bitcoin network code. It fixes a test-only simulator so that it no longer pretends that spent coins still exist in the underlying database. This makes the fuzz test behave more like the real code, but it does not fix a vulnerability in production Bitcoin Core.
No production action needed. Review as normal test-quality improvement. If auditing, confirm no production code paths are affected.
Security signals we found
Test-only code change
Removes artificial spent-coin behavior in fuzz simulator
Adds assertions to enforce unspent coins in mock backend
No change to consensus, networking, wallet, or mempool code
Evidence from the diff
The commit modifies src/test/fuzz/coinscache_sim.cpp. The CoinsViewBottom class is a mock/fake backend used only in fuzzing. Previously it deliberately returned spent coins for certain outpoints (n % 5 == 4) and stored spent coins in its map. The patch removes that artificial behavior: GetCoin() now asserts the coin is unspent and returns it only if present, BatchWrite() erases spent entries, and comparison assertions expect std::nullopt when the simulator has no coin. This aligns the simulator with the real CCoinsViewDB behavior of not storing spent coins.
Changed components
src/test/fuzz/coinscache_sim.cppInspect captured patch +13 / −16
diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp
index f57c2521..c6102c2a 100644
--- a/src/test/fuzz/coinscache_sim.cpp
+++ b/src/test/fuzz/coinscache_sim.cpp
@@ -138,8 +138,6 @@ struct CacheLevel
/** Class for the base of the hierarchy (roughly simulating a memory-backed CCoinsViewDB).
*
* The initial state consists of the empty UTXO set.
- * Coins whose output index is 4 (mod 5) have GetCoin() always succeed after being spent.
- * This exercises code paths with spent, non-DIRTY cache entries.
*/
class CoinsViewBottom final : public CCoinsView
{
@@ -148,16 +146,13 @@ class CoinsViewBottom final : public CCoinsView
public:
std::optional<Coin> GetCoin(const COutPoint& outpoint) const final
{
- // TODO GetCoin shouldn't return spent coins
- if (auto it = m_data.find(outpoint); it != m_data.end()) return it->second;
+ if (auto it{m_data.find(outpoint)}; it != m_data.end()) {
+ assert(!it->second.IsSpent());
+ return it->second;
+ }
return std::nullopt;
}
- bool HaveCoin(const COutPoint& outpoint) const final
- {
- return m_data.contains(outpoint);
- }
-
uint256 GetBestBlock() const final { return {}; }
std::vector<uint256> GetHeadBlocks() const final { return {}; }
std::unique_ptr<CCoinsViewCursor> Cursor() const final { return {}; }
@@ -167,18 +162,20 @@ public:
{
for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) {
if (it->second.IsDirty()) {
- if (it->second.coin.IsSpent() && (it->first.n % 5) != 4) {
+ if (it->second.coin.IsSpent()) {
m_data.erase(it->first);
- } else if (cursor.WillErase(*it)) {
- m_data[it->first] = std::move(it->second.coin);
} else {
- m_data[it->first] = it->second.coin;
+ if (cursor.WillErase(*it)) {
+ m_data[it->first] = std::move(it->second.coin);
+ } else {
+ m_data[it->first] = it->second.coin;
+ }
}
} else {
/* For non-dirty entries being written, compare them with what we have. */
auto it2 = m_data.find(it->first);
if (it->second.coin.IsSpent()) {
- assert(it2 == m_data.end() || it2->second.IsSpent());
+ assert(it2 == m_data.end());
} else {
assert(it2 != m_data.end());
assert(it->second.coin.out == it2->second.out);
@@ -263,7 +260,7 @@ FUZZ_TARGET(coinscache_sim)
auto realcoin = caches.back()->GetCoin(data.outpoints[outpointidx]);
// Compare results.
if (!sim.has_value()) {
- assert(!realcoin || realcoin->IsSpent());
+ assert(!realcoin);
} else {
assert(realcoin && !realcoin->IsSpent());
const auto& simcoin = data.coins[sim->first];
@@ -449,7 +446,7 @@ FUZZ_TARGET(coinscache_sim)
auto realcoin = bottom.GetCoin(data.outpoints[outpointidx]);
auto sim = lookup(outpointidx, 0);
if (!sim.has_value()) {
- assert(!realcoin || realcoin->IsSpent());
+ assert(!realcoin);
} else {
assert(realcoin && !realcoin->IsSpent());
assert(realcoin->out == data.coins[sim->first].out);
Why this scored 18/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.