A comparison that cannot fail is not a check
A week earlier I had been working through the first chapter of a computer architecture
textbook — positional notation, why octal and hex exist, binary arithmetic, truth
tables. Then I opened src/test/hash_tests.cpp in Bitcoin Core, where a comment
says 64 numbers are SipHash-2-4 output taken from a particular URL. The URL no longer serves
that file. Proposing a replacement meant showing the new address points at the same numbers
— and that turned out to be the part worth writing down, because a comparison of two
arrays says nothing until you know it is capable of coming out unequal.
What the comment claimed
Above the array, three claims in four lines, as the file stood then: these are SipHash-2-4 outputs, produced with a specific key and a specific series of inputs, and they came from here.
/* SipHash-2-4 output with
k = 00 01 02 ...
and
in = (empty string)
in = 00 (1 byte)
in = 00 01 (2 bytes)
in = 00 01 02 (3 bytes)
...
in = 00 01 02 ... 3e (63 bytes)
from: https://131002.net/siphash/siphash24.c
*/
uint64_t siphash_4_2_testvec[] = {
0x726fdb47dd0e0e31, ...
— src/test/hash_tests.cpp at f7253703, the state described throughout
this piece. That line now carries the replacement URL; the edit is at the end.
The link is not a 404, which is exactly the problem
It would be easier if it were. A dead host announces itself. This one answers:
https://131002.net/siphash/siphash24.c 301 -> https://aumasson.jp/ (site root, not a file)
https://www.aumasson.jp/siphash/siphash24.c 404
https://www.aumasson.jp/siphash/ 200
131002.net is the old domain of Jean-Philippe Aumasson, SipHash's co-author, and
the redirect lands on his personal page. Follow the reference the way a reader would —
click it, get an HTTP 200 — and you arrive somewhere that cannot tell you where the
numbers came from. The one surviving /siphash/ section on the new domain consists
of a single sentence, which is the domain owner naming the destination himself rather than me
guessing it:
The SipHash page and documentation has moved to GitHub
— www.aumasson.jp/siphash/, the entire page
mlc parses markdown and HTML, so a .cpp
file is outside its selection entirely, and it runs --offline in CI, so external
URLs are not fetched at all either way. This category —
the reference that resolves but no longer refers — is only found by following it and
asking whether the destination still supports the claim.
The chapter had already covered it
The gap between a first-chapter textbook and a cryptographic hash in production is where I expected to spend the day. There was no gap. Four things I had read the week before were the entire toolkit:
One law for every base. M = Σ dᵢ·bⁱ covers
decimal, binary, octal, hex. Only b changes; everything else in the chapter is a
consequence.
Why 8 and 16, not 7 and 12. 8 = 2³ and
16 = 2⁴, so a group of digits maps onto whole bits with no carry between
groups. That is the reason the same value is legible as 0x726fdb47dd0e0e31 and as
0x31, 0x0e, 0x0e, 0xdd, 0x47, 0xdb, 0x6f, 0x72 — sixteen hex digits in one
group, or eight groups of two. Core stores these vectors as 64-bit words; the reference project
stores them as bytes. Same quantity, different grouping, and the whole comparison depends on
noticing that.
Little-endian is that same formula with base 256. A byte is a digit, byte order
is digit order, and int.from_bytes(b, 'little') literally computes
Σ byteᵢ·256ⁱ. This is not an analogy; it is the same
arithmetic one level up.
SipHash is built from three operations on the chapter's last page. Add, rotate, XOR — the ARX class. The round function has nothing else in it:
v0 = (v0 + v1) & M; v1 = rotl(v1, 13); v1 ^= v0; v0 = rotl(v0, 32)
v2 = (v2 + v3) & M; v3 = rotl(v3, 16); v3 ^= v2
v0 = (v0 + v3) & M; v3 = rotl(v3, 21); v3 ^= v0
v2 = (v2 + v1) & M; v1 = rotl(v1, 17); v1 ^= v2; v2 = rotl(v2, 32)
Addition modulo 2⁶⁴, circular shift, XOR. Between the truth table for XOR in a first-year textbook and a keyed hash used in production there are no intermediate floors. I am not claiming to have learned cryptography here — the opposite. Nothing in this task required understanding SipHash as cryptography. It required being able to compute it.
There is no successor file
The tempting move is to say the file moved and link to it at its new home. That would be wrong.
The file on the website in 2012 was self-contained — implementation, vectors and test in
one siphash24.c. In the author's repository it came apart:
| When | What happened |
|---|---|
| 2012 | The site serves siphash24.c: implementation + u8 vectors[64][8] + test_vectors() in a single file |
| 2016-12-16 | The repository's own siphash24.c — a different file, whose vectors lived in main.c — is removed; the implementation becomes siphash.c, the harness becomes test.c |
| 2016-12-17 | The numbers are split out into their own generated vectors.h |
So one file became three, and no single one of them is the successor. Which one belongs in this
comment is decided by what the comment says: from:, attached to an array of numbers,
answers where these numbers came from, not where the algorithm lives.
siphash.c contains zero vectors. The honest replacement is
vectors.h:
same project, same author, same array — not the same file.
Four supports, none of which rely on each other
The claim to establish: the 64 numbers in the Core tree are the same 64 numbers the new reference holds, and both are genuinely SipHash-2-4 output for the stated parameters.
1. Byte-for-byte against the replacement. Extract
siphash_4_2_testvec[] from the tree and vectors_sip64 from
vectors.h, normalise both to uint64_t, compare:
n=64, mismatches=0. First value 0x726fdb47dd0e0e31, last
0x958a324ceb064572.
2. Against a build of the reference, not just its checked-in file. Clone,
make, run both binaries. ./test reports OK for all four variants,
SipHash-2-4-64 among them; a build with -DGETVECTORS regenerates the
vectors, and the
regenerated output matches Core with mismatches=0. This distinguishes "two files
contain the same text" from "these numbers are what the algorithm produces".
3. Against an implementation with no shared ancestry. A short Python
implementation written from the algorithm description — no code from the reference, none
from Core — run over the same parameters (k = 00 01 … 0f, inputs being the prefixes of
00 01 02 … of length 0 to 63). All 64 agree. At this point the claim no longer
depends on either file being trustworthy.
4. Against the file the dead link used to serve. Two Wayback snapshots of the
old URL, 2013 and 2020, are byte-identical to each other (sha256 6d300e01…).
The header reads "SipHash reference C implementation, Written in 2012 by Jean-Philippe Aumasson
and Daniel J. Bernstein"; from line 143, u8 vectors[64][8], beginning
{ 0x31, 0x0e, 0x0e, 0xdd, 0x47, 0xdb, 0x6f, 0x72, }. Both snapshots agree with the
Core array on all 64 values. That closes the chain at the far end — the old reference and
the new one carry the same numbers, so the edit preserves what the comment was pointing at:
archived siphash24.c = hash_tests.cpp = vectors.h = SipHash-2-4 output
One practical note: the 2020 snapshot is served gzipped, and without --compressed
you save the compressed bytes to disk and your parser reads garbage. Half an hour.
Making sure the comparison could fail
Everything above produced mismatches=0 four times, and four zeros from a comparison
that is structurally incapable of producing anything else is not evidence — it is a
tautology with good manners. Two controls, both of which had to come out non-zero:
negative control (interpret bytes as big-endian): 64 of 64 differ
negative control (shift the array by one): 64 of 64 differ
And one specifically for support #2, since regenerating vectors would prove nothing if the
generator were reading the file it claims to compute. Under #ifdef GETVECTORS the
harness prints the out buffer after calling siphash(); the stored
vectors_sip64 array is only read in the #else branch. The generating
path never touches the numbers it is generating.
What went upstream
One line.
- from: https://131002.net/siphash/siphash24.c
+ from: https://github.com/veorq/SipHash/blob/master/vectors.h
The ratio is the point: everything above exists so that a reviewer does not have to take a
one-line diff on faith, and so that the sentence "these are the same numbers" is something I
had actually verified rather than assumed. The approving review was a
tested ACK — the reviewer reproduced the comparison rather than reading it.
Merged in bitcoin/bitcoin#35995 · ACKs from l0rinc (tested) and maflcko · merged by fanquake
What I'd take from it
Green means nothing until you know what red looks like. The negative controls cost ten minutes and were the only reason the four zeros meant anything. Any comparison written to confirm something will confirm it; you learn whether it discriminates by feeding it something wrong on purpose.
A reference can resolve and still be broken. Status codes grade the host, not the claim. The interesting failures live where the URL answers, the page loads, and the thing it was cited for is no longer there — which is why no linter in the pipeline was ever going to flag this line.
Learning from the bottom paid off within a week, unprompted. I did not pick this task to apply the chapter; the chapter turned out to be the whole toolkit. The known risk of starting at the bottom is spending a year in fundamentals and never arriving at the work, and I don't think reading protects you from that — touching real code at regular intervals does. Here the interval happened to be seven days.