Flip One Byte and the Whole Chain Knows
There’s a specific kind of software promise that’s easy to make and hard to keep: “this record can’t be quietly changed after the fact.” It’s the promise behind a Git history, a Certificate Transparency log, an audit trail a regulator will trust — and behind the public “bulletin board” in an end-to-end-verifiable voting system, the record that lets anyone check an election without trusting the people who ran it.
I wanted to build the smallest honest version of that promise, end to end, and — more importantly — the thing that makes it real: the verifier. Anyone can append records to a log. The interesting engineering is the code that hands you back a log someone else controlled and tells you, precisely, whether history was rewritten.
Here’s how it works, where the sharp edges are, and — because I care about not overselling — exactly where this stops and where real cryptographic voting begins.
The primitive: each entry commits to the last
A tamper-evident log is an append-only list where every entry carries a hash of its own contents and the hash of the entry before it:
hash(entry_i) = SHA-256( index ∥ timestamp ∥ payload ∥ hash(entry_{i-1}) )
That single back-reference is the whole trick. Because entry i+1’s hash is computed over entry i’s hash, and i+2 over i+1, the hashes form a chain in which every link depends on everything before it. Change any historical entry and its hash changes; that breaks the next entry’s back-reference, which breaks the next, all the way to the tip. You cannot edit the past in one place — the edit propagates.
Two implementation details matter more than they look:
Canonical serialization. The verifier has to reproduce, byte for byte, what the builder hashed. If the payload is an object, {"a":1,"b":2} and {"b":2,"a":1} are the same data but different strings — and therefore different hashes. So the preimage is built with a stable stringify that sorts keys recursively. Skip this and your log “fails verification” on innocent re-serialization, which trains everyone to ignore the verifier. A tamper-evident log that cries wolf is worse than none.
A transparent hash function. I hand-rolled a dependency-free SHA-256 rather than reach for Web Crypto. Partly ergonomics — crypto.subtle.digest is Promise-based, and I wanted the verifier to be a plain, synchronous function you can read top to bottom. But mostly it’s a values thing: the entire point of a verifiable system is that an auditor can convince themselves of exactly what’s committed to. A 120-line hash function checked against the FIPS 180-4 test vectors is part of that story in a way an opaque platform call isn’t. (This is the same instinct behind Sequent Tech’s open-source, independently-auditable approach — the opposite of the closed blockchain-voting apps that got torn apart by academics.)
The verifier is the artifact
Given a log — possibly tampered with — the verifier trusts nothing in it except the raw contents. For each entry, in order, it recomputes three things:
- Ordering — is the entry’s
indexsequential? (Catches insertions, deletions, reorders.) - Link — does its
prevHashmatch the actual hash of the previous entry? (Catches a cut or a splice.) - Content — does its stored
hashmatch a fresh hash of its contents? (Catches an edit.)
Then it returns every break it found, the kind of each, and the first broken index — the root cause. That structure is what makes it useful in an incident: not “INVALID” but “entry #10 was edited after it was written; here’s the hash it should have.”
The case that justifies the whole design is the sophisticated attacker. A naive tamper — edit an entry, leave its hash — is caught immediately by check 3. But suppose the attacker edits an entry and recomputes that entry’s hash so it’s internally consistent. Check 3 now passes at that entry. Does the tamper get through?
No — and watching why is the point. Check 2 fails at the next entry, because its prevHash still points at the original hash. To fix that, the attacker has to rewrite the next entry’s hash too… which breaks the one after it. To actually hide a single-byte edit in a historical entry, they’d have to forge every subsequent hash in the log — reconstruct the entire tail. That cascade, made visible, is the entire security value of a chain over a pile of independently-hashed rows. My demo has a button for it: edit a ballot, re-hash it, and the “BROKEN” banner moves one step down the chain instead of going away.
I wrapped four attacks in tests and a one-command CLI — flip a tally, edit-and-rehash a ballot, drop a cast ballot, stuff a forged one — and each is caught and pinned to an exact entry. Sixteen tests, all green. The React viewer just runs the verifier on every render, so the VERIFIED/BROKEN indicator is live: click an attack, watch the offending row light up with the reason.
Where this stops (the honest part)
Here’s the boundary I want to be loud about, because eliding it is how security demos become security theater. This proves the integrity of the log, and nothing else.
It says: no one rewrote the record after the fact. It does not say the votes are secret, that the tally is correct, or that the person who cast a ballot wasn’t coerced. A production end-to-end-verifiable voting system — Sequent’s actual domain — layers a great deal on top of a tamper-evident log, none of which my demo implements:
- Ballot secrecy. Real ballots are ElGamal ciphertexts under a threshold key held by multiple trustees. In my demo they’re opaque digests; there’s no encryption.
- Cryptographic mixnets. Re-encryption mix nets shuffle the ballots to sever the link between voter and plaintext, each mix node publishing a zero-knowledge proof that it shuffled honestly (Sequent uses a Terelius–Wikström variant). I name the shuffle events in the log; I don’t produce or check the proofs.
- Cast-as-intended / counted-as-recorded. Benaloh challenges, per-voter tracking codes, and verifiable decryption proofs let a voter — and any observer — confirm their ballot is recorded and the final tally follows from the recorded ballots.
- Consistency across observers. A single chain can still be shown differently to different people (a “split view”). Real systems add signed checkpoints and gossip to detect that; the natural next step here is a Merkle tree with O(log n) inclusion proofs so a voter can check their ballot is in the log cheaply.
I built the part I can stand behind completely today, and I’d rather show that clearly than gesture at cryptography I haven’t implemented.
Why I bothered
Because “tamper-evident” shows up in a lot of specs and a lot fewer codebases, and the gap between the two is exactly the verifier. The primitive is simple; the discipline — canonical preimages, a transparent hash, error messages an auditor can act on, and an honest map of where the guarantees end — is the actual work. It’s the same discipline behind gap-free compliance records, transparency logs, and the bulletin board a democracy checks. Small surface, high stakes. My favorite kind of problem.