Skip to content

Audit chain

Every protocol event — grants, revocations, cease notices, certificate issuance, value access, administrative actions — is appended to a single ordered audit chain.

Each row carries the event body and two hashes:

row_hash = SHA256( prev_row_hash ‖ canonical_json(event) )
  • prev_row_hash — the previous row’s row_hash, as 64 lowercase hex chars. The concatenation is textual: the 64-char hex string immediately followed by the canonical JSON string, then UTF-8 encoded and hashed.
  • The genesis value for the first row is 64 zero characters: 000…000 (64 × 0).
  • row_hash is stored alongside the row; the pair (prev_hash, row_hash) makes each row a link.
  • Event bodies MUST NOT contain personal data — names only field names, hashes, ids, and timestamps. This keeps the full chain exportable to auditors and anchorable externally without a redaction step.
  • Bodies are hashed via canonical JSON; anything canonical JSON rejects (cycles, undefined, non-finite numbers) MUST be rejected at append time.
  • The chain MUST be append-only at the storage-permission level (the writing role holds no UPDATE/DELETE grant) and SHOULD additionally be protected by a trigger or equivalent.
  • Appends MUST be serialized (the reference implementation takes an advisory lock around head-read + append) so two concurrent writers cannot fork the chain.

To verify a slice of the chain given an expected starting prev_hash (genesis, or a previously verified row hash):

prev = expected_first_prev_hash
for each row in order:
assert row.prev_hash == prev # continuity
assert SHA256(row.prev_hash ‖ canonical_json(row.event)) == row.row_hash
prev = row.row_hash

Rewriting, inserting, or deleting any historical row breaks every subsequent row_hash.

Genesis:

prev_hash₀ = 0000000000000000000000000000000000000000000000000000000000000000

Row 1 event:

{ "action": "share.created", "actor_type": "consumer", "entity_id": "sh_0000000000000000example0" }
row_hash₁ = c0216b0d3dbdd053306dc3d0d2af1868832f3e5be408e97802c72bddc3882933

Row 2 event (with prev_hash₂ = row_hash₁):

{ "action": "share.revoked", "actor_type": "consumer", "entity_id": "sh_0000000000000000example0" }
row_hash₂ = 9a85c75b24de5aa169efb080cc49be4bfd60c26dc176e0de67a7efa2e4497d8b

A conforming verifier MUST reproduce both digests.

The current head (audit_seq, row_hash) is periodically anchored externally. A checkpoint at sequence N commits to every row ≤ N: to alter history the operator would have to break this chain and the external ledger’s Merkle consistency proofs simultaneously.