iToverDose/Software· 30 MAY 2026 · 16:01

Why append-only event logs need cryptographic hashing for true immutability

Event sourcing promises permanent records, but databases can still be altered. Learn how cryptographic hashing creates tamper-evident chains that expose even subtle changes to event logs.

DEV Community5 min read0 Comments

Event sourcing sells itself on immutability—once data is written, it can’t be changed. But in practice, that’s only as true as the system’s weakest link. A database row might look immutable in your application code, yet a database administrator with direct access can still update, delete, or corrupt it. Backups can be restored with corrupted data. Migrations can silently alter older records. When that happens, the only clue often comes too late: a customer notices their balance is wrong, and the audit trail has already gone cold.

These risks aren’t theoretical—they happen in production. The solution isn’t to prevent access to the database, but to make any unauthorized change immediately detectable. That’s where cryptographic hashing comes in.

Turning append-only into tamper-evident with hash chains

The core idea is simple: link each event not just to the one that came before it, but cryptographically. Add two new columns to your event table: one storing the hash of the previous event’s contents, and another storing the hash of the current event’s payload. Then define the hash as a deterministic function of both the previous hash and the current event’s data.

Here’s how it works in sequence:

  • Event #1: AccountOpened with previous hash set to a genesis value (00000…) and current hash 70be4f…
  • Event #2: AmountDeposited with previous hash 70be4f… and current hash 796018…
  • Event #3: AmountWithdrawn with previous hash 796018… and current hash 6a0260…

The hash is computed as:

SHA-256(previousHash || JSON(payload))

This isn’t exotic cryptography—just a standard hash function applied consistently. But the dependency chain makes tampering visible. Change one event’s payload, and its hash no longer matches. Force the hash to match, and the next event’s pointer breaks. Fix that too, and the chain continues to unravel. Sooner or later, the inconsistency becomes impossible to hide.

A lightweight implementation in ~40 lines of code

Adding this protection doesn’t require a rewrite. Here’s a minimal implementation in C# that shows how the chain is built and verified:

public HashChainedEntry Append(object payload)
{
    var previousHash = _entries.Count == 0 ? GenesisHash : _entries[^1].Hash;
    var hash = ComputeHash(previousHash, payload);
    var entry = new HashChainedEntry(_entries.Count + 1, payload, previousHash, hash);
    _entries.Add(entry);
    return entry;
}

internal static byte[] ComputeHash(byte[] previousHash, object payload)
{
    var payloadJson = JsonSerializer.SerializeToUtf8Bytes(payload, payload.GetType());
    var combined = new byte[previousHash.Length + payloadJson.Length];
    Buffer.BlockCopy(previousHash, 0, combined, 0, previousHash.Length);
    Buffer.BlockCopy(payloadJson, 0, combined, previousHash.Length, payloadJson.Length);
    return SHA256.HashData(combined);
}

Verification walks the chain in reverse, recomputing hashes and comparing them to stored values:

byte[] previousHash = new byte[32]; // genesis
foreach (var entry in store.Entries)
{
    if (!ByteArraysEqual(previousHash, entry.PreviousHash))
        throw new EventStreamCorruptedException(entry.Sequence, "previous-hash pointer does not match the prior entry's hash");
    
    var recomputed = ComputeHash(previousHash, entry.Payload);
    if (!ByteArraysEqual(recomputed, entry.Hash))
        throw new EventStreamCorruptedException(entry.Sequence, "stored hash does not match a fresh re-hash of the payload (payload was modified after commit)");
    
    previousHash = entry.Hash;
}

Try to alter a deposit amount in the table and the verification fails immediately:

Event stream tampering detected at sequence #2: stored hash does not match a fresh re-hash of the payload (payload was modified after commit)

What tampering looks like—and why it fails

A few common attack patterns and how the hash chain detects them:

  • Edit one event’s payload → the re-hash no longer matches the stored hash
  • Rewrite the stored hash to match the next row → the next row’s pointer no longer matches its predecessor
  • Delete a row from the middle → the next row’s pointer doesn’t match its new neighbor
  • Insert a forged row → the pointer chain breaks at the seam

Each attempt creates a visible inconsistency. Even if an attacker modifies multiple rows, the chain will eventually show a mismatch—unless they recompute every hash after the point of compromise. But that leads to the next limitation.

The ceiling of self-anchored chains

A hash chain is a checksum, not a signature. If someone controls both the database and the verification logic, they can rewrite rows and then recalculate every hash that follows. The chain remains internally consistent, and the verifier sees no problem. That’s the honest ceiling of protecting data within your own infrastructure.

This isn’t a flaw in the hashing—it’s a limitation of trusting your own systems. The only way to break this ceiling is to anchor the chain to something outside your control.

Escaping your own walls with external anchoring

That’s where anchoring comes in. Systems like Stratara maintain a second table of “anchors”—checkpoints that record the head of the hash chain at specific intervals. Each anchor includes a BlockchainTxHash column that acts as a cryptographic hook.

The anchor is then committed to a trusted external source:

  • A public blockchain
  • An RFC 3161 timestamp authority
  • An OpenTimestamps calendar
  • A notary service

Once anchored, the chain’s integrity no longer depends solely on your database. Even if every row and hash is rewritten, the external anchor remains unchanged. Verification shifts from “is this chain internally consistent?” to “does it still match what we committed externally?” That second question is much harder to fake.

Importantly, the anchoring mechanism itself isn’t magic. The sample implementation includes the anchor table and a worker that writes anchors—but it doesn’t automatically push them to a blockchain. That part is left to you, just like choosing your message broker or storage layer. The sample runs entirely in memory so you can see the structure without external dependencies.

One caveat: ownership of the pipeline

If an attacker controls both your database and your anchoring pipeline, they can still forge consistency. The defense only holds if the anchoring destination is genuinely out of their hands. That’s the entire reason to use something external.

Performance and verification strategy

Hashing runs in a background worker, not inline with every write, so appends stay fast. The chain fills in a small delay after the commit. Verification is intentional—scheduled jobs or anchor checks—not part of the read path. You don’t want to slow down every query with a full chain walk.

In short: hash chains don’t prevent tampering, but they make it impossible to hide. And when combined with external anchoring, they turn event sourcing from “mostly immutable” into “provably tamper-evident.”

AI summary

Etkinlik kaynağı (event sourcing) ile verilerinizi değiştirilemez hale getirmek mümkün mü? Hash zinciri ve dış köklendirme yöntemleriyle verilerinizin güvenliğini nasıl artırabilirsiniz.

Comments

00
LEAVE A COMMENT
ID #DXOWQY

0 / 1200 CHARACTERS

Human check

9 + 8 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.