coins: use SipHash-1-3-UJ for `CCoinsMap`
What changed, and why it matters
This commit changes the hash function used inside Bitcoin Core's in-memory coin cache (the UTXO cache) from SipHash-2-4 to a faster SipHash-1-3 variant. The change is framed as a performance optimization. The commit message and code comments explicitly argue that the weaker hash is still safe here because the keys are real transaction IDs, attackers cannot inject arbitrary long-lived keys into this cache, and the hash values are never saved to disk or shared between processes. There is no direct evidence in the commit of an exploitable vulnerability, but it is a deliberate relaxation of a cryptographic primitive in a security-sensitive data structure, which warrants scrutiny.
Treat as a routine but security-relevant optimization. Reviewers should verify that (1) no new code path allows attacker-controlled `COutPoint`s to become long-lived entries in `CCoinsMap`, (2) the `noexcept` hash contract is preserved so libstdc++ does not store cached hash values that could be leaked or misused, and (3) the deterministic salt constants are only used in tests/fuzzing and not in production. No immediate patch or incident response is indicated by the supplied materials.
Security signals we found
Reduced-round SipHash (1-3 instead of 2-4) introduced for a core security-sensitive data structure (UTXO cache).
Commit explicitly addresses attack surface: arbitrary prevouts cannot accumulate because FetchCoin() erases temporary entries on backend miss.
Hash outputs are process-local and not persisted/serialized, limiting cross-process collision attacks.
assumeutxo loader relies on full snapshot content-hash verification before activation, mitigating collision-based snapshot manipulation.
No CVE, advisory, or vendor security disclosure is present in the supplied materials.
Evidence from the diff
The patch introduces a new SaltedCoinsCacheHasher class in src/coins.h that uses SipHasher13UJ::Hash (SipHash-1-3 with a fixed-width ‘UJ’ jumbo-block path) for CCoinsMap, the std::unordered_map backing the UTXO cache. The existing SaltedOutpointHasher is kept for other users but is simplified to always use random salts (its deterministic constructor is removed). The new hasher is noexcept and hashes a COutPoint as the 32-byte txid plus a 64-bit zero-extended output index. The commit message provides a security rationale: retained cache entries contain computed txids; missing-input lookups create temporary entries that are erased immediately on backend miss; assumeutxo snapshot integrity is verified before activation; and hash outputs are process-local and not serialized. The change is therefore a performance-oriented switch to a reduced-round SipHash in a specific, constrained context.
Changed components
src/coins.cppsrc/coins.hsrc/util/hasher.cppsrc/util/hasher.hsrc/test/fuzz/coins_view.cppCCoinsMap / CCoinsViewCache (UTXO cache)SaltedOutpointHasher (other users remain on SipHash-2-4)Inspect captured patch +48 / −9
diff --git a/src/coins.cpp b/src/coins.cpp
index 72cbda2d..50e1aa05 100644
--- a/src/coins.cpp
+++ b/src/coins.cpp
@@ -8,6 +8,7 @@
#include <primitives/block.h>
#include <random.h>
#include <uint256.h>
+#include <util/hasher.h>
#include <util/log.h>
#include <util/threadpool.h>
#include <util/trace.h>
@@ -19,6 +20,13 @@ TRACEPOINT_SEMAPHORE(utxocache, add);
TRACEPOINT_SEMAPHORE(utxocache, spent);
TRACEPOINT_SEMAPHORE(utxocache, uncache);
+SaltedCoinsCacheHasher::SaltedCoinsCacheHasher(bool deterministic)
+ : m_hasher{
+ deterministic ? 0x8e819f2607a18de6 : FastRandomContext().rand64(),
+ deterministic ? 0xf4020d2e3983b0eb : FastRandomContext().rand64()}
+{
+}
+
CoinsViewEmpty& CoinsViewEmpty::Get()
{
static CoinsViewEmpty instance;
@@ -35,7 +43,7 @@ std::optional<Coin> CCoinsViewCache::PeekCoin(const COutPoint& outpoint) const
CCoinsViewCache::CCoinsViewCache(CCoinsView* in_base, bool deterministic) :
CCoinsViewBacked(in_base), m_deterministic(deterministic),
- cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
+ cacheCoins(0, SaltedCoinsCacheHasher{/*deterministic=*/deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
{
m_sentinel.second.SelfRef(m_sentinel);
}
@@ -331,7 +339,7 @@ void CCoinsViewCache::ReallocateCache()
cacheCoins.~CCoinsMap();
m_cache_coins_memory_resource.~CCoinsMapMemoryResource();
::new (&m_cache_coins_memory_resource) CCoinsMapMemoryResource{};
- ::new (&cacheCoins) CCoinsMap{0, SaltedOutpointHasher{/*deterministic=*/m_deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource};
+ ::new (&cacheCoins) CCoinsMap{0, SaltedCoinsCacheHasher{/*deterministic=*/m_deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource};
}
void CCoinsViewCache::SanityCheck() const
diff --git a/src/coins.h b/src/coins.h
index 7fc01af3..71dff015 100644
--- a/src/coins.h
+++ b/src/coins.h
@@ -9,6 +9,7 @@
#include <attributes.h>
#include <compressor.h>
#include <core_memusage.h>
+#include <crypto/siphash.h>
#include <memusage.h>
#include <primitives/transaction.h>
#include <primitives/transaction_identifier.h>
@@ -18,7 +19,6 @@
#include <util/check.h>
#include <util/log.h>
#include <util/overflow.h>
-#include <util/hasher.h>
#include <cassert>
#include <cstdint>
@@ -219,6 +219,37 @@ public:
}
};
+/**
+ * SipHash-1-3-UJ based hasher for the coins cache and related coins containers.
+ *
+ * Retained entries identify real transaction outputs, so their keys contain computed txids.
+ * Missing-input lookups may contain arbitrary claimed prevouts, but FetchCoin() immediately
+ * erases their temporary entries when the backend lookup fails, so non-hash keys cannot
+ * accumulate.
+ *
+ * The assumeutxo loader assumes snapshot txids are valid while loading and verifies the
+ * complete snapshot's content hash before activation.
+ *
+ * Hash values are process-local and must not be persisted, serialized, or compared across
+ * processes.
+ *
+ * Having the hash noexcept lets libstdc++ recalculate it during rehash instead of storing it in
+ * each node.
+ */
+class SaltedCoinsCacheHasher
+{
+ const SipHasher13UJ m_hasher;
+
+public:
+ SaltedCoinsCacheHasher(bool deterministic = false);
+
+ /** Hash an outpoint as its txid jumbo block followed by the zero-extended index as one normal block. */
+ size_t operator()(const COutPoint& id) const noexcept
+ {
+ return m_hasher.Hash(id.hash.ToUint256(), uint64_t{id.n});
+ }
+};
+
/**
* PoolAllocator's MAX_BLOCK_SIZE_BYTES parameter here uses sizeof the data, and adds the size
* of 4 pointers. We do not know the exact node size used in the std::unordered_node implementation
@@ -229,7 +260,7 @@ public:
*/
using CCoinsMap = std::unordered_map<COutPoint,
CCoinsCacheEntry,
- SaltedOutpointHasher,
+ SaltedCoinsCacheHasher,
std::equal_to<COutPoint>,
PoolAllocator<CoinsCachePair,
sizeof(CoinsCachePair) + sizeof(void*) * 4>>;
diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp
index 900d172d..10c29ef7 100644
--- a/src/test/fuzz/coins_view.cpp
+++ b/src/test/fuzz/coins_view.cpp
@@ -245,7 +245,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
sentinel.second.SelfRef(sentinel);
size_t dirty_count{0};
CCoinsMapMemoryResource resource;
- CCoinsMap coins_map{0, SaltedOutpointHasher{/*deterministic=*/true}, CCoinsMap::key_equal{}, &resource};
+ CCoinsMap coins_map{0, SaltedCoinsCacheHasher{/*deterministic=*/true}, CCoinsMap::key_equal{}, &resource};
LIMITED_WHILE (good_data && fuzzed_data_provider.ConsumeBool(), 10'000) {
CCoinsCacheEntry coins_cache_entry;
if (fuzzed_data_provider.ConsumeBool()) {
diff --git a/src/util/hasher.cpp b/src/util/hasher.cpp
index b12f7451..8e70b2a3 100644
--- a/src/util/hasher.cpp
+++ b/src/util/hasher.cpp
@@ -22,9 +22,9 @@ SaltedWtxidHasher::SaltedWtxidHasher() : m_hasher{
FastRandomContext().rand64()}
{}
-SaltedOutpointHasher::SaltedOutpointHasher(bool deterministic) : m_hasher{
- deterministic ? 0x8e819f2607a18de6 : FastRandomContext().rand64(),
- deterministic ? 0xf4020d2e3983b0eb : FastRandomContext().rand64()}
+SaltedOutpointHasher::SaltedOutpointHasher() : m_hasher{
+ FastRandomContext().rand64(),
+ FastRandomContext().rand64()}
{}
SaltedSipHasher::SaltedSipHasher() :
diff --git a/src/util/hasher.h b/src/util/hasher.h
index 7e74c676..b4488a93 100644
--- a/src/util/hasher.h
+++ b/src/util/hasher.h
@@ -58,7 +58,7 @@ class SaltedOutpointHasher
const PresaltedSipHasher m_hasher;
public:
- SaltedOutpointHasher(bool deterministic = false);
+ SaltedOutpointHasher();
/**
* Having the hash noexcept allows libstdc++'s unordered_map to recalculate
Why this scored 26/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.