DEV Community

Cover image for Building a Tamper-Evident Storage Engine
Derek Mwale
Derek Mwale

Posted on

Building a Tamper-Evident Storage Engine

There is a difference between storing data and being able to prove that the data has not been silently changed.

Most storage engines are designed around questions like:

Can I write this record?
Can I read it back?
Can I update it?
Can I recover after a crash?
Enter fullscreen mode Exit fullscreen mode

Those are important questions.

But some systems need to ask another question:

Can I prove that this record has not been modified since it was committed?

That changes the architecture.

A conventional database can tell you:

transaction_id = 10492
status = completed
Enter fullscreen mode Exit fullscreen mode

A tamper-evident storage engine should be able to tell you something stronger:

transaction_id = 10492
status = completed

This record was committed at sequence 10492.
Its content was included in a cryptographic chain.
The chain continued through checkpoint 11000.
Checkpoint 11000 was signed.
The current chain is consistent with that checkpoint.
Enter fullscreen mode Exit fullscreen mode

Now the storage engine is not merely storing information.

It is preserving evidence.

That distinction is useful in systems involving:

  • financial records
  • audit trails
  • security logs
  • compliance systems
  • digital evidence
  • configuration history
  • append-only ledgers
  • infrastructure events
  • software supply-chain metadata
  • high-value business operations

The central idea is surprisingly simple:

Make every committed piece of state depend cryptographically on what came before it.

If somebody modifies an old record, the chain no longer verifies.

If somebody deletes a record, sequence continuity breaks.

If somebody inserts a fake record, the expected hash relationships no longer match.

And if we periodically sign a cryptographic checkpoint, an attacker cannot simply rewrite the entire local history without also defeating the external trust anchor.

That is the architecture we are going to build.


1. Tamper-Evident Is Not Tamper-Proof

Before writing any code, we need to make one distinction.

A tamper-evident system does not magically prevent modification.

It makes unauthorized modification detectable.

Consider a normal file:

records.db
Enter fullscreen mode Exit fullscreen mode

An administrator with sufficient operating-system privileges may be able to modify it.

A cryptographic system cannot necessarily stop that person from changing bytes.

But if those bytes participate in a hash chain:

Record 1
   ↓ hash
Record 2
   ↓ hash
Record 3
   ↓ hash
Record 4
Enter fullscreen mode Exit fullscreen mode

changing Record 2 changes its hash.

That invalidates the relationship with Record 3.

Then Record 3's hash changes.

That invalidates Record 4.

The corruption propagates through the verification chain.

The result is:

Original:

A → B → C → D → E


Tampered:

A → B' → C → D → E
       X
Enter fullscreen mode Exit fullscreen mode

The chain can detect that something is wrong.

But there is another important problem.

An attacker who controls the entire storage system could potentially rewrite:

B'
C'
D'
E'
Enter fullscreen mode Exit fullscreen mode

and recompute all hashes.

The local chain would then be internally consistent again.

This is why a serious tamper-evident storage engine needs external checkpoints.


2. The Core Architecture

Our engine will use several layers.

                    ┌─────────────────────┐
                    │      Client         │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │   Storage API       │
                    │ put / get / delete  │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │ Transaction Manager │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │ Append-Only Journal │
                    └──────────┬──────────┘
                               │
                ┌──────────────┴──────────────┐
                ▼                             ▼
       ┌─────────────────┐          ┌──────────────────┐
       │ Hash Chain      │          │ Merkle Checkpoint │
       └────────┬────────┘          └─────────┬────────┘
                │                             │
                └──────────────┬──────────────┘
                               ▼
                    ┌─────────────────────┐
                    │ Durable Storage     │
                    └─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

There are five major concepts:

  1. Append-only records
  2. Cryptographic hashes
  3. Hash chaining
  4. Merkle-tree checkpoints
  5. Signed external checkpoints

Each solves a different problem.


3. Start With an Append-Only Record

The first design decision is philosophical.

We don't want:

UPDATE record
SET value = ...
Enter fullscreen mode Exit fullscreen mode

to be the fundamental operation.

We want:

append(record)
Enter fullscreen mode Exit fullscreen mode

The previous record remains.

A record might look like:

{
  "sequence": 100,
  "timestamp": "2026-08-25T08:30:00Z",
  "operation": "PUT",
  "key": "account:42",
  "value": {
    "balance": 5000
  },
  "previous_hash": "abc123...",
  "hash": "def456..."
}
Enter fullscreen mode Exit fullscreen mode

The previous_hash is the beginning of our chain.

Conceptually:

Record 99
    │
    │ previous_hash
    ▼
Record 100
    │
    │ previous_hash
    ▼
Record 101
Enter fullscreen mode Exit fullscreen mode

Every record depends on the previous record.


4. Hashing the Record

We need a deterministic representation.

Suppose we define:

canonical_record =
    sequence
    timestamp
    operation
    key
    value
    previous_hash
Enter fullscreen mode Exit fullscreen mode

