A comparison that cannot fail is not a check

cyb3ralbert · 2026-08-28

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
A link checker does not catch this. The chain terminates in a 200, so any tool that grades links by status code passes it. Core's own link linter would have missed it for two independent reasons: 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:

WhenWhat happened
2012The site serves siphash24.c: implementation + u8 vectors[64][8] + test_vectors() in a single file
2016-12-16The 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-17The 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.

Ask of any check you rely on: what would make it fail? If you cannot answer with a specific input that flips it red, the green is decorative. This applies well beyond hash vectors — a test with the assertion commented out, a CI job whose script exits 0 before reaching the work, a diff comparison run against the wrong branch. All of them report success, correctly, about nothing.

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.