Skip to content

Commit 0f6d8a3

Browse files
committed
Merge bitcoin/bitcoin#30442: precalculate SipHash constant salt XORs
6eb5ba5 refactor: extract shared `SipHash` state into `SipHashState` (Lőrinc) 118d22d optimization: cache `PresaltedSipHasher` in `CBlockHeaderAndShortTxIDs` (Lőrinc) 9ca52a4 optimization: migrate `SipHashUint256` to `PresaltedSipHasher` (Lőrinc) ec11b9f optimization: introduce `PresaltedSipHasher` for repeated hashing (Lőrinc) 2033054 refactor: extract `SipHash` C0-C3 constants to class scope (Lőrinc) 9f9eb7f test: rename k1/k2 to k0/k1 in `SipHash` consistency tests (Lőrinc) Pull request description: This change is part of [[IBD] - Tracking PR for speeding up Initial Block Download](bitcoin/bitcoin#32043) ### Summary The in-memory representation of the UTXO set uses (salted) [SipHash](https://github.com/bitcoin/bitcoin/blob/master/src/coins.h#L226) to avoid key collision attacks. Hashing `uint256` keys is performed frequently throughout the codebase. Previously, specialized optimizations existed as standalone functions (`SipHashUint256` and `SipHashUint256Extra`), but the constant salting operations (C0-C3 XOR with keys) were recomputed on every call. This PR introduces `PresaltedSipHasher`, a class that caches the initial SipHash state (v0-v3 after XORing constants with keys), eliminating redundant constant computations when hashing multiple values with the same keys. The optimization is applied uniformly across: - All `Salted*Hasher` classes (`SaltedUint256Hasher`, `SaltedTxidHasher`, `SaltedWtxidHasher`, `SaltedOutpointHasher`) - `CBlockHeaderAndShortTxIDs` for compact block short ID computation ### Details The change replaces the standalone `SipHashUint256` and `SipHashUint256Extra` functions with `PresaltedSipHasher` class methods that cache the constant-salted state. This is particularly beneficial for hash map operations where the same salt is used repeatedly (as suggested by Sipa in bitcoin/bitcoin#30442 (comment)). `CSipHasher` behavior remains unchanged; only the specialized `uint256` paths and callers now reuse the cached state instead of recomputing it. ### Measurements Benchmarks were run using local `SaltedOutpointHasherBench_*` microbenchmarks (not included in this PR) that exercise `SaltedOutpointHasher` in realistic `std::unordered_set` scenarios. <details> <summary>Benchmarks</summary> ```C++ diff --git a/src/bench/crypto_hash.cpp b/src/bench/crypto_hash.cpp --- a/src/bench/crypto_hash.cpp(revision 9b1a7c3) +++ b/src/bench/crypto_hash.cpp(revision e1b4f056b3097e7e34b0eda31f57826d81c9d810) @@ -2,7 +2,6 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. - #include <bench/bench.h> #include <crypto/muhash.h> #include <crypto/ripemd160.h> @@ -12,9 +11,11 @@ #include <crypto/sha512.h> #include <crypto/siphash.h> #include <random.h> -#include <span.h> #include <tinyformat.h> #include <uint256.h> +#include <primitives/transaction.h> +#include <util/hasher.h> +#include <unordered_set> #include <cstdint> #include <vector> @@ -205,6 +206,98 @@ }); } +static void SaltedOutpointHasherBench_hash(benchmark::Bench& bench) +{ + FastRandomContext rng{/*fDeterministic=*/true}; + constexpr size_t size{1000}; + + std::vector<COutPoint> outpoints(size); + for (auto& outpoint : outpoints) { + outpoint = {Txid::FromUint256(rng.rand256()), rng.rand32()}; + } + + const SaltedOutpointHasher hasher; + bench.batch(size).run([&] { + size_t result{0}; + for (const auto& outpoint : outpoints) { + result ^= hasher(outpoint); + } + ankerl::nanobench::doNotOptimizeAway(result); + }); +} + +static void SaltedOutpointHasherBench_match(benchmark::Bench& bench) +{ + FastRandomContext rng{/*fDeterministic=*/true}; + constexpr size_t size{1000}; + + std::unordered_set<COutPoint, SaltedOutpointHasher> values; + std::vector<COutPoint> value_vector; + values.reserve(size); + value_vector.reserve(size); + + for (size_t i{0}; i < size; ++i) { + COutPoint outpoint{Txid::FromUint256(rng.rand256()), rng.rand32()}; + values.emplace(outpoint); + value_vector.push_back(outpoint); + assert(values.contains(outpoint)); + } + + bench.batch(size).run([&] { + bool result{true}; + for (const auto& outpoint : value_vector) { + result ^= values.contains(outpoint); + } + ankerl::nanobench::doNotOptimizeAway(result); + }); +} + +static void SaltedOutpointHasherBench_mismatch(benchmark::Bench& bench) +{ + FastRandomContext rng{/*fDeterministic=*/true}; + constexpr size_t size{1000}; + + std::unordered_set<COutPoint, SaltedOutpointHasher> values; + std::vector<COutPoint> missing_value_vector; + values.reserve(size); + missing_value_vector.reserve(size); + + for (size_t i{0}; i < size; ++i) { + values.emplace(Txid::FromUint256(rng.rand256()), rng.rand32()); + COutPoint missing_outpoint{Txid::FromUint256(rng.rand256()), rng.rand32()}; + missing_value_vector.push_back(missing_outpoint); + assert(!values.contains(missing_outpoint)); + } + + bench.batch(size).run([&] { + bool result{false}; + for (const auto& outpoint : missing_value_vector) { + result ^= values.contains(outpoint); + } + ankerl::nanobench::doNotOptimizeAway(result); + }); +} + +static void SaltedOutpointHasherBench_create_set(benchmark::Bench& bench) +{ + FastRandomContext rng{/*fDeterministic=*/true}; + constexpr size_t size{1000}; + + std::vector<COutPoint> outpoints(size); + for (auto& outpoint : outpoints) { + outpoint = {Txid::FromUint256(rng.rand256()), rng.rand32()}; + } + + bench.batch(size).run([&] { + std::unordered_set<COutPoint, SaltedOutpointHasher> set; + set.reserve(size); + for (const auto& outpoint : outpoints) { + set.emplace(outpoint); + } + ankerl::nanobench::doNotOptimizeAway(set.size()); + }); +} + static void MuHash(benchmark::Bench& bench) { MuHash3072 acc; @@ -276,6 +369,10 @@ BENCHMARK(SHA256_32b_AVX2, benchmark::PriorityLevel::HIGH); BENCHMARK(SHA256_32b_SHANI, benchmark::PriorityLevel::HIGH); BENCHMARK(SipHash_32b, benchmark::PriorityLevel::HIGH); +BENCHMARK(SaltedOutpointHasherBench_hash, benchmark::PriorityLevel::HIGH); +BENCHMARK(SaltedOutpointHasherBench_match, benchmark::PriorityLevel::HIGH); +BENCHMARK(SaltedOutpointHasherBench_mismatch, benchmark::PriorityLevel::HIGH); +BENCHMARK(SaltedOutpointHasherBench_create_set, benchmark::PriorityLevel::HIGH); BENCHMARK(SHA256D64_1024_STANDARD, benchmark::PriorityLevel::HIGH); BENCHMARK(SHA256D64_1024_SSE4, benchmark::PriorityLevel::HIGH); BENCHMARK(SHA256D64_1024_AVX2, benchmark::PriorityLevel::HIGH); ``` </details> > cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release && cmake --build build -j$(nproc) && build/bin/bench_bitcoin -filter='SaltedOutpointHasherBench' -min-time=10000 > Before: | ns/op | op/s | err% | total | benchmark |--------------------:|--------------------:|--------:|----------:|:---------- | 58.60 | 17,065,922.04 | 0.3% | 11.02 | `SaltedOutpointHasherBench_create_set` | 11.97 | 83,576,684.83 | 0.1% | 11.01 | `SaltedOutpointHasherBench_hash` | 14.50 | 68,985,850.12 | 0.3% | 10.96 | `SaltedOutpointHasherBench_match` | 13.90 | 71,942,033.47 | 0.4% | 11.03 | `SaltedOutpointHasherBench_mismatch` > After: | ns/op | op/s | err% | total | benchmark |--------------------:|--------------------:|--------:|----------:|:---------- | 57.27 | 17,462,299.19 | 0.1% | 11.02 | `SaltedOutpointHasherBench_create_set` | 11.24 | 88,997,888.48 | 0.3% | 11.04 | `SaltedOutpointHasherBench_hash` | 13.91 | 71,902,014.20 | 0.2% | 11.01 | `SaltedOutpointHasherBench_match` | 13.29 | 75,230,390.31 | 0.1% | 11.00 | `SaltedOutpointHasherBench_mismatch` compared to master: ```python create_set - 17,462,299.19 / 17,065,922.04 - 2.3% faster hash - 88,997,888.48 / 83,576,684.83 - 6.4% faster match - 71,902,014.20 / 68,985,850.12 - 4.2% faster mismatch - 75,230,390.31 / 71,942,033.47 - 4.5% faster ``` > C++ compiler .......................... GNU 13.3.0 > Before: | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 136.76 | 7,312,133.16 | 0.0% | 1,086.67 | 491.12 | 2.213 | 119.54 | 1.1% | 11.01 | `SaltedOutpointHasherBench_create_set` | 23.82 | 41,978,882.62 | 0.0% | 252.01 | 85.57 | 2.945 | 4.00 | 0.0% | 11.00 | `SaltedOutpointHasherBench_hash` | 60.42 | 16,549,695.42 | 0.1% | 460.51 | 217.04 | 2.122 | 21.00 | 1.4% | 10.99 | `SaltedOutpointHasherBench_match` | 78.66 | 12,713,595.35 | 0.1% | 555.59 | 282.52 | 1.967 | 20.19 | 2.2% | 10.74 | `SaltedOutpointHasherBench_mismatch` > After: | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 135.38 | 7,386,349.49 | 0.0% | 1,078.19 | 486.16 | 2.218 | 119.56 | 1.1% | 11.00 | `SaltedOutpointHasherBench_create_set` | 23.67 | 42,254,558.08 | 0.0% | 247.01 | 85.01 | 2.906 | 4.00 | 0.0% | 11.00 | `SaltedOutpointHasherBench_hash` | 58.95 | 16,962,220.14 | 0.1% | 446.55 | 211.74 | 2.109 | 20.86 | 1.4% | 11.01 | `SaltedOutpointHasherBench_match` | 76.98 | 12,991,047.69 | 0.1% | 548.93 | 276.50 | 1.985 | 20.25 | 2.3% | 10.72 | `SaltedOutpointHasherBench_mismatch` ```python compared to master: create_set - 7,386,349.49 / 7,312,133.16 - 1.0% faster hash - 42,254,558.08 / 41,978,882.62 - 0.6% faster match - 16,962,220.14 / 16,549,695.42 - 2.4% faster mismatch - 12,991,047.69 / 12,713,595.35 - 2.1% faster ``` ACKs for top commit: achow101: ACK 6eb5ba5 vasild: ACK 6eb5ba5 sipa: ACK 6eb5ba5 Tree-SHA512: 9688b87e1d79f8af9efc18a8487922c5f1735487a9c5b78029dd46abc1d94f05d499cd1036bd615849aa7d6b17d11653c968086050dd7d04300403ebd0e81210
2 parents c2975f2 + 6eb5ba5 commit 0f6d8a3

File tree

9 files changed

+132
-122
lines changed

9 files changed

+132
-122
lines changed

src/bench/crypto_hash.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,11 @@ static void SHA512(benchmark::Bench& bench)
193193
static void SipHash_32b(benchmark::Bench& bench)
194194
{
195195
FastRandomContext rng{/*fDeterministic=*/true};
196-
auto k0{rng.rand64()}, k1{rng.rand64()};
196+
PresaltedSipHasher presalted_sip_hasher(rng.rand64(), rng.rand64());
197197
auto val{rng.rand256()};
198198
auto i{0U};
199199
bench.run([&] {
200-
ankerl::nanobench::doNotOptimizeAway(SipHashUint256(k0, k1, val));
201-
++k0;
202-
++k1;
200+
ankerl::nanobench::doNotOptimizeAway(presalted_sip_hasher(val));
203201
++i;
204202
val.data()[i % uint256::size()] ^= i & 0xFF;
205203
});

src/blockencodings.cpp

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,36 @@
1717

1818
#include <unordered_map>
1919

20-
CBlockHeaderAndShortTxIDs::CBlockHeaderAndShortTxIDs(const CBlock& block, const uint64_t nonce) :
21-
nonce(nonce),
22-
shorttxids(block.vtx.size() - 1), prefilledtxn(1), header(block) {
20+
CBlockHeaderAndShortTxIDs::CBlockHeaderAndShortTxIDs(const CBlock& block, uint64_t nonce)
21+
: nonce(nonce),
22+
shorttxids(block.vtx.size() - 1),
23+
prefilledtxn(1),
24+
header(block)
25+
{
2326
FillShortTxIDSelector();
24-
//TODO: Use our mempool prior to block acceptance to predictively fill more than just the coinbase
27+
// TODO: Use our mempool prior to block acceptance to predictively fill more than just the coinbase
2528
prefilledtxn[0] = {0, block.vtx[0]};
2629
for (size_t i = 1; i < block.vtx.size(); i++) {
2730
const CTransaction& tx = *block.vtx[i];
2831
shorttxids[i - 1] = GetShortID(tx.GetWitnessHash());
2932
}
3033
}
3134

32-
void CBlockHeaderAndShortTxIDs::FillShortTxIDSelector() const {
35+
void CBlockHeaderAndShortTxIDs::FillShortTxIDSelector() const
36+
{
3337
DataStream stream{};
3438
stream << header << nonce;
3539
CSHA256 hasher;
3640
hasher.Write((unsigned char*)&(*stream.begin()), stream.end() - stream.begin());
3741
uint256 shorttxidhash;
3842
hasher.Finalize(shorttxidhash.begin());
39-
shorttxidk0 = shorttxidhash.GetUint64(0);
40-
shorttxidk1 = shorttxidhash.GetUint64(1);
43+
m_hasher.emplace(shorttxidhash.GetUint64(0), shorttxidhash.GetUint64(1));
4144
}
4245

43-
uint64_t CBlockHeaderAndShortTxIDs::GetShortID(const Wtxid& wtxid) const {
46+
uint64_t CBlockHeaderAndShortTxIDs::GetShortID(const Wtxid& wtxid) const
47+
{
4448
static_assert(SHORTTXIDS_LENGTH == 6, "shorttxids calculation assumes 6-byte shorttxids");
45-
return SipHashUint256(shorttxidk0, shorttxidk1, wtxid.ToUint256()) & 0xffffffffffffL;
49+
return (*Assert(m_hasher))(wtxid.ToUint256()) & 0xffffffffffffL;
4650
}
4751

4852
/* Reconstructing a compact block is in the hot-path for block relay,

src/blockencodings.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#ifndef BITCOIN_BLOCKENCODINGS_H
66
#define BITCOIN_BLOCKENCODINGS_H
77

8+
#include <crypto/siphash.h>
89
#include <primitives/block.h>
910

1011
#include <functional>
@@ -87,8 +88,7 @@ typedef enum ReadStatus_t
8788
} ReadStatus;
8889

8990
class CBlockHeaderAndShortTxIDs {
90-
private:
91-
mutable uint64_t shorttxidk0, shorttxidk1;
91+
mutable std::optional<PresaltedSipHasher> m_hasher;
9292
uint64_t nonce;
9393

9494
void FillShortTxIDSelector() const;
@@ -112,7 +112,7 @@ class CBlockHeaderAndShortTxIDs {
112112
/**
113113
* @param[in] nonce This should be randomly generated, and is used for the siphash secret key
114114
*/
115-
CBlockHeaderAndShortTxIDs(const CBlock& block, const uint64_t nonce);
115+
CBlockHeaderAndShortTxIDs(const CBlock& block, uint64_t nonce);
116116

117117
uint64_t GetShortID(const Wtxid& wtxid) const;
118118

src/crypto/siphash.cpp

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,33 @@
1919
v2 = std::rotl(v2, 32); \
2020
} while (0)
2121

22-
CSipHasher::CSipHasher(uint64_t k0, uint64_t k1)
23-
{
24-
v[0] = 0x736f6d6570736575ULL ^ k0;
25-
v[1] = 0x646f72616e646f6dULL ^ k1;
26-
v[2] = 0x6c7967656e657261ULL ^ k0;
27-
v[3] = 0x7465646279746573ULL ^ k1;
28-
count = 0;
29-
tmp = 0;
30-
}
22+
CSipHasher::CSipHasher(uint64_t k0, uint64_t k1) : m_state{k0, k1} {}
3123

3224
CSipHasher& CSipHasher::Write(uint64_t data)
3325
{
34-
uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3];
26+
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
3527

36-
assert(count % 8 == 0);
28+
assert(m_count % 8 == 0);
3729

3830
v3 ^= data;
3931
SIPROUND;
4032
SIPROUND;
4133
v0 ^= data;
4234

43-
v[0] = v0;
44-
v[1] = v1;
45-
v[2] = v2;
46-
v[3] = v3;
35+
m_state.v[0] = v0;
36+
m_state.v[1] = v1;
37+
m_state.v[2] = v2;
38+
m_state.v[3] = v3;
4739

48-
count += 8;
40+
m_count += 8;
4941
return *this;
5042
}
5143

5244
CSipHasher& CSipHasher::Write(std::span<const unsigned char> data)
5345
{
54-
uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3];
55-
uint64_t t = tmp;
56-
uint8_t c = count;
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;
5749

5850
while (data.size() > 0) {
5951
t |= uint64_t{data.front()} << (8 * (c % 8));
@@ -68,21 +60,21 @@ CSipHasher& CSipHasher::Write(std::span<const unsigned char> data)
6860
data = data.subspan(1);
6961
}
7062

71-
v[0] = v0;
72-
v[1] = v1;
73-
v[2] = v2;
74-
v[3] = v3;
75-
count = c;
76-
tmp = t;
63+
m_state.v[0] = v0;
64+
m_state.v[1] = v1;
65+
m_state.v[2] = v2;
66+
m_state.v[3] = v3;
67+
m_count = c;
68+
m_tmp = t;
7769

7870
return *this;
7971
}
8072

8173
uint64_t CSipHasher::Finalize() const
8274
{
83-
uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3];
75+
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
8476

85-
uint64_t t = tmp | (((uint64_t)count) << 56);
77+
uint64_t t = m_tmp | (((uint64_t)m_count) << 56);
8678

8779
v3 ^= t;
8880
SIPROUND;
@@ -96,15 +88,11 @@ uint64_t CSipHasher::Finalize() const
9688
return v0 ^ v1 ^ v2 ^ v3;
9789
}
9890

99-
uint64_t SipHashUint256(uint64_t k0, uint64_t k1, const uint256& val)
91+
uint64_t PresaltedSipHasher::operator()(const uint256& val) const noexcept
10092
{
101-
/* Specialized implementation for efficiency */
93+
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
10294
uint64_t d = val.GetUint64(0);
103-
104-
uint64_t v0 = 0x736f6d6570736575ULL ^ k0;
105-
uint64_t v1 = 0x646f72616e646f6dULL ^ k1;
106-
uint64_t v2 = 0x6c7967656e657261ULL ^ k0;
107-
uint64_t v3 = 0x7465646279746573ULL ^ k1 ^ d;
95+
v3 ^= d;
10896

10997
SIPROUND;
11098
SIPROUND;
@@ -136,16 +124,12 @@ uint64_t SipHashUint256(uint64_t k0, uint64_t k1, const uint256& val)
136124
return v0 ^ v1 ^ v2 ^ v3;
137125
}
138126

139-
uint64_t SipHashUint256Extra(uint64_t k0, uint64_t k1, const uint256& val, uint32_t extra)
127+
/** Specialized implementation for efficiency */
128+
uint64_t PresaltedSipHasher::operator()(const uint256& val, uint32_t extra) const noexcept
140129
{
141-
/* Specialized implementation for efficiency */
130+
uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3];
142131
uint64_t d = val.GetUint64(0);
143-
144-
uint64_t v0 = 0x736f6d6570736575ULL ^ k0;
145-
uint64_t v1 = 0x646f72616e646f6dULL ^ k1;
146-
uint64_t v2 = 0x6c7967656e657261ULL ^ k0;
147-
uint64_t v3 = 0x7465646279746573ULL ^ k1 ^ d;
148-
132+
v3 ^= d;
149133
SIPROUND;
150134
SIPROUND;
151135
v0 ^= d;

src/crypto/siphash.h

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,34 @@
55
#ifndef BITCOIN_CRYPTO_SIPHASH_H
66
#define BITCOIN_CRYPTO_SIPHASH_H
77

8+
#include <array>
89
#include <cstdint>
910
#include <span>
1011

1112
class uint256;
1213

13-
/** SipHash-2-4 */
14+
/** Shared SipHash internal state v[0..3], initialized from (k0, k1). */
15+
class SipHashState
16+
{
17+
static constexpr uint64_t C0{0x736f6d6570736575ULL}, C1{0x646f72616e646f6dULL}, C2{0x6c7967656e657261ULL}, C3{0x7465646279746573ULL};
18+
19+
public:
20+
explicit SipHashState(uint64_t k0, uint64_t k1) noexcept : v{C0 ^ k0, C1 ^ k1, C2 ^ k0, C3 ^ k1} {}
21+
22+
std::array<uint64_t, 4> v{};
23+
};
24+
25+
/** General SipHash-2-4 implementation. */
1426
class CSipHasher
1527
{
16-
private:
17-
uint64_t v[4];
18-
uint64_t tmp;
19-
uint8_t count; // Only the low 8 bits of the input size matter.
28+
SipHashState m_state;
29+
uint64_t m_tmp{0};
30+
uint8_t m_count{0}; //!< Only the low 8 bits of the input size matter.
2031

2132
public:
22-
/** Construct a SipHash calculator initialized with 128-bit key (k0, k1) */
33+
/** Construct a SipHash calculator initialized with 128-bit key (k0, k1). */
2334
CSipHasher(uint64_t k0, uint64_t k1);
24-
/** Hash a 64-bit integer worth of data
35+
/** Hash a 64-bit integer worth of data.
2536
* It is treated as if this was the little-endian interpretation of 8 bytes.
2637
* This function can only be used when a multiple of 8 bytes have been written so far.
2738
*/
@@ -32,17 +43,30 @@ class CSipHasher
3243
uint64_t Finalize() const;
3344
};
3445

35-
/** Optimized SipHash-2-4 implementation for uint256.
46+
/**
47+
* Optimized SipHash-2-4 implementation for uint256.
3648
*
37-
* It is identical to:
38-
* SipHasher(k0, k1)
39-
* .Write(val.GetUint64(0))
40-
* .Write(val.GetUint64(1))
41-
* .Write(val.GetUint64(2))
42-
* .Write(val.GetUint64(3))
43-
* .Finalize()
49+
* This class caches the initial SipHash v[0..3] state derived from (k0, k1)
50+
* and implements a specialized hashing path for uint256 values, with or
51+
* without an extra 32-bit word. The internal state is immutable, so
52+
* PresaltedSipHasher instances can be reused for multiple hashes with the
53+
* same key.
4454
*/
45-
uint64_t SipHashUint256(uint64_t k0, uint64_t k1, const uint256& val);
46-
uint64_t SipHashUint256Extra(uint64_t k0, uint64_t k1, const uint256& val, uint32_t extra);
55+
class PresaltedSipHasher
56+
{
57+
const SipHashState m_state;
58+
59+
public:
60+
explicit PresaltedSipHasher(uint64_t k0, uint64_t k1) noexcept : m_state{k0, k1} {}
61+
62+
/** Equivalent to CSipHasher(k0, k1).Write(val).Finalize(). */
63+
uint64_t operator()(const uint256& val) const noexcept;
64+
65+
/**
66+
* Equivalent to CSipHasher(k0, k1).Write(val).Write(extra).Finalize(),
67+
* with `extra` encoded as 4 little-endian bytes.
68+
*/
69+
uint64_t operator()(const uint256& val, uint32_t extra) const noexcept;
70+
};
4771

4872
#endif // BITCOIN_CRYPTO_SIPHASH_H

src/test/fuzz/integer.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ FUZZ_TARGET(integer, .init = initialize_integer)
118118
}
119119
(void)MillisToTimeval(i64);
120120
(void)SighashToStr(uch);
121-
(void)SipHashUint256(u64, u64, u256);
122-
(void)SipHashUint256Extra(u64, u64, u256, u32);
121+
(void)PresaltedSipHasher(u64, u64)(u256);
122+
(void)PresaltedSipHasher(u64, u64)(u256, u32);
123123
(void)ToLower(ch);
124124
(void)ToUpper(ch);
125125
{

src/test/hash_tests.cpp

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ BOOST_AUTO_TEST_CASE(siphash)
104104
hasher.Write(0x2F2E2D2C2B2A2928ULL);
105105
BOOST_CHECK_EQUAL(hasher.Finalize(), 0xe612a3cb9ecba951ull);
106106

107-
BOOST_CHECK_EQUAL(SipHashUint256(0x0706050403020100ULL, 0x0F0E0D0C0B0A0908ULL, uint256{"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"}), 0x7127512f72f27cceull);
107+
BOOST_CHECK_EQUAL(PresaltedSipHasher(0x0706050403020100ULL, 0x0F0E0D0C0B0A0908ULL)(uint256{"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"}), 0x7127512f72f27cceull);
108108

109109
// Check test vectors from spec, one byte at a time
110110
CSipHasher hasher2(0x0706050403020100ULL, 0x0F0E0D0C0B0A0908ULL);
@@ -128,23 +128,25 @@ BOOST_AUTO_TEST_CASE(siphash)
128128
// and the test would be affected by default tx version bumps if not fixed.
129129
tx.version = 1;
130130
ss << TX_WITH_WITNESS(tx);
131-
BOOST_CHECK_EQUAL(SipHashUint256(1, 2, ss.GetHash()), 0x79751e980c2a0a35ULL);
131+
BOOST_CHECK_EQUAL(PresaltedSipHasher(1, 2)(ss.GetHash()), 0x79751e980c2a0a35ULL);
132132

133-
// Check consistency between CSipHasher and SipHashUint256[Extra].
133+
// Check consistency between CSipHasher and PresaltedSipHasher.
134134
FastRandomContext ctx;
135135
for (int i = 0; i < 16; ++i) {
136+
uint64_t k0 = ctx.rand64();
136137
uint64_t k1 = ctx.rand64();
137-
uint64_t k2 = ctx.rand64();
138138
uint256 x = m_rng.rand256();
139+
140+
CSipHasher sip256(k0, k1);
141+
sip256.Write(x);
142+
BOOST_CHECK_EQUAL(PresaltedSipHasher(k0, k1)(x), sip256.Finalize());
143+
144+
CSipHasher sip288 = sip256;
139145
uint32_t n = ctx.rand32();
140146
uint8_t nb[4];
141147
WriteLE32(nb, n);
142-
CSipHasher sip256(k1, k2);
143-
sip256.Write(x);
144-
CSipHasher sip288 = sip256;
145148
sip288.Write(nb);
146-
BOOST_CHECK_EQUAL(SipHashUint256(k1, k2, x), sip256.Finalize());
147-
BOOST_CHECK_EQUAL(SipHashUint256Extra(k1, k2, x, n), sip288.Finalize());
149+
BOOST_CHECK_EQUAL(PresaltedSipHasher(k0, k1)(x, n), sip288.Finalize());
148150
}
149151
}
150152