Then:

record_hash = SHA-256(canonical_record)
Enter fullscreen mode Exit fullscreen mode

The important word is canonical.

If JSON serialization is inconsistent, these two values:

{"a":1,"b":2}
Enter fullscreen mode Exit fullscreen mode

and:

{"b":2,"a":1}
Enter fullscreen mode Exit fullscreen mode

could produce different hashes even though they represent the same logical object.

So our implementation should canonicalize JSON before hashing.

For a simple prototype, Python's json.dumps(..., sort_keys=True, separators=(",", ":")) gives us deterministic serialization.


5. Building the Hash Chain

Here is the essential implementation:

import hashlib
import json
import time


def canonical_json(value):
    return json.dumps(
        value,
        sort_keys=True,
        separators=(",", ":")
    ).encode("utf-8")


def sha256(data):
    return hashlib.sha256(data).hexdigest()


def hash_record(record):
    payload = canonical_json({
        "sequence": record["sequence"],
        "timestamp": record["timestamp"],
        "operation": record["operation"],
        "key": record["key"],
        "value": record["value"],
        "previous_hash": record["previous_hash"],
    })

    return sha256(payload)
Enter fullscreen mode Exit fullscreen mode

Now we can construct records:

def create_record(sequence, operation, key, value, previous_hash):
    record = {
        "sequence": sequence,
        "timestamp": time.time_ns(),
        "operation": operation,
        "key": key,
        "value": value,
        "previous_hash": previous_hash,
    }

    record["hash"] = hash_record(record)

    return record
Enter fullscreen mode Exit fullscreen mode

The chain now becomes:

hash(Record 1)
      ↓
hash(Record 2 + hash1)
      ↓
hash(Record 3 + hash2)
      ↓
hash(Record 4 + hash3)
Enter fullscreen mode Exit fullscreen mode

This means Record 4 indirectly depends on Records 1–3.


6. The Genesis Record

Every chain needs a beginning.

We call it the genesis record.

GENESIS_HASH = "0" * 64
Enter fullscreen mode Exit fullscreen mode

The first record uses:

previous_hash =
0000000000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Then:

Genesis
   ↓
Record 1
   ↓
Record 2
   ↓
Record 3
Enter fullscreen mode Exit fullscreen mode

The genesis record can contain:

{
  "version": 1,
  "engine": "DerekStore",
  "created_at": "...",
  "hash_algorithm": "SHA-256"
}
Enter fullscreen mode Exit fullscreen mode

This makes the format self-describing.


7. Verification

A tamper-evident engine is useless if it cannot verify itself.

Verification should walk the chain.

def verify_chain(records):
    previous_hash = GENESIS_HASH

    for expected_sequence, record in enumerate(records, start=1):

        if record["sequence"] != expected_sequence:
            return False, f"Sequence mismatch at {expected_sequence}"

        if record["previous_hash"] != previous_hash:
            return False, f"Previous hash mismatch at {expected_sequence}"

        expected_hash = hash_record(record)

        if record["hash"] != expected_hash:
            return False, f"Hash mismatch at {expected_sequence}"

        previous_hash = record["hash"]

    return True, "Chain is valid"
Enter fullscreen mode Exit fullscreen mode

Now imagine someone modifies:

balance = 5000
Enter fullscreen mode Exit fullscreen mode

to:

balance = 9000
Enter fullscreen mode Exit fullscreen mode

The record's hash changes.

Verification reports:

Hash mismatch at sequence 100
Enter fullscreen mode Exit fullscreen mode

This is the first major property of our engine.


8. But a Hash Chain Alone Is Not Enough

Here is where storage-engine design becomes interesting.

Suppose an attacker controls the machine.

They modify:

Record 100
Enter fullscreen mode Exit fullscreen mode

Then recompute:

Record 100 hash
Record 101 hash
Record 102 hash
...
Enter fullscreen mode Exit fullscreen mode

Eventually the entire chain is internally valid again.

The engine says:

valid
Enter fullscreen mode Exit fullscreen mode

because it has no independent knowledge of what the chain should have looked like.

This reveals an important security principle:

Integrity verification is only as strong as the trust anchor used to verify the history.

We therefore need checkpoints.


9. Merkle Trees

Instead of verifying every historical record every time, we can periodically construct a Merkle tree.

Suppose we have four record hashes:

H1
H2
H3
H4
Enter fullscreen mode Exit fullscreen mode

Hash them pairwise:

        Root
       /    \
     H12    H34
    /  \    /  \
   H1  H2  H3  H4
Enter fullscreen mode Exit fullscreen mode

Where:

H12 = SHA256(H1 || H2)
H34 = SHA256(H3 || H4)

Root = SHA256(H12 || H34)
Enter fullscreen mode Exit fullscreen mode

The root becomes a compact cryptographic fingerprint of the entire set.


10. Merkle Tree Implementation

A simple implementation looks like this:

def hash_pair(left, right):
    return sha256(
        bytes.fromhex(left) +
        bytes.fromhex(right)
    )


