Defining and Using Tamper-Proof DocTypes
This guide shows how to mark a DocType as append-only and tamper-proof, how the framework enforces immutability, and how to verify record continuity.
1. When to Use Tamper-Proof DocTypes
Use tamper-proof DocTypes for models that represent permanent records that must never be altered or deleted after creation:
- Financial transactions and ledger entries
- Audit logs and access history
- System event logs
2. Declaration
Inherit from TamperProofMixin and set tamper_proof = True in the inner Meta class:
from pydantic import Field
from framework_m_core.domain.base_doctype import BaseDocType
from framework_m_core.domain.mixins import TamperProofMixin
class AuditEntry(TamperProofMixin, BaseDocType):
"""Immutable audit entry."""
action: str = Field(description="Action name")
actor: str = Field(description="User or service identity")
stream_id: str = Field(default="default", description="Stream partition key")
class Meta:
table_name = "audit_entries"
tamper_proof = True
What TamperProofMixin Adds
prev_hash: str | None: Hex-encoded SHA-256 hash of the previous record in the stream partition.hash: str | None: Hex-encoded SHA-256 hash of(prev_hash + canonical_json).
Adding CryptoSignMixin for Multi-Party Dual Signing
For DocTypes that require edge node attestation and operator dual-signing (e.g. POS invoices or bank teller vouchers):
from framework_m_core.domain.mixins import CryptoSignMixin, TamperProofMixin
class FiscalInvoice(TamperProofMixin, CryptoSignMixin, BaseDocType):
invoice_number: str
grand_total: int
class Meta:
table_name = "fiscal_invoices"
tamper_proof = True
dual_sign = True
CryptoSignMixin adds non-repudiation origin and sequencing fields:
node_id&node_signature: The signing node's identity and Ed25519 digital signature.operator_id&operator_signature: The operator's WebAuthn / FIDO2 hardware signature (or shift delegation signature).sequence_no: Gapless monotonic sequence number within the stream partition.prev_outbox_hash: Cumulative outbox stream digest preventing transaction omission or reordering.
What tamper_proof = True Does
During schema synchronization (SyncSchema), SchemaMapper generates:
- Columns
prev_hash(String(64)) andhash(String(64)). - An index on
(stream_id, hash). - Database triggers preventing
UPDATEandDELETEqueries:- PostgreSQL:
prevent_{table_name}_mutation()trigger function. - SQLite:
trg_prevent_{table_name}_updateandtrg_prevent_{table_name}_deletetriggers raising an abort.
- PostgreSQL:
3. Saving Records
Persist instances using GenericRepository:
from framework_m_standard.adapters.db.generic_repository import GenericRepository
repo = GenericRepository(model=AuditEntry, table=table)
entry1 = AuditEntry(action="LOGIN", actor="alice")
saved1 = await repo.save(session, entry1)
entry2 = AuditEntry(action="LOGOUT", actor="alice")
saved2 = await repo.save(session, entry2)
TamperManager coordinates the hash chaining:
saved1.prev_hash: Initialized to"0" * 64.saved1.hash: Calculated via RFC 8785 JCS canonicalization oversaved1payload combined withprev_hash.saved2.prev_hash: Set tosaved1.hash.saved2.hash: Calculated oversaved2payload combined withsaved2.prev_hash.
4. How Mutations Are Prevented
At the Application Layer
If application code attempts to update or delete a saved instance:
saved1.action = "TAMPERED"
await repo.save(session, saved1)
# Raises: framework_m_core.exceptions.TamperProofMutationError
# Message: "Cannot update tamper_proof entity AuditEntry"
await repo.delete(session, saved1.id)
# Raises: framework_m_core.exceptions.TamperProofMutationError
# Message: "Cannot delete tamper_proof entity AuditEntry"
At the Database Layer
If an external script or administrator connects directly with SQL:
UPDATE audit_entries SET action = 'TAMPERED' WHERE id = '...';
-- PostgreSQL Output:
-- ERROR: TAMPER REJECTED: Table audit_entries is append-only and cryptographically sealed.
-- CONTEXT: PL/pgSQL function prevent_tamper_proof_mutation()
The transaction is aborted and rolled back.
5. Verifying Chain Continuity
Use the m audit CLI command to verify that no rows have been deleted, inserted out of sequence, or modified:
# Verify specific DocType across all streams
m audit verify-chain --doctype AuditEntry
# Verify specific stream partition
m audit verify-chain --doctype AuditEntry --stream default
Output when the chain is intact:
✓ AuditEntry (stream=all): 42 records verified. Chain INTACT.
If a row was modified directly while triggers were disabled:
✗ AuditEntry (stream=all): TAMPER DETECTED at record 'AUDI-0012'! Checked 12 records.