What changed, and why it matters
This commit adds a new, weaker variant of the SipHash algorithm inside Bitcoin Core's cryptography module. It is explicitly designed for low-security uses such as internal hash tables, not for protecting funds or network messages. The code itself appears to be a clean, intentional addition with extensive comments explaining the trade-offs. There is no indication in the commit that this fixes a vulnerability or introduces an exploitable bug.
Review future commits that adopt SipHasher13UJ to ensure it is only used in contexts matching its security assumptions (internal hash tables, secret keys, bounded attacker-controlled jumbo blocks). No immediate action is required for this commit alone.
Security signals we found
New weaker cryptographic primitive added (SipHash-1-3 vs. standard SipHash-2-4)
Explicit unpadded input design removes length-commitment padding
Jumbo 256-bit blocks increase attacker control within a single round
Comments restrict usage to hash-table scenarios with bounded non-hash inputs
No caller code or production usage shown in this commit
Evidence from the diff
The patch introduces SipHasher13UJ, a custom SipHash-1-3 variant without padding and with optional 256-bit ‘jumbo’ blocks. It adds Compress1, Compress1Jumbo, and Finalize3U methods to SipHashState and exposes them through a new SipHasher13UJ class. The commit message and header comments describe the design rationale: reduced rounds for speed, unpadded block-oriented input, and jumbo blocks bounded by cryptographic hash outputs. No existing callers are changed, and no security fix or vulnerability disclosure is referenced.
Changed components
src/crypto/siphash.hsrc/crypto/siphash.cppInspect captured patch +103 / −0
diff --git a/src/crypto/siphash.cpp b/src/crypto/siphash.cpp
index 4c3f3b9d..333fad27 100644
--- a/src/crypto/siphash.cpp
+++ b/src/crypto/siphash.cpp
@@ -49,6 +49,23 @@ uint64_t CSipHasher::Finalize() const
.Finalize4();
}
+SipHasher13UJ& SipHasher13UJ::Write(uint64_t data) noexcept
+{
+ m_state.Compress1(data);
+ return *this;
+}
+
+SipHasher13UJ& SipHasher13UJ::WriteJumbo(const uint256& hash) noexcept
+{
+ m_state.Compress1Jumbo(hash);
+ return *this;
+}
+
+uint64_t SipHasher13UJ::Finalize() const noexcept
+{
+ return m_state.Copy().Finalize3U();
+}
+
uint64_t PresaltedSipHasher::operator()(const uint256& val) const noexcept
{
return m_state.Copy()
diff --git a/src/crypto/siphash.h b/src/crypto/siphash.h
index 5bcc6d45..0d17726c 100644
--- a/src/crypto/siphash.h
+++ b/src/crypto/siphash.h
@@ -20,6 +20,9 @@ class SipHashState
static constexpr uint64_t C0{0x736f6d6570736575}, C1{0x646f72616e646f6d}, C2{0x6c7967656e657261}, C3{0x7465646279746573};
/** SipHash v2 finalizer constant. */
static constexpr uint64_t FINALIZER{0xFF};
+ /** SipHash custom unpadded finalizer constant. */
+ static constexpr uint64_t FINALIZER_UNPADDED{0x6465646461706e75};
+ static_assert(FINALIZER_UNPADDED != FINALIZER);
/** Construct a SipHashState with the specified values as state. */
ALWAYS_INLINE SipHashState(uint64_t v0, uint64_t v1, uint64_t v2, uint64_t v3) noexcept : m_v0{v0}, m_v1{v1}, m_v2{v2}, m_v3{v3} {}
@@ -43,6 +46,23 @@ public:
explicit ALWAYS_INLINE SipHashState(uint64_t k0, uint64_t k1) noexcept : SipHashState{C0 ^ k0, C1 ^ k1, C2 ^ k0, C3 ^ k1} {}
/** Construct a copy of this state. */
ALWAYS_INLINE SipHashState Copy() const noexcept { return {m_v0, m_v1, m_v2, m_v3}; }
+ /** Mutably compress one block into this state, with 1 SipRound. */
+ ALWAYS_INLINE SipHashState& Compress1(uint64_t data) noexcept
+ {
+ m_v3 ^= data;
+ SipRound();
+ m_v0 ^= data;
+ return *this;
+ }
+ /** Mutably compress one jumbo block into this state, with 1 SipRound. */
+ ALWAYS_INLINE SipHashState& Compress1Jumbo(const uint256& data) noexcept
+ {
+ const uint64_t d0{data.GetUint64(0)}, d1{data.GetUint64(1)}, d2{data.GetUint64(2)}, d3{data.GetUint64(3)};
+ m_v3 ^= d0; m_v0 ^= d1; m_v1 ^= d2; m_v2 ^= d3;
+ SipRound();
+ m_v0 ^= d0; m_v1 ^= d1; m_v2 ^= d2; m_v3 ^= d3;
+ return *this;
+ }
/** Mutably compress one block into this state, with 2 SipRounds. */
ALWAYS_INLINE SipHashState& Compress2(uint64_t data) noexcept
{
@@ -62,6 +82,16 @@ public:
SipRound();
return m_v0 ^ m_v1 ^ m_v2 ^ m_v3;
}
+ /** Mutably finalize this state with 3 SipRounds using the unpadded finalizer, and return the
+ * resulting hash. */
+ ALWAYS_INLINE uint64_t Finalize3U() noexcept
+ {
+ m_v2 ^= FINALIZER_UNPADDED;
+ SipRound();
+ SipRound();
+ SipRound();
+ return m_v0 ^ m_v1 ^ m_v2 ^ m_v3;
+ }
};
/** General SipHash-2-4 implementation. */
@@ -85,6 +115,62 @@ public:
uint64_t Finalize() const;
};
+/** A custom weaker variant of SipHash-1-3 without padding, and supporting "jumbo" inputs.
+ *
+ * Compared to the traditional SipHash-2-4, this incorporates 3 changes:
+ *
+ * - Use SipHash-1-3: This common variant reduces the number of rounds between input blocks from 2
+ * to 1, and the number of finalization rounds at the end from 4 to 3.
+ * It is a standard faster choice with weaker security guarantees. It is
+ * considered strong enough for use in hash tables and other applications with
+ * only weak security requirements, in particular when attackers cannot directly
+ * observe the hash function output, and cannot control k0 and k1.
+ *
+ * - Remove padding: Standard SipHash operates on byte-oriented inputs, which are converted into
+ * 8-byte blocks internally by adding a padding of 1-8 bytes to obtain a multiple
+ * of 8 bytes. In this variant, the input is a sequence of blocks instead, which
+ * are used without padding. This saves 1 round for inputs that are a multiple of
+ * 8 bytes already. The padding normally adds a commitment to the input's byte
+ * length, but since our input is block oriented, and there is one round per
+ * input, the function of that commitment is directly fulfilled by the number of
+ * rounds. The empty input is permitted; it yields the result of finalizing the
+ * initialization directly. Out of an abundance of caution the finalizing
+ * v2 ^= 0xff is changed to v2 ^= 0x6465646461706e75 ("unpadded" in LE64), to
+ * avoid collisions between standard SipHash-1-3 and this variant.
+ *
+ * - Jumbo blocks: We generalize the input to consist of a sequence of blocks which are each either
+ * exactly 8 bytes (normal blocks) or 32 bytes (jumbo blocks), and may be
+ * arbitrarily mixed. For hash-table use, cryptographic hash outputs must make up
+ * all but a small bounded number of retained jumbo blocks. Note that the block
+ * structure matters: hashing one jumbo block is not the same as hashing the same
+ * data split up into four normal blocks. Jumbo blocks are interpreted as 4 LE64 integers
+ * (d0,d1,d2,d3), and XOR'ed into the state before a round as
+ * (v0,v1,v2,v3) ^= (d1,d2,d3,d0), and XOR'ed into the state after a round as
+ * (v0,v1,v2,v3) ^= (d0,d1,d2,d3). This is a strict generalization of normal block
+ * processing, in the sense that if d1..d3=0, it is identical to processing a normal
+ * block d0 (a single LE64 integer). Of course, making d1..d3=0 should be impossible
+ * in the output of a cryptographic hash function. These jumbo blocks are acceptable
+ * because even though they may give the attacker more control within a single
+ * round, that control is limited by the cryptographic hash in between.
+ *
+ * Other components are unchanged: initialization constants, SipRound, and 64-bit output.
+ * This interface serves as an executable specification for fixed-width implementations.
+ */
+class SipHasher13UJ
+{
+ SipHashState m_state;
+
+public:
+ /** Construct a SipHash-1-3-UJ calculator initialized with 128-bit key (k0, k1). */
+ SipHasher13UJ(uint64_t k0, uint64_t k1) noexcept : m_state{k0, k1} {}
+ /** Hash a normal 64-bit value. */
+ SipHasher13UJ& Write(uint64_t data) noexcept;
+ /** Hash a 256-bit value as a jumbo block. For hash-table use, non-hash inputs must remain few and bounded. */
+ SipHasher13UJ& WriteJumbo(const uint256& hash) noexcept;
+ /** Compute the 64-bit SipHash-1-3-UJ of the data written so far. The object remains untouched. */
+ uint64_t Finalize() const noexcept;
+};
+
/**
* Optimized SipHash-2-4 implementation for uint256.
*
Why this scored 21/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.