util: introduce TrySub to prevent unsigned underflow

Introduce `TrySub(T&, U)` which subtracts an unsigned integral `U` from an unsigned integral `T`, returning `false` on underflow.
Use with `Assume(TrySub(...))` at coins cache accounting decrement sites so invariant violations fail immediately rather than silently wrapping.

Co-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>
Co-authored-by: Pieter Wuille <pieter@wuille.net>
This commit is contained in:
committed 2026-02-23 15:56:25 +01:00
1 parent d9c7364ac5
commit b8fa6f0f70
3 files changed
+18 -9

No files matched your search

+8 -8
View File
@@ -113,8 +113,8 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi
fresh = !it->second.IsDirty(); fresh = !it->second.IsDirty();
} }
if (!inserted) { if (!inserted) {
m_dirty_count -= it->second.IsDirty(); Assume(TrySub(m_dirty_count, it->second.IsDirty()));
cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage(); Assume(TrySub(cachedCoinsUsage, it->second.coin.DynamicMemoryUsage()));
} }
it->second.coin = std::move(coin); it->second.coin = std::move(coin);
CCoinsCacheEntry::SetDirty(*it, m_sentinel); CCoinsCacheEntry::SetDirty(*it, m_sentinel);
@@ -153,8 +153,8 @@ void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool
bool CCoinsViewCache::SpendCoin(const COutPoint &outpoint, Coin* moveout) { bool CCoinsViewCache::SpendCoin(const COutPoint &outpoint, Coin* moveout) {
CCoinsMap::iterator it = FetchCoin(outpoint); CCoinsMap::iterator it = FetchCoin(outpoint);
if (it == cacheCoins.end()) return false; if (it == cacheCoins.end()) return false;
m_dirty_count -= it->second.IsDirty(); Assume(TrySub(m_dirty_count, it->second.IsDirty()));
cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage(); Assume(TrySub(cachedCoinsUsage, it->second.coin.DynamicMemoryUsage()));
TRACEPOINT(utxocache, spent, TRACEPOINT(utxocache, spent,
outpoint.hash.data(), outpoint.hash.data(),
(uint32_t)outpoint.n, (uint32_t)outpoint.n,
@@ -248,12 +248,12 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& ha
if (itUs->second.IsFresh() && it->second.coin.IsSpent()) { if (itUs->second.IsFresh() && it->second.coin.IsSpent()) {
// The grandparent cache does not have an entry, and the coin // The grandparent cache does not have an entry, and the coin
// has been spent. We can just delete it from the parent cache. // has been spent. We can just delete it from the parent cache.
m_dirty_count -= itUs->second.IsDirty(); Assume(TrySub(m_dirty_count, itUs->second.IsDirty()));
cachedCoinsUsage -= itUs->second.coin.DynamicMemoryUsage(); Assume(TrySub(cachedCoinsUsage, itUs->second.coin.DynamicMemoryUsage()));
cacheCoins.erase(itUs); cacheCoins.erase(itUs);
} else { } else {
// A normal modification. // A normal modification.
cachedCoinsUsage -= itUs->second.coin.DynamicMemoryUsage(); Assume(TrySub(cachedCoinsUsage, itUs->second.coin.DynamicMemoryUsage()));
if (cursor.WillErase(*it)) { if (cursor.WillErase(*it)) {
// Since this entry will be erased, // Since this entry will be erased,
// we can move the coin into us instead of copying it // we can move the coin into us instead of copying it
@@ -311,7 +311,7 @@ void CCoinsViewCache::Uncache(const COutPoint& hash)
{ {
CCoinsMap::iterator it = cacheCoins.find(hash); CCoinsMap::iterator it = cacheCoins.find(hash);
if (it != cacheCoins.end() && !it->second.IsDirty()) { if (it != cacheCoins.end() && !it->second.IsDirty()) {
cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage(); Assume(TrySub(cachedCoinsUsage, it->second.coin.DynamicMemoryUsage()));
TRACEPOINT(utxocache, uncache, TRACEPOINT(utxocache, uncache,
hash.hash.data(), hash.hash.data(),
(uint32_t)hash.n, (uint32_t)hash.n,
+2 -1
View File
@@ -15,6 +15,7 @@
#include <support/allocators/pool.h> #include <support/allocators/pool.h>
#include <uint256.h> #include <uint256.h>
#include <util/check.h> #include <util/check.h>
#include <util/overflow.h>
#include <util/hasher.h> #include <util/hasher.h>
#include <cassert> #include <cassert>
@@ -278,7 +279,7 @@ struct CoinsViewCacheCursor
inline CoinsCachePair* NextAndMaybeErase(CoinsCachePair& current) noexcept inline CoinsCachePair* NextAndMaybeErase(CoinsCachePair& current) noexcept
{ {
const auto next_entry{current.second.Next()}; const auto next_entry{current.second.Next()};
m_dirty_count -= current.second.IsDirty(); Assume(TrySub(m_dirty_count, current.second.IsDirty()));
// If we are not going to erase the cache, we must still erase spent entries. // If we are not going to erase the cache, we must still erase spent entries.
// Otherwise, clear the state of the entry. // Otherwise, clear the state of the entry.
if (!m_will_erase) { if (!m_will_erase) {
+8
View File
@@ -31,6 +31,14 @@ template <class T>
return i + j; return i + j;
} }
template <std::unsigned_integral T, std::unsigned_integral U>
[[nodiscard]] constexpr bool TrySub(T& i, const U j) noexcept
{
if (i < T{j}) return false;
i -= T{j};
return true;
}
template <class T> template <class T>
[[nodiscard]] T SaturatingAdd(const T i, const T j) noexcept [[nodiscard]] T SaturatingAdd(const T i, const T j) noexcept
{ {