Skip to content

Commit 32eb521

Browse files
committed
Merge bitcoin/bitcoin#35215: coins: use SipHash-1-3-UJ for CCoinsMap keys
3bfdcbd coins: reuse cache hasher for txid set (Lőrinc) 2beab94 coins: use SipHash-1-3-UJ for `CCoinsMap` (Lőrinc) 7ff55cc bench: add fixed-width SipHash benchmarks (Lőrinc) 3aea854 test: add SipHash-1-3-UJ coverage (Pieter Wuille) a0ccd4a crypto: add fixed-width SipHash-1-3-UJ (Pieter Wuille) c2d7931 crypto: add generic SipHash-1-3-UJ (Pieter Wuille) 25bfca0 refactor: simplify adding SipHash-1-3-UJ (Lőrinc) af50ba8 test: add shared SipHash vectors (Lőrinc) Pull request description: **Problem:** The in-memory UTXO cache hashes `COutPoint` keys containing a 32-byte txid and a 32-bit output index. SipHash-2-4 processes the txid as four independent 64-bit blocks, so its optimized 32-byte and 36-byte paths both take 14 SipRounds. This also matters for hash-prefix index work such as [#35531](bitcoin/bitcoin#35531): once a persisted key format chooses a hash function, changing it later requires reindexing. **Fix:** Add `SipHasher13UJ`, a custom block-oriented variant combining Pieter Wuille's jumbo-block suggestion with SipHash-1-3, the reduced-round variant discussed in the [SipHash analysis](https://eprint.iacr.org/2012/351.pdf). It provides inline `Hash` overloads for the fixed-width inputs used here. Use a dedicated `SaltedCoinsCacheHasher` for `CCoinsMap` and `CoinsViewOverlay`'s temporary earlier-txid set, while other outpoint tables remain on SipHash-2-4. The salted hash values vary between restarts and are never persisted or sent over the network. **Design:** `SipHasher13UJ` accepts normal 64-bit blocks and 256-bit jumbo blocks. For hash-table use, cryptographic hash outputs must make up all but a small bounded number of retained jumbo blocks. The construction mixes all four limbs around one SipRound, omits byte-oriented padding, and uses an `"unpadded"` finalizer distinct from standard SipHash-1-3. The fixed-width paths take four rounds for one `uint256` jumbo block and five when followed by one normal block. For outpoints, the 32-bit output index is zero-extended into a normal 64-bit block. Retained `CCoinsMap` entries identify real transaction outputs, so their keys contain computed txids. Missing-input validation may probe 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. Every entry in the temporary earlier-txid set is a computed transaction hash, and the set is bounded by the block's transaction count. This construction is limited to local hash tables and is not a general-purpose or protocol SipHash replacement. Pieter discussed the construction with [SipHash co-author Jean-Philippe Aumasson](bitcoin/bitcoin#35215 (comment)), whose preliminary analysis did not find an easier collision construction and supported SipHash-1-3 for this hash-table use. <img width="2100" height="860" alt="siphash_compare_updated" src="https://github.com/user-attachments/assets/cefec6f8-5ec0-450a-a0a2-f946de9ef36d" /> **Structure:** Shared vectors first cover the existing generic and fixed SipHash-2-4 paths in C++, the generic path in Python, and their randomized equivalence in the fuzzer. A behavior-neutral refactor then moves the round, compression, and finalization logic into inline `SipHashState` methods; assembly inspection shows that the fixed-width paths retain their instruction counts, while the generic byte loop retains its prior code generation through a local state copy. Three Pieter-authored commits add the generic UJ specification, fixed-width implementation, and shared correctness coverage. Benchmarks follow that coverage, then separate commits change `CCoinsMap`'s hasher and reuse it for the temporary earlier-txid set. **Tests:** The shared JSON supplies the same byte sequences to the generic C++ and Python SipHash-2-4 implementations, with applicable fixed-width paths checked against the same expected output. The SipHash-2-4 rows include the 64 official vectors for inputs from 0 to 63 bytes and cases that vary input chunking. The UJ outputs were generated by an independent implementation and are checked using normal blocks, equivalent zero-extended jumbo blocks, and applicable fixed-width `Hash` overloads. The integer fuzzer extends these comparisons to arbitrary values and mixed normal/jumbo block encodings. [Counting the dbcache buckets](https://gist.github.com/l0rinc/d68f56c3ed89f76f56da6632ef6f2d92) indicates the new outpoint hasher retains the uniform bucket distribution expected by `CCoinsMap`: <img width="1200" height="750" alt="ccoinsmap-collisions" src="https://github.com/user-attachments/assets/eeedec81-acdc-4adf-a9c8-bfce089700da" /> **Benchmarks:** Fixed-width microbenchmarks compare SipHash-2-4 with SipHash-1-3-UJ for 32-byte hashes and inputs containing a 32-byte hash plus a 32-bit index. Reported aarch64 measurements and an [independent x86_64 run](bitcoin/bitcoin#35215 (comment)) show the outpoint path is about 2x faster. <details><summary>Benchmark runner</summary> ```bash for COMPILER in gcc clang; do \ if [ "$COMPILER" = gcc ]; then CC=gcc; CXX=g++; else CC=clang; CXX=clang++; fi; \ cmake -B "build-bench-$COMPILER" -DCMAKE_BUILD_TYPE=Release -DBUILD_BENCH=ON -DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DENABLE_WALLET=OFF -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" >/dev/null 2>&1 && \ cmake --build "build-bench-$COMPILER" --target bench_bitcoin -j"$(nproc)" >/dev/null 2>&1 && \ echo "" && echo "$(date -I) | SipHash fixed-width microbench | $("$CXX" --version | head -1) | $(hostname) | $(uname -m) | $(lscpu | awk -F: '/Model name/{print $2; exit}' | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM" && \ "build-bench-$COMPILER/bin/bench_bitcoin" -filter='SipHash.*32b|SipHash.*36b' -min-time=10000; \ done ``` </details> A two-run GCC `-reindex-chainstate` comparison of the same `CCoinsMap` hot path through height 957,759 with `-dbcache=2000` on a Ryzen 7 3700X/SSD reduced mean wall time from 11,278 s to 10,759 s, a ~5% validation speedup. ACKs for top commit: achow101: light ACK 3bfdcbd sipa: ACK 3bfdcbd (to the extent the code/ideas aren't my own) andrewtoth: ACK 3bfdcbd optout21: ACK 3bfdcbd Tree-SHA512: c3c66051cb1ebdb0cddbc8b8bed2297c524842f92960531de23d9586c5bb950b302c33d06fc15c2320cbbf97273b22ec3f17fd0e7ddcc7df4a8132a47a606277
2 parents 883ef1d + 3bfdcbd commit 32eb521

16 files changed

Lines changed: 1168 additions & 162 deletions

File tree

src/bench/crypto_hash.cpp

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,10 @@ static void SHA512(benchmark::Bench& bench)
190190
});
191191
}
192192

