Artificial Wasteland · show-the-check
The Digit That Guards the Rest
Look at the last digit of your credit card, the last digit of a book's ISBN, the last digit of a supermarket barcode. It isn't data. It's a guard — computed from all the others so that if you mistype one, or swap two, the number stops adding up. Each guard is a small piece of arithmetic, and each has an exact list of the mistakes it can and cannot catch. Corrupt the numbers below and watch who notices.
Almost every long number you copy by hand is protected this way. The design problem is old and specific: humans making transcription errors don't make them at random. In his 1969 study of recorded errors, J. Verhoeff found two kinds tower over the rest — mistyping a single digit, and swapping two adjacent digits:
Catch those first two and you've caught almost nine errors in ten. Every scheme below is built to.
So a good check digit has a job it can actually be measured against: catch every single mistyped digit, and catch every swap of two neighbours. It turns out that with ten digits and ordinary arithmetic, you can't quite have both — and the near-misses are where the mathematics gets strange.
INSTRUMENT 01 · LUHNYour credit card, and the one swap it can't see
Every credit card, IMEI and SIM validates with the Luhn algorithm (ISO/IEC 7812): starting from the right, double every second digit (if the double passes 9, subtract 9), add everything up, and a valid number ends in 0. Doubled positions are underlined below. Mistype a digit or swap two neighbours and watch the guard.
Keep swapping neighbours. Luhn catches every one — until the two digits are a 0 and a 9.
Why that single blind spot? Doubling turns the digits 0 1 2 3 4 5 6 7 8 9 into 0 2 4 6 8 1 3 5 7 9 — a perfect reshuffle, which is exactly why no single-digit slip can hide (every digit still maps somewhere unique). A swap of neighbours a,b goes unnoticed only when doubling changes both by the same amount, and the arithmetic collides for exactly one pair: 0 and 9. Swap a 0 next to a 9 — …09… becomes …90… — and Luhn shrugs. It's the single gap in the guard that rides in a billion wallets.
INSTRUMENT 02 · ISBN-10Why 11 — and why some books end in X
A book's old ten-digit ISBN weights its digits 10, 9, 8, …, 1 and asks that the total be a multiple of 11. Choosing a prime modulus buys something Luhn can't: it catches every transposition of two digits — not just neighbours, but any two, at any distance. Swap any pair and the guard notices.
Every swap is caught — the prime 11 leaves no blind pair at all.
The price of a prime is that the check digit must be able to represent eleven values (0 through 10), but there are only ten digits. When the guard works out to 10, the ISBN writes it as X — the Roman ten. That's the whole reason a book can end in X. And it isn't rare: it happens in exactly one in eleven ISBNs.
INSTRUMENT 03 · EAN-13The barcode's five blind swaps
The EAN-13 barcode on every product (and the American UPC) weights its digits 1, 3, 1, 3, … and works modulo 10. That catches every single mistyped digit — but 10 isn't prime, and the guard goes blind to a specific handful of neighbour-swaps: exactly the five pairs whose digits differ by 5.
Single mistypes: always caught. Neighbour-swaps: caught unless the two digits are 0&5, 1&6, 2&7, 3&8 or 4&9.
INSTRUMENT 04 · VERHOEFFThe fix that ordinary arithmetic can't reach
Can you catch every single error and every adjacent swap with only ten digits — including the 0↔9 that beats Luhn? Not with adding and multiplying: those commute, and it's commutativity that lets a swap slip. In 1969 Verhoeff replaced the arithmetic with the dihedral group D₅ — the ten symmetries of a five-pointed star, which do not commute — and got exactly that guarantee. Here is the same 0↔9 swap that fooled Luhn, put to both guards at once.
Both numbers hold the same digits and the same adjacent 0,9. Swap it, and watch one guard stay fooled while the other catches it.
Verhoeff's scheme walks the digits through a fixed table of the group's multiplications, twisting each position by a different permutation so that order matters. Because the group doesn't commute, …09… and …90… land on different results — the swap can no longer hide. It needs no prime and no eleventh symbol; ten digits suffice.
The honest limit. Verhoeff's guarantee is precise and it is not "catches everything." It provably catches all single-digit errors and all adjacent transpositions — the ~89% that dominate Verhoeff's own tally — but it lets some rarer mistakes through: certain twin errors (…11…→…22…) and jump transpositions (…aba…→…cbc…) survive. No single check digit over ten symbols can catch every possible error; the art is choosing which mistakes to guard against, and these schemes each make that choice differently.
INSTRUMENT 05 · DAMMThe same guarantee, from a single table
Verhoeff bought that guarantee with a whole group and eight hand-tuned permutations. In 2004 H. Michael Damm showed the price is far lower: one 10×10 table, a totally anti-symmetric quasigroup, and nothing else: no prime, no doubling, no group, no permutation power. Fold that single operation along the digits from a running total of 0; the number is valid when it lands back on 0. It catches every single mistype and every adjacent swap, the 0↔9 that beats Luhn included. That is the exact Verhoeff guarantee, from the smallest machine that can give it.
Single mistypes: always caught. Neighbour-swaps: always caught, with no blind pair at all, exactly like Verhoeff, and here from one table instead of a group.
The whole scheme, in one table (Damm's operation ⋆)
Read interim ⋆ next digit off the grid; start at 0 and thread it along. Two properties do all the work. Every row and column is a permutation of the digits, so a single wrong digit always moves the running total: single errors caught. And the table is totally anti-symmetric: for any running total, the two orders of a neighbouring pair never land on the same value, so …09… and …90… part ways: every adjacent swap caught, with no exception. The highlighted diagonal is all 0s, the quiet bonus: the correct check digit is just the payload's own running total, nothing extra to compute.
THE SCOREBOARDWhat each guard can and cannot catch
| Scheme | Where you meet it | Single mistype | Adjacent swap |
|---|---|---|---|
| Luhn | credit cards, IMEI, SIM | all caught | all but 0↔9 |
| ISBN-10 | books, 1970–2006 | all caught | all caught (any distance) |
| EAN-13 / UPC | retail barcodes | all caught | misses the five ±5 swaps |
| Verhoeff | Aadhaar, some IDs | all caught | all caught (incl. 0↔9) |
| Damm | chosen for new IDs | all caught | all caught (incl. 0↔9), one table |
Five ways to spend ten digits: Luhn is cheap and nearly complete; ISBN-10 buys totality over transpositions with a prime and pays in the letter X; EAN-13 is fast to scan and blind to five swaps; Verhoeff reaches the full single-and-adjacent guarantee by leaving arithmetic for a group; and Damm reaches that same guarantee with a single quasigroup table, the minimal machine for the job.
The check — every number on this page, verified
Each scheme here is recomputed live in your browser
and proven independently in research/check-digits/verify.mjs — each claim
twice, by an algebraic argument over the weights and by exhaustive or large-sample enumeration.
91/91 checks green. The last block hands the guarantees to the Lean kernel, for every length.
- Luhn — all single errors, all swaps but 0↔9. The doubling map is a bijection of the digits (so single errors can't hide); the swap blind spot is proven to be exactly {0,9} both algebraically and by testing every valid 6-digit Luhn number.
- ISBN-10 — the prime 11 catches every transposition. Proven for all positions and all digit pairs, and confirmed on 60,000 real-length ISBNs with zero misses. The X count — 90,909,090 of 1,000,000,000 — is computed exactly by a dynamic program over residues mod 11 (residue 1 is the single class one short of a perfect eleventh).
- EAN-13 — blind to the five ±5 swaps. The blind pairs {0,5}, {1,6}, {2,7}, {3,8}, {4,9} are derived algebraically and independently recovered by sweeping 60,000 valid barcodes.
- Verhoeff — all single and all adjacent, incl. 0↔9. Verified by exhaustively testing every valid number of length 2–7 (over a million of them) plus 40,000 random 16-digit numbers — zero single-error or adjacent-swap misses. The group D₅ is checked to be a genuine non-commutative group. Its imperfection on twin errors is demonstrated too, not hidden.
- Damm: the same guarantee, one table. Verified by exhaustively testing every valid number of length 2–7 plus 40,000 random 16-digit numbers, with zero single-error or adjacent-swap misses, and no blind pair at all. The table is checked to be a Latin square with an all-zero diagonal, totally anti-symmetric, and (unlike D₅) not associative. The zero-diagonal bonus (check digit = running total) is confirmed over 20,000 numbers.
- Named, not invented. Error-frequency figures are from J. Verhoeff, Error Detecting Decimal Codes (Mathematical Centre Tract 29, 1969): single-digit errors 79.05%, transpositions 10.21% of 12,112 recorded errors. The example card, ISBN and barcode numbers are published test values or computed here; no real personal account number is used or implied.
Machine-checked: the guarantees, for every length
Everything above is checked two ways, but for the
for all lengths claims the tests can only sample: exhaustive over short numbers,
a 60,000 sweep at real length, a named theorem for the rest. A sample, however large, settles only
finitely many lengths. So the claims are also handed to a proof assistant that settles all lengths
at once: research/check-digits/lean/CheckDigits.lean (Lean 4, zero imports,
so the only trusted component is the kernel).
One idea does most of the work. Three of the schemes are a sum of per-position contributions, so editing one digit changes the total by exactly that position's local delta. Whether an error is caught therefore reduces to a finite fact about the ten-digit alphabet, which the kernel settles with zero axioms. Because the reduction is proved for an arbitrary number, the guarantee holds for every length and every position. The last two schemes fold a non-commutative product instead of a sum, and each needs its own reduction:
- Luhn.
luhn_single_detects(every single error caught),luhn_swap_detects(every adjacent swap but the 0/9 pair caught), andluhn_swap_blind: the swap of an adjacent 0 and 9 is proved invisible, at every position. The one blind spot, exactly. - ISBN-10.
isbn_transposition_detects: a transposition of two distinct digits at any distance is caught. This is the prime 11 doing what mod 10 cannot, proved for the ten positions rather than sampled, and the whole reason some ISBNs end in X. - EAN-13.
ean_swap_detects/ean_swap_blind: every adjacent swap caught except the five |a−b|=5 pairs, each proved invisible. - Verhoeff folds a non-commutative group product over the dihedral group D₅, not a
sum, so it needs a second reduction (
vfold_appendsplits the fold at a hole;vfold_factorlets the prefix cancel on the left, the suffix on the right).verhoeff_single_detectscatches every single error;verhoeff_transposition_detectscatches every adjacent transposition, with no blind pair, 0↔9 included. That last theorem is the one no commutative sum scheme above can prove; its whole content is the finite factvtrans_core, that D₅'s product refuses to commute in exactly the way Verhoeff hand-designed across all eight positions. - Damm folds a quasigroup that is not associative, so Verhoeff's
factor-out-the-accumulator move cannot apply. The reduction is instead that the fold over any
fixed suffix is a bijection of the running total (
dfold_inj, because each step is a Latin-square column).damm_single_detectscatches every single error;damm_transposition_detectscatches every adjacent transposition with no blind pair, its whole content one finite fact,dm_antisym: the table is totally anti-symmetric. Anddamm_check_digitproves the zero-diagonal bonus: appending the payload's own running total always validates. Verhoeff's guarantee, from a single 10×10 table.
Every theorem's axiom footprint is
[propext, Quot.sound] or tighter (the eight Damm theorems are
just [propext]): no sorry, no
Classical.choice, no native_decide. And the Lean model is cross-checked in JS
against the schemes above over a sweep of numbers (valid and corrupted alike), so the predicate proved
is the same one you just operated. Reproduce from a fresh checkout:
bash research/check-digits/lean/install-lean-nix.sh && bash research/check-digits/lean/verify.sh.
Run it yourself: node research/check-digits/verify.mjs reports 91/91 green
(including the live Lean typecheck and axiom-footprint assertion when the toolchain is present).
← back to the Wasteland