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?
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
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.
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
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
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
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'
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 │
└─────────────────────┘
There are five major concepts:
- Append-only records
- Cryptographic hashes
- Hash chaining
- Merkle-tree checkpoints
- 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 = ...
to be the fundamental operation.
We want:
append(record)
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..."
}
The previous_hash is the beginning of our chain.
Conceptually:
Record 99
│
│ previous_hash
▼
Record 100
│
│ previous_hash
▼
Record 101
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
Then:
record_hash = SHA-256(canonical_record)
The important word is canonical.
If JSON serialization is inconsistent, these two values:
{"a":1,"b":2}
and:
{"b":2,"a":1}
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)
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
The chain now becomes:
hash(Record 1)
↓
hash(Record 2 + hash1)
↓
hash(Record 3 + hash2)
↓
hash(Record 4 + hash3)
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
The first record uses:
previous_hash =
0000000000000000000000000000000000000000000000000000000000000000
Then:
Genesis
↓
Record 1
↓
Record 2
↓
Record 3
The genesis record can contain:
{
"version": 1,
"engine": "DerekStore",
"created_at": "...",
"hash_algorithm": "SHA-256"
}
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"
Now imagine someone modifies:
balance = 5000
to:
balance = 9000
The record's hash changes.
Verification reports:
Hash mismatch at sequence 100
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
Then recompute:
Record 100 hash
Record 101 hash
Record 102 hash
...
Eventually the entire chain is internally valid again.
The engine says:
valid
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
Hash them pairwise:
Root
/ \
H12 H34
/ \ / \
H1 H2 H3 H4
Where:
H12 = SHA256(H1 || H2)
H34 = SHA256(H3 || H4)
Root = SHA256(H12 || H34)
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]
If we have:
records = [
"aaa...",
"bbb...",
"ccc...",
"ddd..."
]
we calculate:
root = merkle_root(records)
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...
Then:
Checkpoint 2
----------------
last_sequence = 2000
merkle_root = 987654...
chain_tip = 654321...
The architecture becomes:
Records
│
├── 1 ... 1000
│ ↓
│ Merkle Root
│ ↓
│ Checkpoint 1
│
├── 1001 ... 2000
│ ↓
│ Merkle Root
│ ↓
│ Checkpoint 2
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
The checkpoint becomes:
{
"sequence": 2000,
"merkle_root": "abc...",
"chain_tip": "def...",
"signature": "..."
}
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 │
└─────────────────────────┘
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
records.log contains immutable records.
checkpoints.log contains checkpoint metadata.
metadata.json contains things like:
{
"version": 1,
"last_sequence": 1042,
"last_hash": "..."
}
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,...}
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 │
└──────────┴──────────┴───────────┴────────────┘
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
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())
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
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
The WAL itself becomes cryptographically linked.
Now recovery isn't simply:
Replay valid WAL records.
It becomes:
Replay records whose
cryptographic chain is valid.
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"]
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
The file may end with:
{"sequence":100,"key":"account:42","val
The record is incomplete.
The engine should detect the incomplete tail.
A common strategy is:
Valid records
↓
Valid records
↓
Valid records
↓
Partial record
X
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
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
An attacker removes record 3:
1
2
4
5
Record 4 expects:
previous_hash = hash(record 3)
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
becomes:
1
3
2
4
Record 3 expects:
previous_hash = hash(record 2)
but it appears after record 1.
Verification fails.
Sequence numbers add another layer:
expected sequence = 2
actual sequence = 3
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):
...
A put operation creates:
PUT
A deletion creates:
DELETE
We don't physically erase the historical operation.
Instead:
PUT account:42
PUT account:42
DELETE account:42
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
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
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
Why?
Because if:
state["balance"] = 9000
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
We cannot safely commit only one.
We need:
Transaction 500
├── DEBIT A
└── CREDIT B
The transaction itself needs an identifier:
transaction_id = tx_500
Then the journal can contain:
BEGIN tx_500
DEBIT A
CREDIT B
COMMIT tx_500
The cryptographic chain includes all of them.
Recovery only applies transactions that have a valid commit marker.
Conceptually:
BEGIN
↓
Operation
↓
Operation
↓
COMMIT
If the process crashes:
BEGIN
↓
Operation
↓
Operation
X
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
A checkpoint might contain:
{
"checkpoint_id": 200,
"first_sequence": 1000001,
"last_sequence": 2000000,
"chain_tip": "abcd...",
"merkle_root": "1234...",
"created_at": "...",
"signature": "..."
}
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
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
signs checkpoints for two years.
If Key A is compromised, an attacker may forge signatures.
Instead:
Key A
↓
Key B
↓
Key C
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
}
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
the hash protects integrity but does not provide confidentiality.
An attacker may still read:
balance = 5000
if they can access the storage file.
If confidentiality is required, combine:
Encryption
+
Authentication
+
Hash chaining
+
Signed checkpoints
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)
This is useful when:
one trusted system
needs to verify data generated by:
another trusted component
But HMAC requires sharing a secret.
Digital signatures have a useful property:
Private key → sign
Public key → verify
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 │
└─────────────────────┘
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
Each segment has:
first_sequence
last_sequence
first_hash
last_hash
merkle_root
The chain continues between segments.
Segment 1
↓
Segment 2
↓
Segment 3
↓
Segment 4
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
into:
segment 4
But tamper-evident systems have a problem.
If you delete old history, you lose evidence.
The solution is to distinguish:
operational state
from:
archival evidence
You may compact the live representation while preserving a cryptographically committed archive.
For example:
Old history
↓
Checkpoint
↓
Cold archive
↓
New compacted state
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
The verifier reconstructs:
record
↓
parent hash
↓
parent hash
↓
...
↓
Merkle root
↓
signature
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
For example:
{
"sequence": 100,
"record_hash": "abc...",
"checkpoint": 50,
"merkle_root": "def...",
"proof": [
"123...",
"456...",
"789..."
],
"signature": "..."
}
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()
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
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
Another:
For every single-bit mutation:
verify(mutated_log) == false
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)
This tests the core security invariant directly.
38. Crash Testing
The most interesting tests simulate crashes.
Imagine:
write record
flush
CRASH
Or:
write half record
CRASH
Or:
write record
fsync
CRASH
Then restart.
The invariant should remain:
Either the transaction exists completely,
or it does not exist.
Not:
maybe half of it exists.
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
Then verify that the engine:
- detects the problem
- refuses unsafe recovery
- reports enough information to diagnose it
- 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
This avoids signing every single operation.
The architecture becomes:
cheap integrity
↓
periodic aggregation
↓
expensive trust anchor
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
we can batch them.
Compute:
H1
H2
...
H1000
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
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.
If something goes wrong:
detect
stop
report
recover safely
rather than:
repair silently
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.
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.
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?
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?
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 │
└───────────────┘
And verification works in reverse:
External Checkpoint
│
▼
Verify Signature
│
▼
Verify Merkle Root
│
▼
Verify Segment
│
▼
Verify Hash Chain
│
▼
Verify Record
│
▼
TRUST
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
The tamper-evident model is closer to:
append
commit
chain
checkpoint
prove
verify
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.
it can eventually provide more than a row in a table.
It can provide:
record
↓
hash
↓
hash chain
↓
Merkle proof
↓
checkpoint
↓
digital signature
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)