From 9f9eb7fbc053b38daeb1f4ca4e284d51e07fe50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 1 Feb 2025 19:01:36 +0100 Subject: [PATCH 1/6] test: rename k1/k2 to k0/k1 in `SipHash` consistency tests Aligns test variable naming with the `k0`/`k1` convention used consistently throughout the codebase for `SipHash` keys. Also splits the single-param `SipHash` test from the one with extra, for clarity. --- src/test/hash_tests.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/test/hash_tests.cpp b/src/test/hash_tests.cpp index 2fe44960f46..46901893e9b 100644 --- a/src/test/hash_tests.cpp +++ b/src/test/hash_tests.cpp @@ -133,18 +133,20 @@ BOOST_AUTO_TEST_CASE(siphash) // Check consistency between CSipHasher and SipHashUint256[Extra]. FastRandomContext ctx; for (int i = 0; i < 16; ++i) { + uint64_t k0 = ctx.rand64(); uint64_t k1 = ctx.rand64(); - uint64_t k2 = ctx.rand64(); uint256 x = m_rng.rand256(); + + CSipHasher sip256(k0, k1); + sip256.Write(x); + BOOST_CHECK_EQUAL(SipHashUint256(k0, k1, x), sip256.Finalize()); // TODO modified in follow-up commit + + CSipHasher sip288 = sip256; uint32_t n = ctx.rand32(); uint8_t nb[4]; WriteLE32(nb, n); - CSipHasher sip256(k1, k2); - sip256.Write(x); - CSipHasher sip288 = sip256; sip288.Write(nb); - BOOST_CHECK_EQUAL(SipHashUint256(k1, k2, x), sip256.Finalize()); - BOOST_CHECK_EQUAL(SipHashUint256Extra(k1, k2, x, n), sip288.Finalize()); + BOOST_CHECK_EQUAL(SipHashUint256Extra(k0, k1, x, n), sip288.Finalize()); // TODO modified in follow-up commit } } From 20330548cf5f44cec057c0ed099b64c81afb124d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 1 Feb 2025 19:16:17 +0100 Subject: [PATCH 2/6] refactor: extract `SipHash` C0-C3 constants to class scope Moves the `SipHash` initialization constants (C0-C3) from magic numbers to named static constexpr members of `CSipHasher`. --- src/crypto/siphash.cpp | 26 ++++++++++++++------------ src/crypto/siphash.h | 5 +++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/crypto/siphash.cpp b/src/crypto/siphash.cpp index 1a9eb771a82..37ed918908f 100644 --- a/src/crypto/siphash.cpp +++ b/src/crypto/siphash.cpp @@ -21,10 +21,10 @@ CSipHasher::CSipHasher(uint64_t k0, uint64_t k1) { - v[0] = 0x736f6d6570736575ULL ^ k0; - v[1] = 0x646f72616e646f6dULL ^ k1; - v[2] = 0x6c7967656e657261ULL ^ k0; - v[3] = 0x7465646279746573ULL ^ k1; + v[0] = C0 ^ k0; + v[1] = C1 ^ k1; + v[2] = C2 ^ k0; + v[3] = C3 ^ k1; count = 0; tmp = 0; } @@ -101,10 +101,11 @@ uint64_t SipHashUint256(uint64_t k0, uint64_t k1, const uint256& val) /* Specialized implementation for efficiency */ uint64_t d = val.GetUint64(0); - uint64_t v0 = 0x736f6d6570736575ULL ^ k0; - uint64_t v1 = 0x646f72616e646f6dULL ^ k1; - uint64_t v2 = 0x6c7967656e657261ULL ^ k0; - uint64_t v3 = 0x7465646279746573ULL ^ k1 ^ d; + // TODO moved in followup commit + uint64_t v0 = CSipHasher::C0 ^ k0; + uint64_t v1 = CSipHasher::C1 ^ k1; + uint64_t v2 = CSipHasher::C2 ^ k0; + uint64_t v3 = CSipHasher::C3 ^ k1 ^ d; SIPROUND; SIPROUND; @@ -141,10 +142,11 @@ uint64_t SipHashUint256Extra(uint64_t k0, uint64_t k1, const uint256& val, uint3 /* Specialized implementation for efficiency */ uint64_t d = val.GetUint64(0); - uint64_t v0 = 0x736f6d6570736575ULL ^ k0; - uint64_t v1 = 0x646f72616e646f6dULL ^ k1; - uint64_t v2 = 0x6c7967656e657261ULL ^ k0; - uint64_t v3 = 0x7465646279746573ULL ^ k1 ^ d; + // TODO moved in followup commit + uint64_t v0 = CSipHasher::C0 ^ k0; + uint64_t v1 = CSipHasher::C1 ^ k1; + uint64_t v2 = CSipHasher::C2 ^ k0; + uint64_t v3 = CSipHasher::C3 ^ k1 ^ d; SIPROUND; SIPROUND; diff --git a/src/crypto/siphash.h b/src/crypto/siphash.h index 8d41a083ac8..4293fdf93b4 100644 --- a/src/crypto/siphash.h +++ b/src/crypto/siphash.h @@ -19,6 +19,11 @@ private: uint8_t count; // Only the low 8 bits of the input size matter. public: + static constexpr uint64_t C0{0x736f6d6570736575ULL}; + static constexpr uint64_t C1{0x646f72616e646f6dULL}; + static constexpr uint64_t C2{0x6c7967656e657261ULL}; + static constexpr uint64_t C3{0x7465646279746573ULL}; + /** Construct a SipHash calculator initialized with 128-bit key (k0, k1) */ CSipHasher(uint64_t k0, uint64_t k1); /** Hash a 64-bit integer worth of data From ec11b9fede2af826abb144a443115fb586c49597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sat, 1 Feb 2025 19:21:34 +0100 Subject: [PATCH 3/6] optimization: introduce `PresaltedSipHasher` for repeated hashing Replaces the `SipHashUint256Extra` function with the `PresaltedSipHasher` class that caches the constant-salted state (v[0-3] after XORing with keys). This avoids redundant XOR operations when hashing multiple values with the same keys, benefiting use cases like `SaltedOutpointHasher`. This essentially brings the precalculations in the `CSipHasher` constructor to the `uint256`-specialized SipHash implementation. > cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release && cmake --build build -j$(nproc) && build/src/bench/bench_bitcoin -filter='SaltedOutpointHasherBench.*' -min-time=10000 > C++ compiler .......................... AppleClang 16.0.0.16000026 | 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: 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 | 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` compared to master: create_set - 7,386,349.49/7,312,133.16 - 1% 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% faster Co-authored-by: sipa --- src/crypto/siphash.cpp | 13 ++++--------- src/crypto/siphash.h | 16 +++++++++++++++- src/test/fuzz/integer.cpp | 2 +- src/test/hash_tests.cpp | 4 ++-- src/util/hasher.cpp | 9 +++++---- src/util/hasher.h | 9 ++++----- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/crypto/siphash.cpp b/src/crypto/siphash.cpp index 37ed918908f..a60297162a8 100644 --- a/src/crypto/siphash.cpp +++ b/src/crypto/siphash.cpp @@ -137,17 +137,12 @@ uint64_t SipHashUint256(uint64_t k0, uint64_t k1, const uint256& val) return v0 ^ v1 ^ v2 ^ v3; } -uint64_t SipHashUint256Extra(uint64_t k0, uint64_t k1, const uint256& val, uint32_t extra) +/** Specialized implementation for efficiency */ +uint64_t PresaltedSipHasher::operator()(const uint256& val, uint32_t extra) const noexcept { - /* Specialized implementation for efficiency */ + uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3]; uint64_t d = val.GetUint64(0); - - // TODO moved in followup commit - uint64_t v0 = CSipHasher::C0 ^ k0; - uint64_t v1 = CSipHasher::C1 ^ k1; - uint64_t v2 = CSipHasher::C2 ^ k0; - uint64_t v3 = CSipHasher::C3 ^ k1 ^ d; - + v3 ^= d; SIPROUND; SIPROUND; v0 ^= d; diff --git a/src/crypto/siphash.h b/src/crypto/siphash.h index 4293fdf93b4..a4aeb3ae479 100644 --- a/src/crypto/siphash.h +++ b/src/crypto/siphash.h @@ -48,6 +48,20 @@ public: * .Finalize() */ uint64_t SipHashUint256(uint64_t k0, uint64_t k1, const uint256& val); -uint64_t SipHashUint256Extra(uint64_t k0, uint64_t k1, const uint256& val, uint32_t extra); + +class PresaltedSipHasher +{ + uint64_t v[4]; + +public: + explicit PresaltedSipHasher(uint64_t k0, uint64_t k1) noexcept { + v[0] = CSipHasher::C0 ^ k0; + v[1] = CSipHasher::C1 ^ k1; + v[2] = CSipHasher::C2 ^ k0; + v[3] = CSipHasher::C3 ^ k1; + } + + uint64_t operator()(const uint256& val, uint32_t extra) const noexcept; +}; #endif // BITCOIN_CRYPTO_SIPHASH_H diff --git a/src/test/fuzz/integer.cpp b/src/test/fuzz/integer.cpp index 2e13d4226e1..81516fd9f87 100644 --- a/src/test/fuzz/integer.cpp +++ b/src/test/fuzz/integer.cpp @@ -119,7 +119,7 @@ FUZZ_TARGET(integer, .init = initialize_integer) (void)MillisToTimeval(i64); (void)SighashToStr(uch); (void)SipHashUint256(u64, u64, u256); - (void)SipHashUint256Extra(u64, u64, u256, u32); + (void)PresaltedSipHasher(u64, u64)(u256, u32); (void)ToLower(ch); (void)ToUpper(ch); { diff --git a/src/test/hash_tests.cpp b/src/test/hash_tests.cpp index 46901893e9b..d4592df7eeb 100644 --- a/src/test/hash_tests.cpp +++ b/src/test/hash_tests.cpp @@ -130,7 +130,7 @@ BOOST_AUTO_TEST_CASE(siphash) ss << TX_WITH_WITNESS(tx); BOOST_CHECK_EQUAL(SipHashUint256(1, 2, ss.GetHash()), 0x79751e980c2a0a35ULL); - // Check consistency between CSipHasher and SipHashUint256[Extra]. + // Check consistency between CSipHasher and SipHashUint256 and PresaltedSipHasher. FastRandomContext ctx; for (int i = 0; i < 16; ++i) { uint64_t k0 = ctx.rand64(); @@ -146,7 +146,7 @@ BOOST_AUTO_TEST_CASE(siphash) uint8_t nb[4]; WriteLE32(nb, n); sip288.Write(nb); - BOOST_CHECK_EQUAL(SipHashUint256Extra(k0, k1, x, n), sip288.Finalize()); // TODO modified in follow-up commit + BOOST_CHECK_EQUAL(PresaltedSipHasher(k0, k1)(x, n), sip288.Finalize()); } } diff --git a/src/util/hasher.cpp b/src/util/hasher.cpp index c4051ae0013..cac602a893f 100644 --- a/src/util/hasher.cpp +++ b/src/util/hasher.cpp @@ -19,14 +19,15 @@ SaltedWtxidHasher::SaltedWtxidHasher() : k0{FastRandomContext().rand64()}, k1{FastRandomContext().rand64()} {} -SaltedOutpointHasher::SaltedOutpointHasher(bool deterministic) : - k0{deterministic ? 0x8e819f2607a18de6 : FastRandomContext().rand64()}, - k1{deterministic ? 0xf4020d2e3983b0eb : FastRandomContext().rand64()} +SaltedOutpointHasher::SaltedOutpointHasher(bool deterministic) : m_hasher{ + deterministic ? 0x8e819f2607a18de6 : FastRandomContext().rand64(), + deterministic ? 0xf4020d2e3983b0eb : FastRandomContext().rand64()} {} SaltedSipHasher::SaltedSipHasher() : m_k0{FastRandomContext().rand64()}, - m_k1{FastRandomContext().rand64()} {} + m_k1{FastRandomContext().rand64()} +{} size_t SaltedSipHasher::operator()(const std::span& script) const { diff --git a/src/util/hasher.h b/src/util/hasher.h index be5d9eb13df..e7e69530cc5 100644 --- a/src/util/hasher.h +++ b/src/util/hasher.h @@ -60,9 +60,7 @@ public: class SaltedOutpointHasher { -private: - /** Salt */ - const uint64_t k0, k1; + const PresaltedSipHasher m_hasher; public: SaltedOutpointHasher(bool deterministic = false); @@ -76,8 +74,9 @@ public: * * @see https://gcc.gnu.org/onlinedocs/gcc-13.2.0/libstdc++/manual/manual/unordered_associative.html */ - size_t operator()(const COutPoint& id) const noexcept { - return SipHashUint256Extra(k0, k1, id.hash.ToUint256(), id.n); + size_t operator()(const COutPoint& id) const noexcept + { + return m_hasher(id.hash.ToUint256(), id.n); } }; From 9ca52a4cbece2963363d201ef2b15d58d96ea221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Tue, 30 Sep 2025 20:58:19 -0400 Subject: [PATCH 4/6] optimization: migrate `SipHashUint256` to `PresaltedSipHasher` Replaces standalone `SipHashUint256` with an `operator()` overload in `PresaltedSipHasher`. Updates all hasher classes (`SaltedUint256Hasher`, `SaltedTxidHasher`, `SaltedWtxidHasher`) to use `PresaltedSipHasher` internally, enabling the same constant-state caching optimization while keeping behavior unchanged. Benchmark was also adjusted to cache the salting part. --- src/bench/crypto_hash.cpp | 6 ++---- src/blockencodings.cpp | 3 ++- src/crypto/siphash.cpp | 11 +++-------- src/crypto/siphash.h | 18 ++++++++---------- src/test/fuzz/integer.cpp | 2 +- src/test/hash_tests.cpp | 8 ++++---- src/util/hasher.cpp | 21 ++++++++++++--------- src/util/hasher.h | 31 +++++++++++++------------------ 8 files changed, 45 insertions(+), 55 deletions(-) diff --git a/src/bench/crypto_hash.cpp b/src/bench/crypto_hash.cpp index 2f1ff564388..05c7788d74b 100644 --- a/src/bench/crypto_hash.cpp +++ b/src/bench/crypto_hash.cpp @@ -193,13 +193,11 @@ static void SHA512(benchmark::Bench& bench) static void SipHash_32b(benchmark::Bench& bench) { FastRandomContext rng{/*fDeterministic=*/true}; - auto k0{rng.rand64()}, k1{rng.rand64()}; + PresaltedSipHasher presalted_sip_hasher(rng.rand64(), rng.rand64()); auto val{rng.rand256()}; auto i{0U}; bench.run([&] { - ankerl::nanobench::doNotOptimizeAway(SipHashUint256(k0, k1, val)); - ++k0; - ++k1; + ankerl::nanobench::doNotOptimizeAway(presalted_sip_hasher(val)); ++i; val.data()[i % uint256::size()] ^= i & 0xFF; }); diff --git a/src/blockencodings.cpp b/src/blockencodings.cpp index cf6da55aa9a..55c8f8a056f 100644 --- a/src/blockencodings.cpp +++ b/src/blockencodings.cpp @@ -42,7 +42,8 @@ void CBlockHeaderAndShortTxIDs::FillShortTxIDSelector() const { uint64_t CBlockHeaderAndShortTxIDs::GetShortID(const Wtxid& wtxid) const { static_assert(SHORTTXIDS_LENGTH == 6, "shorttxids calculation assumes 6-byte shorttxids"); - return SipHashUint256(shorttxidk0, shorttxidk1, wtxid.ToUint256()) & 0xffffffffffffL; + PresaltedSipHasher hasher(shorttxidk0, shorttxidk1); // TODO extract + return hasher(wtxid.ToUint256()) & 0xffffffffffffL; } /* Reconstructing a compact block is in the hot-path for block relay, diff --git a/src/crypto/siphash.cpp b/src/crypto/siphash.cpp index a60297162a8..49cc234993e 100644 --- a/src/crypto/siphash.cpp +++ b/src/crypto/siphash.cpp @@ -96,16 +96,11 @@ uint64_t CSipHasher::Finalize() const return v0 ^ v1 ^ v2 ^ v3; } -uint64_t SipHashUint256(uint64_t k0, uint64_t k1, const uint256& val) +uint64_t PresaltedSipHasher::operator()(const uint256& val) const noexcept { - /* Specialized implementation for efficiency */ + uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3]; uint64_t d = val.GetUint64(0); - - // TODO moved in followup commit - uint64_t v0 = CSipHasher::C0 ^ k0; - uint64_t v1 = CSipHasher::C1 ^ k1; - uint64_t v2 = CSipHasher::C2 ^ k0; - uint64_t v3 = CSipHasher::C3 ^ k1 ^ d; + v3 ^= d; SIPROUND; SIPROUND; diff --git a/src/crypto/siphash.h b/src/crypto/siphash.h index a4aeb3ae479..b81b9557dec 100644 --- a/src/crypto/siphash.h +++ b/src/crypto/siphash.h @@ -37,18 +37,15 @@ public: uint64_t Finalize() const; }; -/** Optimized SipHash-2-4 implementation for uint256. +/** + * Optimized SipHash-2-4 implementation for uint256. * - * It is identical to: - * SipHasher(k0, k1) - * .Write(val.GetUint64(0)) - * .Write(val.GetUint64(1)) - * .Write(val.GetUint64(2)) - * .Write(val.GetUint64(3)) - * .Finalize() + * This class caches the initial SipHash v[0..3] state derived from (k0, k1) + * and implements a specialized hashing path for uint256 values, with or + * without an extra 32-bit word. The internal state is immutable, so + * PresaltedSipHasher instances can be reused for multiple hashes with the + * same key. */ -uint64_t SipHashUint256(uint64_t k0, uint64_t k1, const uint256& val); - class PresaltedSipHasher { uint64_t v[4]; @@ -61,6 +58,7 @@ public: v[3] = CSipHasher::C3 ^ k1; } + uint64_t operator()(const uint256& val) const noexcept; uint64_t operator()(const uint256& val, uint32_t extra) const noexcept; }; diff --git a/src/test/fuzz/integer.cpp b/src/test/fuzz/integer.cpp index 81516fd9f87..89c29dbcb70 100644 --- a/src/test/fuzz/integer.cpp +++ b/src/test/fuzz/integer.cpp @@ -118,7 +118,7 @@ FUZZ_TARGET(integer, .init = initialize_integer) } (void)MillisToTimeval(i64); (void)SighashToStr(uch); - (void)SipHashUint256(u64, u64, u256); + (void)PresaltedSipHasher(u64, u64)(u256); (void)PresaltedSipHasher(u64, u64)(u256, u32); (void)ToLower(ch); (void)ToUpper(ch); diff --git a/src/test/hash_tests.cpp b/src/test/hash_tests.cpp index d4592df7eeb..a5059a8fe8c 100644 --- a/src/test/hash_tests.cpp +++ b/src/test/hash_tests.cpp @@ -104,7 +104,7 @@ BOOST_AUTO_TEST_CASE(siphash) hasher.Write(0x2F2E2D2C2B2A2928ULL); BOOST_CHECK_EQUAL(hasher.Finalize(), 0xe612a3cb9ecba951ull); - BOOST_CHECK_EQUAL(SipHashUint256(0x0706050403020100ULL, 0x0F0E0D0C0B0A0908ULL, uint256{"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"}), 0x7127512f72f27cceull); + BOOST_CHECK_EQUAL(PresaltedSipHasher(0x0706050403020100ULL, 0x0F0E0D0C0B0A0908ULL)(uint256{"1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"}), 0x7127512f72f27cceull); // Check test vectors from spec, one byte at a time CSipHasher hasher2(0x0706050403020100ULL, 0x0F0E0D0C0B0A0908ULL); @@ -128,9 +128,9 @@ BOOST_AUTO_TEST_CASE(siphash) // and the test would be affected by default tx version bumps if not fixed. tx.version = 1; ss << TX_WITH_WITNESS(tx); - BOOST_CHECK_EQUAL(SipHashUint256(1, 2, ss.GetHash()), 0x79751e980c2a0a35ULL); + BOOST_CHECK_EQUAL(PresaltedSipHasher(1, 2)(ss.GetHash()), 0x79751e980c2a0a35ULL); - // Check consistency between CSipHasher and SipHashUint256 and PresaltedSipHasher. + // Check consistency between CSipHasher and PresaltedSipHasher. FastRandomContext ctx; for (int i = 0; i < 16; ++i) { uint64_t k0 = ctx.rand64(); @@ -139,7 +139,7 @@ BOOST_AUTO_TEST_CASE(siphash) CSipHasher sip256(k0, k1); sip256.Write(x); - BOOST_CHECK_EQUAL(SipHashUint256(k0, k1, x), sip256.Finalize()); // TODO modified in follow-up commit + BOOST_CHECK_EQUAL(PresaltedSipHasher(k0, k1)(x), sip256.Finalize()); CSipHasher sip288 = sip256; uint32_t n = ctx.rand32(); diff --git a/src/util/hasher.cpp b/src/util/hasher.cpp index cac602a893f..3d5ffcf89b4 100644 --- a/src/util/hasher.cpp +++ b/src/util/hasher.cpp @@ -7,17 +7,20 @@ #include #include -SaltedUint256Hasher::SaltedUint256Hasher() : - k0{FastRandomContext().rand64()}, - k1{FastRandomContext().rand64()} {} +SaltedUint256Hasher::SaltedUint256Hasher() : m_hasher{ + FastRandomContext().rand64(), + FastRandomContext().rand64()} +{} -SaltedTxidHasher::SaltedTxidHasher() : - k0{FastRandomContext().rand64()}, - k1{FastRandomContext().rand64()} {} +SaltedTxidHasher::SaltedTxidHasher() : m_hasher{ + FastRandomContext().rand64(), + FastRandomContext().rand64()} +{} -SaltedWtxidHasher::SaltedWtxidHasher() : - k0{FastRandomContext().rand64()}, - k1{FastRandomContext().rand64()} {} +SaltedWtxidHasher::SaltedWtxidHasher() : m_hasher{ + FastRandomContext().rand64(), + FastRandomContext().rand64()} +{} SaltedOutpointHasher::SaltedOutpointHasher(bool deterministic) : m_hasher{ deterministic ? 0x8e819f2607a18de6 : FastRandomContext().rand64(), diff --git a/src/util/hasher.h b/src/util/hasher.h index e7e69530cc5..cdf2250bf3c 100644 --- a/src/util/hasher.h +++ b/src/util/hasher.h @@ -17,47 +17,43 @@ class SaltedUint256Hasher { -private: - /** Salt */ - const uint64_t k0, k1; + const PresaltedSipHasher m_hasher; public: SaltedUint256Hasher(); - size_t operator()(const uint256& hash) const { - return SipHashUint256(k0, k1, hash); + size_t operator()(const uint256& hash) const + { + return m_hasher(hash); } }; class SaltedTxidHasher { -private: - /** Salt */ - const uint64_t k0, k1; + const PresaltedSipHasher m_hasher; public: SaltedTxidHasher(); - size_t operator()(const Txid& txid) const { - return SipHashUint256(k0, k1, txid.ToUint256()); + size_t operator()(const Txid& txid) const + { + return m_hasher(txid.ToUint256()); } }; class SaltedWtxidHasher { -private: - /** Salt */ - const uint64_t k0, k1; + const PresaltedSipHasher m_hasher; public: SaltedWtxidHasher(); - size_t operator()(const Wtxid& wtxid) const { - return SipHashUint256(k0, k1, wtxid.ToUint256()); + size_t operator()(const Wtxid& wtxid) const + { + return m_hasher(wtxid.ToUint256()); } }; - class SaltedOutpointHasher { const PresaltedSipHasher m_hasher; @@ -80,8 +76,7 @@ public: } }; -struct FilterHeaderHasher -{ +struct FilterHeaderHasher { size_t operator()(const uint256& hash) const { return ReadLE64(hash.begin()); } }; From 118d22ddb4ba6af6cd54204dda579c2ff9a70c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Tue, 30 Sep 2025 19:03:02 -0400 Subject: [PATCH 5/6] optimization: cache `PresaltedSipHasher` in `CBlockHeaderAndShortTxIDs` Replaces separate `shorttxidk0`/`shorttxidk1` members with a cached `PresaltedSipHasher`, so `GetShortID()` reuses the precomputed `SipHash` state instead of rebuilding it on every call. `CBlockHeaderAndShortTxIDs` was never intended to be used before `FillShortTxIDSelector()` runs; doing so already relied on indeterminate salt values. The new `Assert(m_hasher)` just makes this invariant explicit and fails fast if the object is used in an uninitialized state. --- src/blockencodings.cpp | 23 +++++++++++++---------- src/blockencodings.h | 6 +++--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/blockencodings.cpp b/src/blockencodings.cpp index 55c8f8a056f..d48ba400a1e 100644 --- a/src/blockencodings.cpp +++ b/src/blockencodings.cpp @@ -17,11 +17,14 @@ #include -CBlockHeaderAndShortTxIDs::CBlockHeaderAndShortTxIDs(const CBlock& block, const uint64_t nonce) : - nonce(nonce), - shorttxids(block.vtx.size() - 1), prefilledtxn(1), header(block) { +CBlockHeaderAndShortTxIDs::CBlockHeaderAndShortTxIDs(const CBlock& block, uint64_t nonce) + : nonce(nonce), + shorttxids(block.vtx.size() - 1), + prefilledtxn(1), + header(block) +{ FillShortTxIDSelector(); - //TODO: Use our mempool prior to block acceptance to predictively fill more than just the coinbase + // TODO: Use our mempool prior to block acceptance to predictively fill more than just the coinbase prefilledtxn[0] = {0, block.vtx[0]}; for (size_t i = 1; i < block.vtx.size(); i++) { const CTransaction& tx = *block.vtx[i]; @@ -29,21 +32,21 @@ CBlockHeaderAndShortTxIDs::CBlockHeaderAndShortTxIDs(const CBlock& block, const } } -void CBlockHeaderAndShortTxIDs::FillShortTxIDSelector() const { +void CBlockHeaderAndShortTxIDs::FillShortTxIDSelector() const +{ DataStream stream{}; stream << header << nonce; CSHA256 hasher; hasher.Write((unsigned char*)&(*stream.begin()), stream.end() - stream.begin()); uint256 shorttxidhash; hasher.Finalize(shorttxidhash.begin()); - shorttxidk0 = shorttxidhash.GetUint64(0); - shorttxidk1 = shorttxidhash.GetUint64(1); + m_hasher.emplace(shorttxidhash.GetUint64(0), shorttxidhash.GetUint64(1)); } -uint64_t CBlockHeaderAndShortTxIDs::GetShortID(const Wtxid& wtxid) const { +uint64_t CBlockHeaderAndShortTxIDs::GetShortID(const Wtxid& wtxid) const +{ static_assert(SHORTTXIDS_LENGTH == 6, "shorttxids calculation assumes 6-byte shorttxids"); - PresaltedSipHasher hasher(shorttxidk0, shorttxidk1); // TODO extract - return hasher(wtxid.ToUint256()) & 0xffffffffffffL; + return (*Assert(m_hasher))(wtxid.ToUint256()) & 0xffffffffffffL; } /* Reconstructing a compact block is in the hot-path for block relay, diff --git a/src/blockencodings.h b/src/blockencodings.h index 133724b64e8..124df50a3d3 100644 --- a/src/blockencodings.h +++ b/src/blockencodings.h @@ -5,6 +5,7 @@ #ifndef BITCOIN_BLOCKENCODINGS_H #define BITCOIN_BLOCKENCODINGS_H +#include #include #include @@ -87,8 +88,7 @@ typedef enum ReadStatus_t } ReadStatus; class CBlockHeaderAndShortTxIDs { -private: - mutable uint64_t shorttxidk0, shorttxidk1; + mutable std::optional m_hasher; uint64_t nonce; void FillShortTxIDSelector() const; @@ -112,7 +112,7 @@ public: /** * @param[in] nonce This should be randomly generated, and is used for the siphash secret key */ - CBlockHeaderAndShortTxIDs(const CBlock& block, const uint64_t nonce); + CBlockHeaderAndShortTxIDs(const CBlock& block, uint64_t nonce); uint64_t GetShortID(const Wtxid& wtxid) const; From 6eb5ba569141b722a5e27ef36e04994886769c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Tue, 30 Sep 2025 19:03:02 -0400 Subject: [PATCH 6/6] refactor: extract shared `SipHash` state into `SipHashState` Split the repeated `SipHash` v[0..3] initialization into a small `SipHashState` helper that is used by both `CSipHasher` and `PresaltedSipHasher`. Added explanatory comments to clarify behavior, documenting the equivalence of `PresaltedSipHasher` `operator()` overloads to `CSipHasher` usage. Co-authored-by: Ryan Ofsky --- src/crypto/siphash.cpp | 50 ++++++++++++++++++------------------------ src/crypto/siphash.h | 45 +++++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/crypto/siphash.cpp b/src/crypto/siphash.cpp index 49cc234993e..89dbad6f44f 100644 --- a/src/crypto/siphash.cpp +++ b/src/crypto/siphash.cpp @@ -19,41 +19,33 @@ v2 = std::rotl(v2, 32); \ } while (0) -CSipHasher::CSipHasher(uint64_t k0, uint64_t k1) -{ - v[0] = C0 ^ k0; - v[1] = C1 ^ k1; - v[2] = C2 ^ k0; - v[3] = C3 ^ k1; - count = 0; - tmp = 0; -} +CSipHasher::CSipHasher(uint64_t k0, uint64_t k1) : m_state{k0, k1} {} CSipHasher& CSipHasher::Write(uint64_t data) { - uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3]; + uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3]; - assert(count % 8 == 0); + assert(m_count % 8 == 0); v3 ^= data; SIPROUND; SIPROUND; v0 ^= data; - v[0] = v0; - v[1] = v1; - v[2] = v2; - v[3] = v3; + m_state.v[0] = v0; + m_state.v[1] = v1; + m_state.v[2] = v2; + m_state.v[3] = v3; - count += 8; + m_count += 8; return *this; } CSipHasher& CSipHasher::Write(std::span data) { - uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3]; - uint64_t t = tmp; - uint8_t c = count; + uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3]; + uint64_t t = m_tmp; + uint8_t c = m_count; while (data.size() > 0) { t |= uint64_t{data.front()} << (8 * (c % 8)); @@ -68,21 +60,21 @@ CSipHasher& CSipHasher::Write(std::span data) data = data.subspan(1); } - v[0] = v0; - v[1] = v1; - v[2] = v2; - v[3] = v3; - count = c; - tmp = t; + m_state.v[0] = v0; + m_state.v[1] = v1; + m_state.v[2] = v2; + m_state.v[3] = v3; + m_count = c; + m_tmp = t; return *this; } uint64_t CSipHasher::Finalize() const { - uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3]; + uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3]; - uint64_t t = tmp | (((uint64_t)count) << 56); + uint64_t t = m_tmp | (((uint64_t)m_count) << 56); v3 ^= t; SIPROUND; @@ -98,7 +90,7 @@ uint64_t CSipHasher::Finalize() const uint64_t PresaltedSipHasher::operator()(const uint256& val) const noexcept { - uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3]; + uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3]; uint64_t d = val.GetUint64(0); v3 ^= d; @@ -135,7 +127,7 @@ uint64_t PresaltedSipHasher::operator()(const uint256& val) const noexcept /** Specialized implementation for efficiency */ uint64_t PresaltedSipHasher::operator()(const uint256& val, uint32_t extra) const noexcept { - uint64_t v0 = v[0], v1 = v[1], v2 = v[2], v3 = v[3]; + uint64_t v0 = m_state.v[0], v1 = m_state.v[1], v2 = m_state.v[2], v3 = m_state.v[3]; uint64_t d = val.GetUint64(0); v3 ^= d; SIPROUND; diff --git a/src/crypto/siphash.h b/src/crypto/siphash.h index b81b9557dec..2f28473a4f1 100644 --- a/src/crypto/siphash.h +++ b/src/crypto/siphash.h @@ -5,28 +5,34 @@ #ifndef BITCOIN_CRYPTO_SIPHASH_H #define BITCOIN_CRYPTO_SIPHASH_H +#include #include #include class uint256; -/** SipHash-2-4 */ -class CSipHasher +/** Shared SipHash internal state v[0..3], initialized from (k0, k1). */ +class SipHashState { -private: - uint64_t v[4]; - uint64_t tmp; - uint8_t count; // Only the low 8 bits of the input size matter. + static constexpr uint64_t C0{0x736f6d6570736575ULL}, C1{0x646f72616e646f6dULL}, C2{0x6c7967656e657261ULL}, C3{0x7465646279746573ULL}; public: - static constexpr uint64_t C0{0x736f6d6570736575ULL}; - static constexpr uint64_t C1{0x646f72616e646f6dULL}; - static constexpr uint64_t C2{0x6c7967656e657261ULL}; - static constexpr uint64_t C3{0x7465646279746573ULL}; + explicit SipHashState(uint64_t k0, uint64_t k1) noexcept : v{C0 ^ k0, C1 ^ k1, C2 ^ k0, C3 ^ k1} {} - /** Construct a SipHash calculator initialized with 128-bit key (k0, k1) */ + std::array v{}; +}; + +/** General SipHash-2-4 implementation. */ +class CSipHasher +{ + SipHashState m_state; + uint64_t m_tmp{0}; + uint8_t m_count{0}; //!< Only the low 8 bits of the input size matter. + +public: + /** Construct a SipHash calculator initialized with 128-bit key (k0, k1). */ CSipHasher(uint64_t k0, uint64_t k1); - /** Hash a 64-bit integer worth of data + /** Hash a 64-bit integer worth of data. * It is treated as if this was the little-endian interpretation of 8 bytes. * This function can only be used when a multiple of 8 bytes have been written so far. */ @@ -48,17 +54,18 @@ public: */ class PresaltedSipHasher { - uint64_t v[4]; + const SipHashState m_state; public: - explicit PresaltedSipHasher(uint64_t k0, uint64_t k1) noexcept { - v[0] = CSipHasher::C0 ^ k0; - v[1] = CSipHasher::C1 ^ k1; - v[2] = CSipHasher::C2 ^ k0; - v[3] = CSipHasher::C3 ^ k1; - } + explicit PresaltedSipHasher(uint64_t k0, uint64_t k1) noexcept : m_state{k0, k1} {} + /** Equivalent to CSipHasher(k0, k1).Write(val).Finalize(). */ uint64_t operator()(const uint256& val) const noexcept; + + /** + * Equivalent to CSipHasher(k0, k1).Write(val).Write(extra).Finalize(), + * with `extra` encoded as 4 little-endian bytes. + */ uint64_t operator()(const uint256& val, uint32_t extra) const noexcept; };