193-
static void SipHash_32b(benchmark::Bench& bench)
193+
static void SipHash24_32b(benchmark::Bench& bench)
194194
{
195195
FastRandomContext rng{/*fDeterministic=*/true};
196-
PresaltedSipHasher presalted_sip_hasher(rng.rand64(), rng.rand64());
196+
PresaltedSipHasher presalted_sip_hasher{rng.rand64(), rng.rand64()};
197197
auto val{rng.rand256()};
198198
auto i{0U};
199199
bench.run([&] {
@@ -203,6 +203,49 @@ static void SipHash_32b(benchmark::Bench& bench)
203203
});
204204
}
205205

206+
static void SipHash24_36b(benchmark::Bench& bench)
207+
{
208+
FastRandomContext rng{/*fDeterministic=*/true};
209+
PresaltedSipHasher presalted_sip_hasher{rng.rand64(), rng.rand64()};
210+
auto val{rng.rand256()};
211+
uint32_t extra{rng.rand32()};
212+
auto i{0U};
213+
bench.run([&] {
214+
ankerl::nanobench::doNotOptimizeAway(presalted_sip_hasher(val, extra));
215+
++i;
216+
val.data()[i % uint256::size()] ^= i & 0xFF;
217+
extra += i;
218+
});
219+
}
220+
221+
static void SipHash13UJ_32b(benchmark::Bench& bench)
222+
{
223+
FastRandomContext rng{/*fDeterministic=*/true};
224+
SipHasher13UJ sip_hasher{rng.rand64(), rng.rand64()};
225+
auto val{rng.rand256()};
226+
auto i{0U};
227+
bench.run([&] {
228+
ankerl::nanobench::doNotOptimizeAway(sip_hasher.Hash(val));
229+
++i;
230+
val.data()[i % uint256::size()] ^= i & 0xFF;
231+
});
232+
}
233+
234+
static void SipHash13UJ_36b(benchmark::Bench& bench)
235+
{
236+
FastRandomContext rng{/*fDeterministic=*/true};
237+
SipHasher13UJ sip_hasher{rng.rand64(), rng.rand64()};
238+
auto val{rng.rand256()};
239+
uint32_t extra{rng.rand32()};
240+
auto i{0U};
241+
bench.run([&] {
242+
ankerl::nanobench::doNotOptimizeAway(sip_hasher.Hash(val, uint64_t{extra}));
243+
++i;
244+
val.data()[i % uint256::size()] ^= i & 0xFF;
245+
extra += i;
246+
});
247+
}
248+
206249
static void MuHash(benchmark::Bench& bench)
207250
{
208251
MuHash3072 acc;
@@ -273,7 +316,10 @@ BENCHMARK(SHA256_32b_STANDARD);
273316
BENCHMARK(SHA256_32b_SSE4);
274317
BENCHMARK(SHA256_32b_AVX2);
275318
BENCHMARK(SHA256_32b_SHANI);
276-
BENCHMARK(SipHash_32b);
319+
BENCHMARK(SipHash24_32b);
320+
BENCHMARK(SipHash24_36b);
321+
BENCHMARK(SipHash13UJ_32b);
322+
BENCHMARK(SipHash13UJ_36b);
277323
BENCHMARK(SHA256D64_1024_STANDARD);
278324
BENCHMARK(SHA256D64_1024_SSE4);
279325
BENCHMARK(SHA256D64_1024_AVX2);

src/coins.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ TRACEPOINT_SEMAPHORE(utxocache, add);
1919
TRACEPOINT_SEMAPHORE(utxocache, spent);
2020
TRACEPOINT_SEMAPHORE(utxocache, uncache);
2121

22+
SaltedCoinsCacheHasher::SaltedCoinsCacheHasher(bool deterministic)
23+
: m_hasher{
24+
deterministic ? 0x8e819f2607a18de6 : FastRandomContext().rand64(),
25+
deterministic ? 0xf4020d2e3983b0eb : FastRandomContext().rand64()}
26+
{
27+
}
28+
2229
CoinsViewEmpty& CoinsViewEmpty::Get()
2330
{
2431
static CoinsViewEmpty instance;
@@ -35,7 +42,7 @@ std::optional<Coin> CCoinsViewCache::PeekCoin(const COutPoint& outpoint) const
3542

3643
CCoinsViewCache::CCoinsViewCache(CCoinsView* in_base, bool deterministic) :
3744
CCoinsViewBacked(in_base), m_deterministic(deterministic),
38-
cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
45+
cacheCoins(0, SaltedCoinsCacheHasher{/*deterministic=*/deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
3946
{
4047
m_sentinel.second.SelfRef(m_sentinel);
4148
}
@@ -331,7 +338,7 @@ void CCoinsViewCache::ReallocateCache()
331338
cacheCoins.~CCoinsMap();
332339
m_cache_coins_memory_resource.~CCoinsMapMemoryResource();
333340
::new (&m_cache_coins_memory_resource) CCoinsMapMemoryResource{};
334-
::new (&cacheCoins) CCoinsMap{0, SaltedOutpointHasher{/*deterministic=*/m_deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource};
341+
::new (&cacheCoins) CCoinsMap{0, SaltedCoinsCacheHasher{/*deterministic=*/m_deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource};
335342
}
336343

337344
void CCoinsViewCache::SanityCheck() const
@@ -376,7 +383,7 @@ CCoinsViewCache::ResetGuard CoinsViewOverlay::StartFetching(const CBlock& block
376383
// Loop through the block inputs and set their prevouts in the queue.
377384
// Filter inputs that spend outputs created earlier in the same block. These outputs will be created
378385
// directly in the cache from the tx that creates them, so they will not be requested from a base view.
379-
std::unordered_set<Txid, SaltedTxidHasher> earlier_txids;
386+
std::unordered_set<Txid, SaltedCoinsCacheHasher> earlier_txids;
380387
earlier_txids.reserve(block.vtx.size());
381388
for (const auto& tx : block.vtx | std::views::drop(1)) {
382389
for (const auto& input : tx->vin) {

src/coins.h

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <attributes.h>
1010
#include <compressor.h>
1111
#include <core_memusage.h>
12+
#include <crypto/siphash.h>
1213
#include <memusage.h>
1314
#include <primitives/transaction.h>
1415
#include <primitives/transaction_identifier.h>
@@ -18,7 +19,6 @@
1819
#include <util/check.h>
1920
#include <util/log.h>
2021
#include <util/overflow.h>
21-
#include <util/hasher.h>
2222

2323
#include <cassert>
2424
#include <cstdint>
@@ -219,6 +219,43 @@ struct CCoinsCacheEntry
219219
}
220220
};
221221

222+
/**
223+
* SipHash-1-3-UJ based hasher for the coins cache and related coins containers.
224+
*
225+
* Retained entries identify real transaction outputs, so their keys contain computed txids.
226+
* Missing-input lookups may contain arbitrary claimed prevouts, but FetchCoin() immediately
227+
* erases their temporary entries when the backend lookup fails, so non-hash keys cannot
228+
* accumulate.
229+
*
230+
* The assumeutxo loader assumes snapshot txids are valid while loading and verifies the
231+
* complete snapshot's content hash before activation.
232+
*
233+
* Hash values are process-local and must not be persisted, serialized, or compared across
234+
* processes.
235+
*
236+
* Having the hash noexcept lets libstdc++ recalculate it during rehash instead of storing it in
237+
* each node.
238+
*/
239+
class SaltedCoinsCacheHasher
240+
{
241+
const SipHasher13UJ m_hasher;
242+
243+
public:
244+
SaltedCoinsCacheHasher(bool deterministic = false);
245+
246+
/** Hash a transaction ID, itself a cryptographic hash, as one jumbo block. */
247+
size_t operator()(const Txid& id) const noexcept
248+
{
249+
return m_hasher.Hash(id.ToUint256());
250+
}
251+
252+
/** Hash an outpoint as its txid jumbo block followed by the zero-extended index as one normal block. */
253+
size_t operator()(const COutPoint& id) const noexcept
254+
{
255+
return m_hasher.Hash(id.hash.ToUint256(), uint64_t{id.n});
256+
}
257+
};
258+
222259
/**
223260
* PoolAllocator's MAX_BLOCK_SIZE_BYTES parameter here uses sizeof the data, and adds the size
224261
* of 4 pointers. We do not know the exact node size used in the std::unordered_node implementation
@@ -229,7 +266,7 @@ struct CCoinsCacheEntry
229266
*/
230267
using CCoinsMap = std::unordered_map<COutPoint,
231268
CCoinsCacheEntry,
232-
SaltedOutpointHasher,
269+
SaltedCoinsCacheHasher,
233270
std::equal_to<COutPoint>,
234271
PoolAllocator<CoinsCachePair,
235272
sizeof(CoinsCachePair) + sizeof(void*) * 4>>;

src/crypto/siphash.cpp

Lines changed: 38 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -6,64 +6,36 @@
66

77
#include <uint256.h>
88

9-
#include <bit>
109
#include <cassert>
1110
#include <span>
1211

13-
#define SIPROUND do { \
14-
v0 += v1; v1 = std::rotl(v1, 13); v1 ^= v0; \
15-
v0 = std::rotl(v0, 32); \
16-
v2 += v3; v3 = std::rotl(v3, 16); v3 ^= v2; \
17-
v0 += v3; v3 = std::rotl(v3, 21); v3 ^= v0; \
18-
v2 += v1; v1 = std::rotl(v1, 17); v1 ^= v2; \
19-
v2 = std::rotl(v2, 32); \
20-
} while (0)
21-
2212
CSipHasher::CSipHasher(uint64_t k0, uint64_t k1) : m_state{k0, k1} {}
2313

2414
CSipHasher& CSipHasher::Write(uint64_t data)
2515
{
26-
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
27-
2816
assert(m_count % 8 == 0);
29-
30-
v3 ^= data;
31-
SIPROUND;
32-
SIPROUND;
33-
v0 ^= data;
34-
35-
m_state.v[0] = v0;
36-
m_state.v[1] = v1;
37-
m_state.v[2] = v2;
38-
m_state.v[3] = v3;
39-
17+
m_state.Compress2(data);
4018
m_count += 8;
4119
return *this;
4220
}
4321

4422
CSipHasher& CSipHasher::Write(std::span<const unsigned char> data)
4523
{
46-
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
47-
uint64_t t = m_tmp;
48-
uint8_t c = m_count;
24+
SipHashState state{m_state.Copy()};
25+
uint64_t t{m_tmp};
26+
uint8_t c{m_count};
4927

5028
while (data.size() > 0) {
5129
t |= uint64_t{data.front()} << (8 * (c % 8));
5230
c++;
5331
if ((c & 7) == 0) {
54-
v3 ^= t;
55-
SIPROUND;
56-
SIPROUND;
57-
v0 ^= t;
32+
state.Compress2(t);
5833
t = 0;
5934
}
6035
data = data.subspan(1);
6136
}
6237

63-
m_state.v[0] = v0;
64-
m_state.v[1] = v1;
65-
m_state.v[2] = v2;
66-
m_state.v[3] = v3;
38+
m_state = state;
6739
m_count = c;
6840
m_tmp = t;
6941

@@ -72,91 +44,46 @@ CSipHasher& CSipHasher::Write(std::span<const unsigned char> data)
7244

7345
uint64_t CSipHasher::Finalize() const
7446
{
75-
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
47+
return m_state.Copy()
48+
.Compress2(m_tmp | (uint64_t{m_count} << 56))
49+
.Finalize4();
50+
}
7651

77-
uint64_t t = m_tmp | (((uint64_t)m_count) << 56);
52+
SipHasher13UJ& SipHasher13UJ::Write(uint64_t data) noexcept
53+
{
54+
m_state.Compress1(data);
55+
return *this;
56+
}
7857

79-
v3 ^= t;
80-
SIPROUND;
81-
SIPROUND;
82-
v0 ^= t;
83-
v2 ^= 0xFF;
84-
SIPROUND;
85-
SIPROUND;
86-
SIPROUND;
87-
SIPROUND;
88-
return v0 ^ v1 ^ v2 ^ v3;
58+
SipHasher13UJ& SipHasher13UJ::WriteJumbo(const uint256& hash) noexcept
59+
{
60+
m_state.Compress1Jumbo(hash);
61+
return *this;
8962
}
9063

91-
uint64_t PresaltedSipHasher::operator()(const uint256& val) const noexcept
64+
uint64_t SipHasher13UJ::Finalize() const noexcept
9265
{
93-
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
94-
uint64_t d = val.GetUint64(0);
95-
v3 ^= d;
66+
return m_state.Copy().Finalize3U();
67+
}
9668

97-
SIPROUND;
98-
SIPROUND;
99-
v0 ^= d;
100-
d = val.GetUint64(1);
101-
v3 ^= d;
102-
SIPROUND;
103-
SIPROUND;
104-
v0 ^= d;
105-
d = val.GetUint64(2);
106-
v3 ^= d;
107-
SIPROUND;
108-
SIPROUND;
109-
v0 ^= d;
110-
d = val.GetUint64(3);
111-
v3 ^= d;
112-
SIPROUND;
113-
SIPROUND;
114-
v0 ^= d;
115-
v3 ^= (uint64_t{4}) << 59;
116-
SIPROUND;
117-
SIPROUND;
118-
v0 ^= (uint64_t{4}) << 59;
119-
v2 ^= 0xFF;
120-
SIPROUND;
121-
SIPROUND;
122-
SIPROUND;
123-
SIPROUND;
124-
return v0 ^ v1 ^ v2 ^ v3;
69+
uint64_t PresaltedSipHasher::operator()(const uint256& val) const noexcept
70+
{
71+
return m_state.Copy()
72+
.Compress2(val.GetUint64(0))
73+
.Compress2(val.GetUint64(1))
74+
.Compress2(val.GetUint64(2))
75+
.Compress2(val.GetUint64(3))
76+
.Compress2(uint64_t{32} << 56)
77+
.Finalize4();
12578
}
12679

127-
/** Specialized implementation for efficiency */
12880
uint64_t PresaltedSipHasher::operator()(const uint256& val, uint32_t extra) const noexcept
12981
{
130-
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
131-
uint64_t d = val.GetUint64(0);
132-
v3 ^= d;
133-
SIPROUND;
134-
SIPROUND;
135-
v0 ^= d;
136-
d = val.GetUint64(1);
137-
v3 ^= d;
138-
SIPROUND;
139-
SIPROUND;
140-
v0 ^= d;
141-
d = val.GetUint64(2);
142-
v3 ^= d;
143-
SIPROUND;
144-
SIPROUND;
145-
v0 ^= d;
146-
d = val.GetUint64(3);
147-
v3 ^= d;
148-
SIPROUND;
149-
SIPROUND;
150-
v0 ^= d;
151-
d = ((uint64_t{36}) << 56) | extra;
152-
v3 ^= d;
153-
SIPROUND;
154-
SIPROUND;
155-
v0 ^= d;
156-
v2 ^= 0xFF;
157-
SIPROUND;
158-
SIPROUND;
159-
SIPROUND;
160-
SIPROUND;
161-
return v0 ^ v1 ^ v2 ^ v3;
82+
return m_state.Copy()
83+
.Compress2(val.GetUint64(0))
84+
.Compress2(val.GetUint64(1))
85+
.Compress2(val.GetUint64(2))
86+
.Compress2(val.GetUint64(3))
87+
.Compress2((uint64_t{36} << 56) | extra)
88+
.Finalize4();
16289
}

0 commit comments

Comments
 (0)