def merkle_root(hashes):
    if not hashes:
        return GENESIS_HASH

    level = hashes[:]

    while len(level) > 1:
        next_level = []

        for i in range(0, len(level), 2):
            left = level[i]

            if i + 1 < len(level):
                right = level[i + 1]
            else:
                right = left

            next_level.append(hash_pair(left, right))

        level = next_level

    return level[0]
Enter fullscreen mode Exit fullscreen mode

If we have:

records = [
    "aaa...",
    "bbb...",
    "ccc...",
    "ddd..."
]
Enter fullscreen mode Exit fullscreen mode

we calculate:

root = merkle_root(records)
Enter fullscreen mode Exit fullscreen mode

The root represents the complete checkpoint.


11. Checkpoints

Instead of signing every record individually, we can periodically create:

Checkpoint 1
----------------
last_sequence = 1000
merkle_root = abcdef...
chain_tip = 123456...
Enter fullscreen mode Exit fullscreen mode

Then:

Checkpoint 2
----------------
last_sequence = 2000
merkle_root = 987654...
chain_tip = 654321...
Enter fullscreen mode Exit fullscreen mode

The architecture becomes:

Records
  │
  ├── 1 ... 1000
  │        ↓
  │   Merkle Root
  │        ↓
  │   Checkpoint 1
  │
  ├── 1001 ... 2000
  │        ↓
  │   Merkle Root
  │        ↓
  │   Checkpoint 2
Enter fullscreen mode Exit fullscreen mode

Now we have durable landmarks in history.


12. Sign the Checkpoint

A hash proves consistency.

A digital signature provides an external authenticity mechanism.

Suppose our storage engine owns a signing key:

private key
    ↓
sign(checkpoint)
    ↓
signature
Enter fullscreen mode Exit fullscreen mode

The checkpoint becomes:

{
  "sequence": 2000,
  "merkle_root": "abc...",
  "chain_tip": "def...",
  "signature": "..."
}
Enter fullscreen mode Exit fullscreen mode

Anyone with the public key can verify the signature.

Now an attacker who rewrites the local database would have to produce a new valid signature for the forged checkpoint.

Without the private key, they cannot legitimately do that.

This is the point where the architecture becomes significantly stronger.


13. The Trust Model

The security model now looks like:

                   Private Signing Key
                           │
                           ▼
                    Signed Checkpoint
                           │
                           ▼
              ┌─────────────────────────┐
              │ Local Storage Engine    │
              │                         │
              │ Record → Hash Chain     │
              │       → Merkle Root     │
              └─────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The important question becomes:

Where is the signing key stored?

If the attacker compromises the machine and steals the private key, they may be able to forge future checkpoints.

Therefore, production systems should consider:

  • hardware security modules
  • cloud KMS
  • isolated signing services
  • key rotation
  • offline root keys
  • threshold signing

The storage engine should not casually keep its most important private key beside the database.


14. A Practical Storage Engine

Now let's combine the ideas.

We can build a minimal append-only storage engine around a log file.

The on-disk architecture:

store/
├── records.log
├── checkpoints.log
└── metadata.json
Enter fullscreen mode Exit fullscreen mode

records.log contains immutable records.

checkpoints.log contains checkpoint metadata.

metadata.json contains things like:

{
  "version": 1,
  "last_sequence": 1042,
  "last_hash": "..."
}
Enter fullscreen mode Exit fullscreen mode

But metadata itself should not become an untrusted source of truth.

It can be rebuilt from the log.

That is an important design principle:

Derived metadata should be reconstructible.


15. Append-Only Record Format

For a prototype, newline-delimited JSON works:

{"sequence":1,...}
{"sequence":2,...}
{"sequence":3,...}
Enter fullscreen mode Exit fullscreen mode

However, production storage engines often use binary formats because they provide:

  • smaller records
  • faster parsing
  • explicit framing
  • checksums
  • better control over compatibility

A binary record might look like:

┌──────────┬──────────┬───────────┬────────────┐
│ Length   │ Sequence │ Type      │ Payload    │
├──────────┼──────────┼───────────┼────────────┤
│ 4 bytes  │ 8 bytes  │ 2 bytes   │ N bytes     │
└──────────┴──────────┴───────────┴────────────┘
Enter fullscreen mode Exit fullscreen mode

The record hash can then be calculated over the exact serialized bytes.

That makes the format deterministic.


16. Durable Writes Matter

A tamper-evident storage engine still needs to be a storage engine.

If you append:

Record 100
Enter fullscreen mode Exit fullscreen mode

and return success before the operating system has safely persisted it, a power failure can create ambiguity.

This is where durability mechanisms such as fsync matter.

For example:

with open("records.log", "ab") as f:
    f.write(serialized_record)
    f.flush()
    os.fsync(f.fileno())
Enter fullscreen mode Exit fullscreen mode

This doesn't magically defeat faulty hardware or every filesystem problem, but it establishes an explicit persistence barrier.