src/util/hasher.cpp

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,30 @@
77
#include <span.h>
88
#include <util/hasher.h>
99

10-
SaltedUint256Hasher::SaltedUint256Hasher() :
11-
k0{FastRandomContext().rand64()},
12-
k1{FastRandomContext().rand64()} {}
10+
SaltedUint256Hasher::SaltedUint256Hasher() : m_hasher{
11+
FastRandomContext().rand64(),
12+
FastRandomContext().rand64()}
13+
{}
1314

14-
SaltedTxidHasher::SaltedTxidHasher() :
15-
k0{FastRandomContext().rand64()},
16-
k1{FastRandomContext().rand64()} {}
15+
SaltedTxidHasher::SaltedTxidHasher() : m_hasher{
16+
FastRandomContext().rand64(),
17+
FastRandomContext().rand64()}
18+
{}
1719

18-
SaltedWtxidHasher::SaltedWtxidHasher() :
19-
k0{FastRandomContext().rand64()},
20-
k1{FastRandomContext().rand64()} {}
20+
SaltedWtxidHasher::SaltedWtxidHasher() : m_hasher{
21+
FastRandomContext().rand64(),
22+
FastRandomContext().rand64()}
23+
{}
2124

22-
SaltedOutpointHasher::SaltedOutpointHasher(bool deterministic) :
23-
k0{deterministic ? 0x8e819f2607a18de6 : FastRandomContext().rand64()},
24-
k1{deterministic ? 0xf4020d2e3983b0eb : FastRandomContext().rand64()}
25+
SaltedOutpointHasher::SaltedOutpointHasher(bool deterministic) : m_hasher{
26+
deterministic ? 0x8e819f2607a18de6 : FastRandomContext().rand64(),
27+
deterministic ? 0xf4020d2e3983b0eb : FastRandomContext().rand64()}
2528
{}
2629

2730
SaltedSipHasher::SaltedSipHasher() :
2831
m_k0{FastRandomContext().rand64()},
29-
m_k1{FastRandomContext().rand64()} {}
32+
m_k1{FastRandomContext().rand64()}
33+
{}
3034

3135
size_t SaltedSipHasher::operator()(const std::span<const unsigned char>& script) const
3236
{

0 commit comments

Comments
 (0)