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:
AccountOpenedwith previous hash set to a genesis value (00000…) and current hash70be4f… - Event #2:
AmountDepositedwith previous hash70be4f…and current hash796018… - Event #3:
AmountWithdrawnwith previous hash796018…and current hash6a0260…
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.