This distinction is important because integrity and durability are different properties.

A hash can prove that bytes are internally consistent.

It does not guarantee that the bytes survived a power failure.

SQLite's transaction model provides a useful real-world reference here: SQLite explicitly targets atomic, consistent, isolated, and durable transactions even across crashes and power failures, and its documentation discusses how synchronization affects durability.


17. Write-Ahead Logging

A mature storage engine can separate logical commits from physical layout.

The conceptual flow becomes:

Client
  ↓
Transaction
  ↓
WAL
  ↓ fsync
Commit
  ↓
Memtable / Index
  ↓
Immutable Segment
Enter fullscreen mode Exit fullscreen mode

This is similar to the role a write-ahead log plays in established storage systems.

SQLite's WAL mode, for example, records committed changes in a separate WAL before they are later transferred back into the main database during checkpointing. SQLite also notes that WAL permits readers to continue while changes are appended, providing snapshot-style isolation.

Our tamper-evident engine can use the same general architectural idea while adding cryptographic integrity.


18. Combining WAL With Hash Chains

Imagine:

Client
   │
   ▼
Transaction
   │
   ▼
WAL Entry
   │
   ├── payload
   ├── sequence
   ├── previous_hash
   └── hash
   │
   ▼
fsync
   │
   ▼
Committed
Enter fullscreen mode Exit fullscreen mode

The WAL itself becomes cryptographically linked.

Now recovery isn't simply:

Replay valid WAL records.
Enter fullscreen mode Exit fullscreen mode

It becomes:

Replay records whose
cryptographic chain is valid.
Enter fullscreen mode Exit fullscreen mode

During recovery:

while True:
    record = read_next()

    if not record:
        break

    if record["previous_hash"] != previous_hash:
        raise CorruptionError()

    if hash_record(record) != record["hash"]:
        raise CorruptionError()

    apply(record)

    previous_hash = record["hash"]
Enter fullscreen mode Exit fullscreen mode

The engine refuses to silently continue through corrupted history.


19. Recovery Is Part of the Security Model

Many storage engines treat recovery as a performance problem.

A tamper-evident storage engine should treat recovery as a security operation.

Suppose the process crashes halfway through writing:

Record 100
Enter fullscreen mode Exit fullscreen mode

The file may end with:

{"sequence":100,"key":"account:42","val
Enter fullscreen mode Exit fullscreen mode

The record is incomplete.

The engine should detect the incomplete tail.

A common strategy is:

Valid records
     ↓
Valid records
     ↓
Valid records
     ↓
Partial record
     X
Enter fullscreen mode Exit fullscreen mode

Recovery can truncate the incomplete tail only if the format and durability rules make that safe.

But a malformed record in the middle of the log is different.

Record 99
Record 100
CORRUPTED
Record 102
Enter fullscreen mode Exit fullscreen mode

The engine should not simply skip it.

The chain is broken.

That should produce a corruption alert.


20. Detecting Deletion

Suppose the log originally contains:

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

An attacker removes record 3:

1
2
4
5
Enter fullscreen mode Exit fullscreen mode

Record 4 expects:

previous_hash = hash(record 3)
Enter fullscreen mode Exit fullscreen mode

but record 2 is now the predecessor.

Verification fails.

This is one of the beautiful properties of a hash chain.

Deletion is detectable.

Insertion is detectable.

Modification is detectable.

Reordering is detectable.


21. Detecting Reordering

Imagine:

1
2
3
4
Enter fullscreen mode Exit fullscreen mode

becomes:

1
3
2
4
Enter fullscreen mode Exit fullscreen mode

Record 3 expects:

previous_hash = hash(record 2)
Enter fullscreen mode Exit fullscreen mode

but it appears after record 1.

Verification fails.

Sequence numbers add another layer:

expected sequence = 2
actual sequence = 3
Enter fullscreen mode Exit fullscreen mode

Now the engine has both structural and cryptographic evidence that the log has been manipulated.


22. Key-Value API

Let's expose a simple API:

class TamperEvidentStore:

    def put(self, key, value):
        ...

    def get(self, key):
        ...

    def delete(self, key):
        ...

    def verify(self):
        ...

    def checkpoint(self):
        ...
Enter fullscreen mode Exit fullscreen mode

A put operation creates:

PUT
Enter fullscreen mode Exit fullscreen mode

A deletion creates:

DELETE
Enter fullscreen mode Exit fullscreen mode

We don't physically erase the historical operation.

Instead:

PUT account:42
PUT account:42
DELETE account:42
Enter fullscreen mode Exit fullscreen mode

The current state is derived from the latest valid operation.


23. Minimal Implementation

Here is a simplified implementation:

import hashlib
import json
import os
import time


GENESIS_HASH = "0" * 64


class TamperEvidentStore:

    def __init__(self, path):
        self.path = path
        self.state = {}

        os.makedirs(os.path.dirname(path) or ".", exist_ok=True)

        if not os.path.exists(path):
            open(path, "ab").close()

        self._recover()

    def _canonical(self, value):
        return json.dumps(
            value,
            sort_keys=True,
            separators=(",", ":")
        ).encode()

    def _hash(self, record):
        payload = self._canonical({
            "sequence": record["sequence"],
            "timestamp": record["timestamp"],
            "operation": record["operation"],
            "key": record["key"],
            "value": record["value"],
            "previous_hash": record["previous_hash"],
        })

        return hashlib.sha256(payload).hexdigest()

    def _recover(self):
        self.state = {}

        previous_hash = GENESIS_HASH
        sequence = 0

        with open(self.path, "rb") as f:

            for line_number, raw in enumerate(f, start=1):

                if not raw.strip():
                    continue

                record = json.loads(raw)

                sequence += 1

                if record["sequence"] != sequence:
                    raise RuntimeError(
                        f"Sequence corruption at line {line_number}"
                    )

                if record["previous_hash"] != previous_hash:
                    raise RuntimeError(
                        f"Hash chain broken at sequence {sequence}"
                    )

                expected_hash = self._hash(record)

                if record["hash"] != expected_hash:
                    raise RuntimeError(
                        f"Record modified at sequence {sequence}"
                    )

                self._apply(record)

                previous_hash = record["hash"]

        self.sequence = sequence
        self.last_hash = previous_hash

    def _apply(self, record):
        key = record["key"]

        if record["operation"] == "PUT":
            self.state[key] = record["value"]

        elif record["operation"] == "DELETE":
            self.state.pop(key, None)

    def _append(self, operation, key, value=None):

        record = {
            "sequence": self.sequence + 1,
            "timestamp": time.time_ns(),
            "operation": operation,
            "key": key,
            "value": value,
            "previous_hash": self.last_hash,
        }

        record["hash"] = self._hash(record)

        raw = (
            self._canonical(record) +
            b"\n"
        )

        with open(self.path, "ab") as f:
            f.write(raw)
            f.flush()
            os.fsync(f.fileno())

        self._apply(record)

        self.sequence = record["sequence"]
        self.last_hash = record["hash"]

        return record

    def put(self, key, value):
        return self._append("PUT", key, value)

    def delete(self, key):
        return self._append("DELETE", key)

    def get(self, key):
        return self.state.get(key)

    def verify(self):
        try:
            self._recover()
            return True
        except Exception:
            return False
Enter fullscreen mode Exit fullscreen mode

This is deliberately simple.

It is not a production database.

But it demonstrates the architecture.

The important sequence is:

construct record
      ↓
calculate hash
      ↓
append
      ↓
flush
      ↓
fsync
      ↓
apply state
      ↓
advance chain
Enter fullscreen mode Exit fullscreen mode

24. Why We Apply State After Durability

Notice something subtle.

We do not update the in-memory state before the record has been persisted.

The order is:

append
↓
flush
↓
fsync
↓
apply
Enter fullscreen mode Exit fullscreen mode

Why?

Because if:

state["balance"] = 9000
Enter fullscreen mode Exit fullscreen mode

changes first and the process crashes before the durable write succeeds, memory says one thing while disk says another.

A storage engine should define exactly what "committed" means.

In our model:

A transaction becomes committed after its integrity-protected record has been durably appended.

This is an architectural invariant.


25. Adding Transactions

Single-record operations are not enough.

Suppose a transfer needs:

debit account A
credit account B
Enter fullscreen mode Exit fullscreen mode

We cannot safely commit only one.

We need:

Transaction 500
 ├── DEBIT A
 └── CREDIT B
Enter fullscreen mode Exit fullscreen mode

The transaction itself needs an identifier:

transaction_id = tx_500
Enter fullscreen mode Exit fullscreen mode

Then the journal can contain:

BEGIN tx_500
DEBIT A
CREDIT B
COMMIT tx_500
Enter fullscreen mode Exit fullscreen mode

The cryptographic chain includes all of them.

Recovery only applies transactions that have a valid commit marker.

Conceptually:

BEGIN
  ↓
Operation
  ↓
Operation
  ↓
COMMIT
Enter fullscreen mode Exit fullscreen mode

If the process crashes:

BEGIN
  ↓
Operation
  ↓
Operation
  X
Enter fullscreen mode Exit fullscreen mode

the transaction remains uncommitted.

Recovery can discard its effects.


26. Merkle Checkpointing at Scale

Hash-chain verification requires walking the chain.

For a billion records, doing that on every startup is expensive.

This is where checkpoints become useful.

Imagine:

Records 1–1,000,000
       ↓
Checkpoint A

Records 1,000,001–2,000,000
       ↓
Checkpoint B
Enter fullscreen mode Exit fullscreen mode

A checkpoint might contain:

{
  "checkpoint_id": 200,
  "first_sequence": 1000001,
  "last_sequence": 2000000,
  "chain_tip": "abcd...",
  "merkle_root": "1234...",
  "created_at": "...",
  "signature": "..."
}
Enter fullscreen mode Exit fullscreen mode

The engine can verify recent history quickly and use checkpoints to establish historical anchors.


27. Externalizing the Trust Anchor

This is perhaps the most important production feature.

Don't store the only trusted checkpoint beside the data it is supposed to protect.

That creates a circular trust problem.

Instead:

Storage Engine
       │
       ▼
Signed Checkpoint
       │
       ▼
External Trust Store
Enter fullscreen mode Exit fullscreen mode

The external system might be:

  • another server
  • object storage with restricted access
  • an offline archive
  • a hardware-backed key service
  • a transparency log
  • a separate administrative system

The exact implementation depends on the threat model.

The principle is:

The evidence used to detect tampering should not be trivially modifiable by the same attacker who can modify the data.


28. Key Rotation

Signing keys should not live forever.

Suppose:

Key A
Enter fullscreen mode Exit fullscreen mode

signs checkpoints for two years.

If Key A is compromised, an attacker may forge signatures.

Instead:

Key A
  ↓
Key B
  ↓
Key C
Enter fullscreen mode Exit fullscreen mode

Each key transition should itself be recorded.

A key-rotation event might say:

{
  "operation": "KEY_ROTATION",
  "old_key": "key-A",
  "new_key": "key-B",
  "sequence": 900000
}
Enter fullscreen mode Exit fullscreen mode

The new key can sign a checkpoint that references the old checkpoint.

Now the signing history becomes part of the integrity chain.


29. Why Cryptographic Hashes Are Not Encryption

Another common misunderstanding is that hashing hides data.

It doesn't.

If the record contains:

balance = 5000
Enter fullscreen mode Exit fullscreen mode

the hash protects integrity but does not provide confidentiality.

An attacker may still read:

balance = 5000
Enter fullscreen mode Exit fullscreen mode

if they can access the storage file.

If confidentiality is required, combine:

Encryption
+
Authentication
+
Hash chaining
+
Signed checkpoints
Enter fullscreen mode Exit fullscreen mode

For example, authenticated encryption such as AES-GCM or ChaCha20-Poly1305 can protect confidentiality and integrity of encrypted payloads.

The hash chain then protects the historical relationship between records.

These are different layers.


30. What About HMAC?

Sometimes the threat model doesn't require public verification.

Instead of a digital signature, a system could use an HMAC:

HMAC(secret_key, checkpoint)
Enter fullscreen mode Exit fullscreen mode

This is useful when:

one trusted system
Enter fullscreen mode Exit fullscreen mode

needs to verify data generated by:

another trusted component
Enter fullscreen mode Exit fullscreen mode

But HMAC requires sharing a secret.

Digital signatures have a useful property:

Private key → sign
Public key  → verify
Enter fullscreen mode Exit fullscreen mode

The verifier doesn't need the signing secret.

That is often better for independent audit systems.


31. Storage Engine Architecture

At this point, our engine looks like:

                    ┌─────────────────────┐
                    │      API Layer      │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │ Transaction Manager │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │     WAL / Log       │
                    │                     │
                    │ Sequence            │
                    │ Previous Hash       │
                    │ Record Hash         │
                    └──────────┬──────────┘
                               │
                         fsync │
                               ▼
                    ┌─────────────────────┐
                    │ Immutable Segments  │
                    └──────────┬──────────┘
                               │
                     checkpoint
                               ▼
                    ┌─────────────────────┐
                    │    Merkle Tree      │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │ Signed Checkpoint   │
                    └──────────┬──────────┘
                               │
                               ▼
                    ┌─────────────────────┐
                    │ External Trust      │
                    │ Anchor              │
                    └─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Now we have something that resembles an actual storage architecture.


32. Segment-Based Storage

Keeping one enormous log file forever is inconvenient.

Instead, rotate segments.

segment-000001.log
segment-000002.log
segment-000003.log
segment-000004.log
Enter fullscreen mode Exit fullscreen mode

Each segment has:

first_sequence
last_sequence
first_hash
last_hash
merkle_root
Enter fullscreen mode Exit fullscreen mode

The chain continues between segments.

Segment 1
   ↓
Segment 2
   ↓
Segment 3
   ↓
Segment 4
Enter fullscreen mode Exit fullscreen mode

This makes:

  • compaction
  • backups
  • replication
  • archival
  • parallel verification

much easier.


33. Compaction Is Dangerous

Traditional databases love compaction.

A storage engine might merge:

segment 1
segment 2
segment 3
Enter fullscreen mode Exit fullscreen mode

into:

segment 4
Enter fullscreen mode Exit fullscreen mode

But tamper-evident systems have a problem.

If you delete old history, you lose evidence.

The solution is to distinguish:

operational state
Enter fullscreen mode Exit fullscreen mode

from:

archival evidence
Enter fullscreen mode Exit fullscreen mode

You may compact the live representation while preserving a cryptographically committed archive.

For example:

Old history
    ↓
Checkpoint
    ↓
Cold archive
    ↓
New compacted state
Enter fullscreen mode Exit fullscreen mode

The checkpoint proves what the old history contained.


34. Proofs Instead of Full Verification

Merkle trees give us another powerful property.

Suppose someone asks:

Prove that record 8,500,221 belongs to checkpoint 9,000,000.

You don't need to send the entire database.

You can provide:

record hash
+
Merkle sibling hashes
+
Merkle root
+
signed checkpoint
Enter fullscreen mode Exit fullscreen mode

The verifier reconstructs:

record
  ↓
parent hash
  ↓
parent hash
  ↓
...
  ↓
Merkle root
  ↓
signature
Enter fullscreen mode Exit fullscreen mode

This is a compact proof.

That makes tamper-evident storage particularly interesting for distributed verification.


35. Audit APIs

Our engine could expose:

GET /records/100
GET /proofs/100
GET /checkpoints/latest
POST /verify
Enter fullscreen mode Exit fullscreen mode

For example:

{
  "sequence": 100,
  "record_hash": "abc...",
  "checkpoint": 50,
  "merkle_root": "def...",
  "proof": [
    "123...",
    "456...",
    "789..."
  ],
  "signature": "..."
}
Enter fullscreen mode Exit fullscreen mode

Now another system can independently verify the record.

The storage engine becomes a provider of verifiable history.


36. Testing the Engine

Security-sensitive storage systems require aggressive testing.

Don't only test:

put()
get()
delete()
Enter fullscreen mode Exit fullscreen mode

Test corruption.

For example:

modify byte 1
modify byte 100
delete record
duplicate record
reorder records
truncate file
replace checkpoint
change sequence number
change previous hash
change record hash
Enter fullscreen mode Exit fullscreen mode

Every unauthorized modification should produce a detectable failure.


37. Property-Based Testing

This kind of engine is particularly well suited to property-based testing.

One property is:

For every valid log:
verify(log) == true
Enter fullscreen mode Exit fullscreen mode

Another:

For every single-bit mutation:
verify(mutated_log) == false
Enter fullscreen mode Exit fullscreen mode

We can generate random records and mutate them.

Conceptually:

for _ in range(10_000):

    log = generate_valid_log()

    assert verify(log)

    corrupted = mutate_random_byte(log)

    assert not verify(corrupted)
Enter fullscreen mode Exit fullscreen mode

This tests the core security invariant directly.


38. Crash Testing

The most interesting tests simulate crashes.

Imagine:

write record
flush
CRASH
Enter fullscreen mode Exit fullscreen mode

Or:

write half record
CRASH
Enter fullscreen mode Exit fullscreen mode

Or:

write record
fsync
CRASH
Enter fullscreen mode Exit fullscreen mode

Then restart.

The invariant should remain:

Either the transaction exists completely,
or it does not exist.
Enter fullscreen mode Exit fullscreen mode

Not:

maybe half of it exists.
Enter fullscreen mode Exit fullscreen mode

Durability and recovery should therefore be tested together.


39. Fault Injection

You can deliberately introduce:

disk-full
partial-write
I/O error
checksum mismatch
missing segment
corrupted checkpoint
invalid signature
Enter fullscreen mode Exit fullscreen mode

Then verify that the engine:

  1. detects the problem
  2. refuses unsafe recovery
  3. reports enough information to diagnose it
  4. does not silently rewrite history

A secure storage engine should fail loudly when integrity is uncertain.


40. Performance Considerations

Hashing every record has a cost.

So does fsync.

So does building Merkle trees.

So does digital signing.

A production design should therefore separate the security levels.

For example:

Every record
→ SHA-256 hash

Every 10,000 records
→ Merkle checkpoint

Every checkpoint
→ digital signature

Every hour
→ external checkpoint publication
Enter fullscreen mode Exit fullscreen mode

This avoids signing every single operation.

The architecture becomes:

cheap integrity
      ↓
periodic aggregation
      ↓
expensive trust anchor
Enter fullscreen mode Exit fullscreen mode

This is a common systems-design pattern:

Use inexpensive primitives frequently and expensive primitives strategically.


41. Batching

If the workload is high-throughput:

Record 1
Record 2
Record 3
...
Record 1000
Enter fullscreen mode Exit fullscreen mode

we can batch them.

Compute:

H1
H2
...
H1000
Enter fullscreen mode Exit fullscreen mode

then build a Merkle tree.

The root is one compact commitment to the batch.

This can dramatically reduce the number of expensive operations.

However, batching introduces a latency trade-off.

Waiting for 1,000 records means a record may not receive a signed checkpoint immediately.

Therefore:

low latency → small batches
high throughput → large batches
Enter fullscreen mode Exit fullscreen mode

The correct choice depends on the application's threat model.


42. The Most Important Invariant

If I were designing this engine, I would write one invariant at the top of the codebase:

Once a record is committed, its content and position in the authenticated history must never change.

Everything else follows from this.

That means:

No silent updates.
No silent deletion.
No sequence reuse.
No hash rewriting.
No checkpoint rewriting.
No ambiguous recovery.
Enter fullscreen mode Exit fullscreen mode

If something goes wrong:

detect
stop
report
recover safely
Enter fullscreen mode Exit fullscreen mode

rather than:

repair silently
Enter fullscreen mode Exit fullscreen mode

Silent repair is dangerous when historical integrity matters.


43. What the Engine Can and Cannot Prove

This is where mature engineering requires honesty.

Our engine can prove things like:

The record matches its cryptographic commitment.

The chain has not been modified since the checkpoint.

The checkpoint was signed by the holder of the signing key.

The sequence is internally consistent.

The record belongs to a particular Merkle root.
Enter fullscreen mode Exit fullscreen mode

But it cannot automatically prove:

The person who created the record was honest.

The business transaction was legitimate.

The server itself was uncompromised.

The signing key was never stolen.

The timestamp reflects physical reality.

The original input was truthful.
Enter fullscreen mode Exit fullscreen mode

Cryptography protects integrity.

It does not create truth.

That distinction is fundamental.


44. The Bigger Architectural Lesson

A normal storage engine answers:

What is the current value?
Enter fullscreen mode Exit fullscreen mode

A tamper-evident storage engine answers:

What is the current value?

What happened before it?

Can I verify the history?

Can I prove that history has not changed?

Can another system independently verify that proof?
Enter fullscreen mode Exit fullscreen mode

That is a fundamentally different kind of storage.

The database is no longer simply an optimization for retrieving state.

It becomes a system of memory with cryptographic evidence.

And this idea has enormous potential.

Imagine a financial system where every transaction can be independently verified.

Imagine infrastructure logs where deleting an event leaves cryptographic evidence.

Imagine configuration management where you can prove exactly what configuration existed at a specific checkpoint.

Imagine an API platform where security events can be audited without trusting the machine that generated the report.

Imagine distributed systems exchanging not just data, but proofs about data.

That is where tamper-evident storage becomes much more interesting than simply adding SHA-256 to a log file.


45. Final Architecture

The complete system looks like this:

                         CLIENT
                            │
                            ▼
                    ┌───────────────┐
                    │ Storage API   │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │ Transaction   │
                    │ Manager       │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │ Append-Only   │
                    │ WAL           │
                    └───────┬───────┘
                            │
                      SHA-256 chain
                            │
                            ▼
                    ┌───────────────┐
                    │ Immutable     │
                    │ Segments      │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │ Merkle Tree   │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │ Signed        │
                    │ Checkpoint    │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │ External      │
                    │ Trust Anchor  │
                    └───────────────┘
Enter fullscreen mode Exit fullscreen mode

And verification works in reverse:

External Checkpoint
        │
        ▼
Verify Signature
        │
        ▼
Verify Merkle Root
        │
        ▼
Verify Segment
        │
        ▼
Verify Hash Chain
        │
        ▼
Verify Record
        │
        ▼
       TRUST
Enter fullscreen mode Exit fullscreen mode

That is the architecture.

Not an impenetrable database.

Not magic.

Not blockchain for the sake of blockchain.

A carefully designed storage engine where history becomes cryptographically observable.


Conclusion

Building a tamper-evident storage engine forces us to think differently about databases.

The traditional model is:

write
read
update
delete
Enter fullscreen mode Exit fullscreen mode

The tamper-evident model is closer to:

append
commit
chain
checkpoint
prove
verify
Enter fullscreen mode Exit fullscreen mode

Instead of destroying history, we preserve it.

Instead of trusting the current bytes, we verify them.

Instead of trusting the local database completely, we create cryptographic commitments.

Instead of signing every operation individually, we aggregate records into Merkle trees.

Instead of keeping the only checkpoint beside the data, we establish an external trust anchor.

Instead of silently repairing corruption, we treat unexplained inconsistency as a security event.

The result is not simply a database.

It is a verifiable history engine.

And that is the most interesting idea here.

A storage engine normally remembers.

A tamper-evident storage engine remembers and can prove that its memory has not been silently rewritten.

That property is powerful.

It means that when the system says:

Transaction 84921 happened.
Enter fullscreen mode Exit fullscreen mode

it can eventually provide more than a row in a table.

It can provide:

record
   ↓
hash
   ↓
hash chain
   ↓
Merkle proof
   ↓
checkpoint
   ↓
digital signature
Enter fullscreen mode Exit fullscreen mode

Now the statement has evidence attached to it.

That is the difference between data and verifiable data.

And as software systems increasingly manage money, identity, infrastructure, security, compliance, and digital assets, that distinction is going to matter more.

The future of storage is not only about making reads faster and writes cheaper.

It is also about making history harder to rewrite without leaving evidence.

Because sometimes the most important question a database can answer isn't:

What is stored here?

It is:

Can you prove that what is stored here is still what was originally committed?

That is where tamper-evident storage begins.

Top comments (0)