DEV Community

Cover image for I built a tamper-evident record log in Go using nothing but SHA-256 and a hash chain
Kevin Nambubbi
Kevin Nambubbi

Posted on

I built a tamper-evident record log in Go using nothing but SHA-256 and a hash chain

I've been learning Go, and instead of another CRUD tutorial project, I wanted to build something with a real property: provable tamper-evidence. Not "we have logs and we trust them" — actually provable, the same way a blockchain proves a block wasn't altered, minus all the distributed-consensus baggage you don't need for a single append-only record.

The core idea is simple enough to fit in a paragraph: every log entry contains a hash of the entry before it. If anyone edits an entry after the fact — even directly in the storage file, even as an admin — every hash from that point forward stops matching. You don't need to trust the log; you can verify it.

The data model

type AuditEntry struct {
    ID           int    `json:"id"`
    Timestamp    string `json:"timestamp"`
    Actor        string `json:"actor"`
    Action       string `json:"action"`
    Document     string `json:"document"`
    DocumentHash string `json:"document_hash"`
    PreviousHash string `json:"previous_hash"`
    EntryHash    string `json:"entry_hash"`
}
Enter fullscreen mode Exit fullscreen mode

Nothing exotic — PreviousHash is what turns a list of records into a chain.

Computing the chain

func computeEntryHash(entry AuditEntry) string {
    entry.EntryHash = ""
    data, err := json.Marshal(entry)
    if err != nil {
        return ""
    }
    hash := sha256.Sum256(data)
    return hex.EncodeToString(hash[:])
}
Enter fullscreen mode Exit fullscreen mode

Zero out the entry's own hash field before marshaling (otherwise you'd be hashing a value that depends on itself), marshal the rest, hash it. Each new entry's PreviousHash is set to the prior entry's EntryHash, so the whole log becomes a singly linked list of hashes.

Verifying the chain

func verifyChain() error {
    entries, err := loadLog()
    if err != nil {
        return err
    }
    for i, entry := range entries {
        computed := computeEntryHash(entry)
        if computed != entry.EntryHash {
            return fmt.Errorf("CHAIN BROKEN at entry %d: entry hash mismatch", entry.ID)
        }
        if i == 0 {
            if entry.PreviousHash != GenesisHash {
                return fmt.Errorf("CHAIN BROKEN at entry %d: invalid genesis hash", entry.ID)
            }
        } else if entry.PreviousHash != entries[i-1].EntryHash {
            return fmt.Errorf("CHAIN BROKEN at entry %d: previous hash mismatch", entry.ID)
        }
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Walk the chain, recompute every hash, compare against what's stored. If someone edited entry 2's document field and tried to just fix entry 2's own EntryHash to match, entry 3's PreviousHash would still point to the old value — the break is unavoidable without rewriting everything downstream.

I put together a small interactive page where you can try this yourself — edit a field, watch the chain visibly break and the verification stamp flip from green to red: https://getcustodian-demo.netlify.app/

Where I think this is genuinely useful (not just a toy)

The pattern behind this — hash-chained records — is the same primitive behind things like Amazon QLDB's ledger, Certificate Transparency logs, and git itself (a commit chain is structurally identical to this). The interesting applications aren't "blockchain for X," they're boring, specific ones: proving a medical record wasn't edited after an incident, proving a chain-of-custody log for evidence handling wasn't tampered with, or giving an auditor something they can independently verify instead of just trusting your server logs.

What it doesn't solve yet, and where I'd love feedback

Right now this is a CLI over a single JSON file — fine for a demo, not fine for anything real. The open problems I'm actively thinking through:

  • External anchoring: right now, if someone controls the whole storage file, they could theoretically rewrite the entire chain from scratch and it would self-verify. The fix is periodically publishing the latest hash somewhere you don't control (a public timestamping service, a public commit, etc.) so there's an external reference point. Curious if people have opinions on OpenTimestamps vs. just anchoring into a public git repo vs. something else.
  • Concurrent writers: a single JSON file obviously doesn't survive concurrent access or crash-mid-write. Leaning toward SQLite with WAL mode for a v2, but open to arguments for an append-only log format instead (more Kafka-esque).
  • Making it a real service vs. staying a library/CLI people embed.

Repo's rough right now but happy to open it up if there's interest. Mostly wanted to write up the core idea since I think hash-chained logs are a genuinely underused pattern outside of blockchain contexts — would love to hear if others have built similar things or hit walls I haven't found yet.

Top comments (0)