Security, provenance and oversight

Proof of governed execution and the five-chain ledger

Rajesh Ranjan Mahapatra

A consequential action is governably proved only when identity, evidence, authority, execution and outcome records are independently signed and bound to the same action.

© 2026 Rajesh Ranjan Mahapatra. 33 min read

Proof of governed execution and the five-chain ledgerAll Human–Machine Convergence

Security, provenance and oversight

Proof of governed execution and the five-chain ledger

A consequential action is governably proved only when identity, evidence, authority, execution and outcome records are independently signed and bound to the same action.

TLDR

  1. Logs can all be authentic while referring to different actions, policies, evidence snapshots or retries.
  2. The join key must be a digest of the exact typed action, rather than only a trace ID or human-readable description.
  3. Five chains are required: identity, evidence, authority, execution and independently observed outcome.
  4. Signatures and append-only receipts protect records, but they do not prove truth, completeness or causal order by themselves.
  5. Release authority should depend on a machine-verifiable proof package whose failures block, contain or compensate the action.
Opening case

Five true records and one false conclusion

At 09:00, an operations agent proposes a £4,800 payment for invoice INV-742. The identity service records a valid workload credential. The policy engine records an allow decision. The payment gateway records an accepted request. The settlement ledger records a debit. An observability trace links the services. Every component has a log, and every log entry is authentic.

A week later, the supplier reports that the money arrived at the wrong beneficiary. The investigation discovers something awkward. The identity record belonged to the correct agent run. The evidence snapshot came from an earlier run, before a beneficiary change. The policy decision approved the earlier snapshot. The gateway receipt belonged to a retry whose idempotency key had changed. The debit was real, but the readback service never checked which beneficiary token received it.

No record was forged. The conclusion was forged by composition. Reviewers silently assumed that a shared trace identifier meant all records committed to the same action. It did not. A trace says that events travelled together. It does not say that each participant signed the same amount, beneficiary, purpose, evidence cut-off, policy decision, request and observed effect.

The security problem is not the absence of records. It is the absence of a verifiable join. A governed system must prove more than “something happened near these logs”. It must show that the right identity used the permitted evidence, received authority for the exact typed action, dispatched that action once, and obtained an independently verified postcondition.

This distinction matters most when an action is disputed. A conventional audit trail invites investigators to infer continuity from timestamps, names and proximity. A proof package makes continuity an explicit claim that software can reject. The goal is not to replace human judgement. It is to prevent humans from spending their judgement on questions that hashes, signatures, temporal rules and independent readback can settle deterministically.

Five evidence currents joining at one action digest Five differently patterned streams labelled identity, evidence, authority, execution and outcome converge through a narrow action-digest lock. A verified decision emerges only after all streams match. IdentityEvidenceAuthorityExecutionOutcome same typed action action_digest same time window proof accepted
Five currents, one crossing. Each chain has its own source, semantics and signer. Governance appears only where all five commit to the same action digest and satisfy the same causal window. Illustrative conceptual figure.

The audit trail that proves too little

Most distributed systems can tell a plausible story about an action. Far fewer can demonstrate that the story’s sentences refer to the same event.

Modern observability is good at correlation. W3C Trace Context standardises identifiers that travel across service boundaries, and OpenTelemetry can attach those identifiers to spans and logs.[1][2] This is invaluable for diagnosis. Yet a trace identifier is normally mutable application context. It does not commit to the amount, object, evidence snapshot or authority under which a call was made. A compromised or confused component can copy the identifier onto a different request.

Policy systems improve the picture. Open Policy Agent decision logs can record the query input, policy bundle metadata and decision identifier.[3] That proves more, but only if the input contains the exact action and if the dispatch later proves it used that decision. A log of “allow” beside a payment is not enough. The decision might concern another beneficiary, a lower amount, an earlier policy bundle or an evidence snapshot that expired before execution.

Two causal routes to the same ledger debit The upper route passes through evidence, policy and typed execution. The lower route is a direct database write. Both end at the same visible debit, showing why outcome equality does not prove governed causation. Governed route sealedevidence policyauthority typedexecution ledger debit£4,800 direct databasewrite Ungoverned route remove policy: only this route stops
Same visible state, different cause. A final-state comparison cannot distinguish governed execution from an unauthorised path. The discriminating evidence is the causally necessary chain. Illustrative causal-intervention diagram.

Why a central audit lake can still mislead

Teams often respond to fragmented logs by copying everything into one warehouse. Centralisation improves search, retention and access control, but it does not repair missing semantics. An ingestion pipeline may flatten policy bundle versions, convert event times into arrival times, discard signature envelopes, or normalise distinct identities into the same display name. The resulting table looks coherent precisely because the distinctions have been removed.

A central store also creates correlated control. If one administrator, pipeline or service account can rewrite identity, policy and execution records before they are sealed, the apparent multiplicity of witnesses collapses. The better pattern is to let each authoritative producer sign a small, typed statement before export. The audit lake may index those statements, but the verifier should validate the original commitments and their role-specific keys. Search convenience and evidential authority should not be confused.

Three claims that must remain separate

A useful design starts by separating three claims. A record integrity claim says bytes have not changed since a signer committed to them. A process conformance claim says required steps occurred in an acceptable order. An outcome truth claim says the world now satisfies a stated postcondition. Signatures help the first. Linked attestations help the second. Independent readback helps the third. None automatically supplies the others.

W3C PROV gives a domain-neutral vocabulary for entities, activities and agents, which helps represent how data or artefacts were produced.[7] in-toto link attestations bind a supply-chain step to materials, products and a signer, while SLSA provenance records how a build platform produced artefacts.[8][9] These are strong precedents. A governed action needs a related structure, but its proof must also carry policy authority, temporal entitlement, idempotency and verified postconditions.

Boundary condition

Cryptographic identity is not moral responsibility

A workload credential can show which software principal held a key. It does not settle which human, team or institution should answer for the action. The proof package should therefore distinguish executing workload, initiating actor, represented subject, approving principal and policy owner. Collapsing them into one “user” field creates a confused-deputy channel.

The argument in one pass
Chart · flow
flowchart LR
  A["Five true records and one<br/>false conclusion"] --> B["The audit trail that proves<br/>too little"]
  B --> C["Five chains, one action"]
  C --> D["Proof is a join, not a pile"]
  D -. "re-test" .-> A
  classDef first fill:#0a1424,stroke:#087A98,stroke-width:2px,color:#e6f3ff
  classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff
  class A first
  class B,C,D step
A structural route through Proof of governed execution and the five-chain ledger: from Five true records and one false conclusion to Require all five chains before the effect. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

Five chains, one action

The five-chain ledger is not one database. It is a logical proof object assembled from records whose authority comes from different systems and, ideally, different control domains.

Begin with a typed action projection. Include every property whose change would require a new decision: operation, subject, resource, material parameters, purpose, evidence-snapshot digest, requested time and idempotency key. Exclude incidental transport details. Canonicalise the projection using a specified representation, add a domain separator and hash it. JSON Canonicalization Scheme exists because cryptographic operations require a repeatable byte representation; COSE and DSSE provide established envelopes for type-bound signed content.[10][11][12]

Decisive mechanism

The action digest

The action digest is a commitment, not a correlation label. Every chain record repeats it. A verifier can therefore reject a valid policy decision, execution receipt or readback that belongs to another action. The digest must be recomputed from the supplied typed action; accepting a caller-provided digest without the action body only relocates the trust problem.

A = canonical typed action projection D = SHA-256("gexec/action/v1" || A) For each chain i and record j: Rᵢⱼ = {chain, event, D, time, previous_hash, signer, payload} Hᵢⱼ = SHA-256("gexec/record/v1" || canonical(Rᵢⱼ)) Sᵢⱼ = Sign(keyᵢ, "gexec/signature/v1" || canonical(Rᵢⱼ))

Here, A is the exact action being judged. D is its digest. Each record carries the previous record hash, creating continuity inside that chain. The signature says the chain’s issuer committed to the record. The assumption is that canonicalisation is fixed, hash and signature algorithms remain acceptable, verification keys are trusted for the stated role, and the issuer’s event-capture path is not silently bypassed.

Worked example 1 · Toy commitment

Change one field, change every admissible join

Suppose the action is “release £4,800 for INV-742 to beneficiary token 9f7a”. The canonical bytes produce digest sha256:6d4…c81. A policy record signed against 6d4…c81 cannot authorise an execution record carrying 2ab…e05, even when both display “INV-742” in a dashboard. If the amount changes to £480, the typed action changes, so the digest changes. The old authority is unusable by construction.

Authorised action£4,800 · token 9f7a
6d4…c81
Amount changed£480 · token 9f7a
2ab…e05
Beneficiary changed£4,800 · token 51bb
95c…7f2
Worked example 2 · Small computational chain

A deletion that a dashboard cannot see

Consider an authority chain with two records. Record one says policy bundle p17 allowed digest D. Record two says approver u42 confirmed decision dec-9. The second record stores the first record’s hash as previous_hash. If an exporter drops record one but keeps record two, a dashboard may still show “approved by u42”. The verifier recomputes the chain and finds that record two points to a missing predecessor.

Now suppose an attacker inserts a new policy record and reuses the old approval. The inserted record has a different hash, so the approval’s predecessor no longer matches. To make the chain valid, the attacker must also rewrite and re-sign the approval. This does not make compromise impossible, but it localises the required compromise to the authority signer rather than allowing an untrusted log pipeline to rewrite history silently.

Cutaway of a typed supplier-payment action A central action capsule contains operation, subject, resource, amount, beneficiary token, purpose, evidence snapshot and idempotency key. Arrows show that each field contributes to the action digest. typed action capsule operation: release_payment subject: vendor V-204 resource: invoice INV-742 amount: GBP 4,800 beneficiary: token 9f7a purpose + evidence digest material decision fieldsa changed field requiresa new authority decision incidental transport fieldsstay outside the capsuleunless policy depends on them sha256:6d4…c81
Anatomy of the commitment. The action projection contains fields whose variation should invalidate prior authority. Transport details stay outside unless the policy explicitly depends on them. Illustrative worked example; digest shortened for readability.

Chain 1: identity

The identity chain answers: who or what acted, on whose behalf, for which subject and purpose, in which measured environment? It can include a short-lived workload credential, runtime attestation result, initiating actor, represented subject, delegation reference and credential expiry. Entity Attestation Tokens provide a standards-track format for attested claims about an entity’s state and characteristics, but the relying party still decides how much trust to place in those claims.[6]

The chain must be valid at dispatch, rather than only at planning time. It should also expose substitutions. If a tool adapter, model runtime or execution worker changes after approval, the verifier needs a rule that decides whether the replacement remains inside the authorised equivalence class.

Chain 2: evidence

The evidence chain answers: what facts were permitted to influence the decision, from which versions, with what provenance, entitlement and cut-off? Store content hashes or selectively disclosed commitments rather than copying sensitive documents into every ledger. Record the snapshot digest, source versions, acquisition times, authorisation basis, freshness limit and redaction policy.

This chain prevents a common retrospective trick: evaluating yesterday’s action against today’s cleaner evidence. A system may reach the same conclusion after a data correction, yet the original decision remains unsupported. The proof must preserve what was knowable and authorised at the decision cut-off.

Chain 3: authority

The authority chain answers: which policy and principal allowed this exact action, under what conditions, until when? Record the policy bundle digest, query input digest, decision identifier, obligations, limits, human confirmation where required and expiry. NIST’s zero-trust model separates policy decision and enforcement functions; the five-chain design extends that separation by requiring the enforcement path to return evidence that it consumed the decision for the same action.[16]

Authority is perishable. An allow decision at 09:00 cannot be replayed at 12:00 if the policy, credential, evidence or world state has changed beyond its declared window. The dispatch must prove that all required entitlements remained live at the moment of effect.

Chain 4: execution

The execution chain answers: what typed request crossed the effect boundary, through which tool and version, with which idempotency key, and what did the executor acknowledge? Separate the request receipt from the effect receipt. A 202 response, queue acceptance or network acknowledgement does not mean the business effect occurred. Record timeout semantics and an explicit unknown-outcome state.

in-toto’s link model is useful here: materials enter a named step and products emerge, with signed metadata about command, environment and by-products.[8] For operational actions, the “product” may be an effect identifier or authoritative state transition rather than a file.

Chain 5: outcome

The outcome chain answers: what state was observed after execution, by which independent path, and did it satisfy the intended postcondition? Read back from an authoritative source rather than trusting the tool’s success message. Bind the readback to the effect identifier and action digest. Record mismatch, compensation and unresolved uncertainty.

Independence is practical, not metaphysical. The read path should not reuse the same adapter, cache, credentials and operator that produced the write. A self-authored success receipt is evidence of what the executor claims, not independent evidence of what the world became.

Worked example 3 · Realistic join

What the verifier asks before releasing £4,800

The verifier starts from the action body, not from the logs. It recomputes digest D over vendor V-204, invoice INV-742, amount, currency, beneficiary token, purpose, evidence snapshot and idempotency key. It then asks five narrow questions. Did the workload credential and actor binding name D and remain valid at dispatch? Did the sealed invoice and vendor snapshot match the evidence digest inside the action? Did policy decision dec-4821 evaluate D, remain unexpired and carry the required approval? Did the payment gateway receive D with the authorised idempotency key? Did an independent settlement read path observe the same effect identifier, amount and beneficiary?

Any “no” changes the operational response. A stale evidence snapshot triggers re-evaluation. A changed beneficiary creates a new action requiring new authority. A timeout creates an effect-unknown state, not permission to retry. A mismatched readback triggers containment or compensation. This is why the five chains are not five copies of the same audit event. Each one closes a different inference gap.

System boundary view of five proof producers and an independent verifier Five separate services produce identity, evidence, authority, execution and outcome chain records. Their signed chain heads enter a proof assembler and then an independent verifier. A transparency service can receipt the join manifest. Execution environment identity issuer / attester evidence snapshot service policy decision service typed effect gateway independent readback proof assemblerchecks chain headsbuilds join manifest independent verifier trust anchors + invariants accept, block or compensate transparency receipt proves inclusion, not truth
The ledger is a federation of witnesses. Separate producers sign chain records; an assembler locks their current heads into a join manifest; an independent verifier applies trust and semantic rules. A transparency receipt can make later deletion or equivocation harder, but cannot certify the underlying statements as true. Practitioner architecture synthesis informed by SPIFFE, RATS, in-toto, SLSA and SCITT.
Temporal window for a governed action A horizontal timeline shows evidence cutoff, identity attestation, policy decision, approval, dispatch, effect receipt and independent readback. Evidence, identity and authority validity bands must overlap dispatch. evidencecut-off identityattested policyallow approval dispatch effectreceipt readback identity credential valid evidence freshness window authority validity window all required windows overlap here unknown outcome until readback
Authority must be live at the effect boundary. Event order alone is insufficient. The evidence, identity and authority validity windows must overlap the dispatch that can create the effect. Illustrative timeline.

Proof is a join, not a pile

A folder containing five signed records is still only a folder. The verifier must test their semantic joins, their order and the independence of the witnesses.

A join manifest commits to the action digest and the current head of each chain. Hash continuity detects deletion, insertion or reordering inside a supplied chain. Signatures identify which trusted key committed to each record. The manifest’s hash locks the assembled proof package. A transparency service may then issue an inclusion receipt so later omission or equivocation becomes detectable.

Certificate Transparency demonstrates how append-only Merkle structures support public audit of logged statements.[13] COSE Receipts generalise signed proofs about verifiable data structures, including inclusion and consistency. SCITT defines an architecture for registering signed statements with transparency services and receiving verifiable receipts.[14][15] These mechanisms strengthen accountability. They do not prove that the issuer’s statement was true, that every event was logged, or that a legitimate signer was uncompromised.

Acceptance rule

Five classes of checks

A verifier should reject unless it can establish: syntactic integrity of every record; action equality across all chains; causal order and freshness; role-appropriate signatures with adequate separation; and semantic postconditions tied to the effect identifier. A green signature icon addresses only the first and part of the fourth.

Splice attack across three valid action runs Records from runs A, B and C share a trace label but carry different action digests. A dashboard groups them, while the verifier rejects the mixed package because the digests do not match. Three real runs assembled story Run A · digest 6d4…c81 identity evidence authority Run B · digest 2ab…e05 authority execution Run C · digest 95c…7f2 execution outcome trace-7731 identity from A evidence from A authority from B execution from C outcome from C digest mismatch: reject
The splice attack. Authentic records from different runs can form a persuasive dashboard story. Repeating and verifying the action digest turns the splice into a deterministic failure. Illustrative adversarial path.
State machine for governed execution and recovery States move from proposed to authorised, dispatched and then either verified, effect unknown, compensated or rejected. Retry is allowed only after reconciliation or with the same idempotency key. proposed authorised dispatched verified effect unknown compensated rejected five-chain pre-checkdispatch once readback matchestimeout / 202 compensatereconcile before retry same idempotency key
Unknown is a first-class state. A timeout does not justify a fresh dispatch. The system must reconcile by effect identifier or idempotency key, then verify or compensate. Illustrative state-transition model.
Worked example 4 · Failure counterexample

Every signature verifies, yet the package must fail

Assume all five chain signatures are cryptographically valid. The evidence record expired at 09:02, while dispatch occurred at 09:04. The authority record allowed the right digest, but its obligation required a beneficiary check that never appeared in the execution receipt. The outcome record was signed by the same gateway that performed the write. Nothing has been forged.

The package still fails three semantic tests: evidence was stale at the effect boundary, an authority obligation was not consumed, and the outcome lacked an independent observer. This counterexample matters because signature verification is often treated as the finish line. In governed execution, it is merely admission to semantic verification. A valid signature authenticates a statement; it does not make the statement sufficient for the decision at hand.

Four honest-looking failure modes

Signed but stale: the issuer committed honestly, but the claim had expired before effect. Transparent but false: a transparency service proves that a signed statement was registered, while the statement’s sensor or issuer was wrong. Complete but bypassed: the supplied package is internally perfect, yet another ungoverned interface created a second effect. Separate names, common control: five services use different hostnames but share one administrator, signing key or mutable database.

These failures suggest a disciplined language for assurance. Say exactly what was established: “the authority service signed an allow record for digest D under policy p17”, not “the action was compliant”. The latter conclusion requires the other chains, temporal constraints and the absence of a relevant bypass. Precise claims make both audits and incident response faster because they reveal which premise is missing.

Independence has degrees

Five signatures from one service account do not provide five witnesses. Neither do five microservices administered by one pipeline, sharing one key vault and one database. Independence improves when chains use separate keys, code paths, data sources, operators and failure modes. Perfect independence is rarely possible, so the proof should declare common dependencies.

The small chart below uses an illustrative index, not a failure probability. Let five chains have common-control correlation ρ. Define effective challenge paths as 1 + (5 − 1)(1 − ρ). At zero correlation, the index is five. At complete correlation, it is one. The formula merely forces an architectural conversation: where can one compromise rewrite several chains?

Illustrative decline in effective challenge paths as control correlation rises A descending line starts at five effective challenge paths when control correlation is zero and reaches one when correlation is complete. 54321 00.250.500.751.00 common-control correlation ρ effective challenge paths 1 + 4(1 − ρ)illustrative index, not risk estimate
Five records can collapse to one witness. Shared administrators, keys, pipelines or storage reduce the value of nominally separate chains. The plotted index is synthetic and exists to expose correlation, not quantify security risk. Illustrative chart using the stated formula.
Strongest limitation

The ledger cannot prove complete capture from inside itself

A perfectly valid package proves claims about the records it contains under stated trust anchors. It cannot show that no unrecorded action occurred through another path. Completeness requires architectural enforcement: all effects must cross a boundary that refuses requests without valid proof inputs and emits non-bypassable receipts. External reconciliation, transparency monitoring and asset-level invariants are needed to detect paths that evade the ledger entirely.

How the claim changes under pressure
Chart · state
stateDiagram-v2
  [*] --> Claim
  state "The identical debit" as Claim
  state "Why a central audit lake can<br/>still mislead" as Test
  state "Three claims that must remain<br/>separate" as Revision
  Claim --> Test: expose mechanism
  Test --> Decision: evidence survives
  Test --> Revision: rival survives
  Revision --> Claim: narrow or rebuild
  Decision --> [*]
The revision loop begins with The identical debit and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

Build the smallest useful verifier

The minimum viable implementation is not a blockchain, a new observability platform or a universal ontology. It is a typed action, five append-only record sequences, separate signers, a join manifest and a deterministic verifier.

Executable lab · Configured system

A supplier payment package

The runnable artefact below constructs a synthetic payment action. The identity chain attests a payment-orchestrator workload and binds an approving actor. The evidence chain seals invoice and vendor-master versions. The authority chain records a policy allow and human approval. The execution chain records dispatch and an effect receipt under one idempotency key. The outcome chain reads the settlement ledger through a separate service and verifies amount, currency, beneficiary token and effect identifier.

The verifier recomputes the action digest; checks hash continuity and signatures; requires named events; compares subjects, purpose, evidence and policy input; enforces causal order and expiry; verifies idempotency; matches execution to readback; checks signer separation; and recomputes the join manifest.

Five-chain acceptance matrix A matrix maps each chain to required evidence, decisive join, common false positive and rejection consequence. ChainRequired evidenceDecisive joinFalse positiveResponse Identitywho may actSVID / attestationactor, subject, purposevalid at dispatchsame action digestservice name onlyblock Evidencewhat informed itsnapshot + sourcescut-off, entitlementsnapshot digestfresh at dispatchcurrent data replayre-decide Authoritywhy it was allowedpolicy bundle + allowapproval + expiryinput digestobligations consumednearby “allow” logblock Executionwhat crossed boundarytyped request + toolidempotency + receiptrequest digesteffect identifierHTTP success alonereconcile Outcomewhat became trueindependent readbackpostcondition resulteffect + action digestexpected stateexecutor self-reportcompensate
The acceptance matrix. Each chain has a distinct evidential job, join condition and failure response. “More logs” is not a substitute for the missing row. Practitioner decision instrument.

A migration path that does not require a platform rewrite

Start at one irreversible boundary, not across the whole estate. Choose a single typed action such as payment release, account closure or production configuration change. Define its material fields and idempotency semantics. Add the action digest to the existing policy query, tool request and readback. This first slice already detects many splice and mutation failures.

Next, move signatures to the authoritative producers. The identity service signs actor and workload binding. The evidence service signs the snapshot commitment. The policy engine signs its decision input and obligations. The effect gateway signs dispatch and effect receipts. The read service signs the observed postcondition. Keep existing logs for diagnosis, but treat signed records as the evidential layer.

Finally, put verification in the control path. Before dispatch, validate identity, evidence and authority. After dispatch, persist an effect-unknown state until readback resolves it. Assemble the five heads, verify semantic joins, and optionally register the manifest in a transparency service. This staged route lets teams measure value before adopting more elaborate ledgers or attestation infrastructure.

Key custody, privacy and retention are proof properties

A verifier is only as credible as its trust anchors. Give each chain role a distinct key policy, rather than only a distinct key identifier. Record issuer, key version, validity interval, revocation status and algorithm. Retain enough retired public-key material to verify historical packages. Where hardware or confidential-computing attestation is used, keep the appraisal result and reference-value version, not an unexplained “trusted” boolean.

Evidence minimisation matters equally. The ledger rarely needs raw invoices, personal data or model prompts. It needs stable commitments, entitlement metadata and a retrieval path governed for audit. Selective disclosure or encrypted evidence stores can preserve later verification without turning the proof system into a second uncontrolled data lake. Retention should follow the consequence and dispute window of the action, while deletion policies should preserve non-sensitive hashes and receipts only where they remain lawful and useful.

This design should not be applied mechanically to every read or low-impact suggestion. The extra signing, storage and verification cost is justified when an action is consequential, disputed outcomes are expensive, or several organisations must rely on the record. For reversible, low-risk operations, a smaller subset may be proportionate, but the omitted inference gaps should be named rather than disguised as full proof.

Run the lab

Save the file as paper-196-five-chain-ledger-verifier.py and run it with Python 3.10 or later. The default package passes. Each tamper option introduces one failure. The demo uses HMAC keys embedded in the file so it requires no external library. That choice is intentionally unsuitable for production. Replace it with independently managed asymmetric signatures, specified envelopes and external trust anchors.

Expected pass

python paper-196-five-chain-ledger-verifier.py
All five chains join to one authorised action and verified outcome.

Splice authority

python paper-196-five-chain-ledger-verifier.py --tamper policy-splice
Rejects a validly re-signed authority record tied to another action.

Expire evidence

python paper-196-five-chain-ledger-verifier.py --tamper stale-evidence
Rejects evidence whose freshness window ended before dispatch.

Fork the retry

python paper-196-five-chain-ledger-verifier.py --tamper duplicate-key
Rejects a dispatch that changes the authorised idempotency key.

Python · five-chain proof verifier
#!/usr/bin/env python3
"""Synthetic verifier for the five-chain governed-execution ledger.

Run:
    python paper-196-five-chain-ledger-verifier.py
    python paper-196-five-chain-ledger-verifier.py --tamper policy-splice

The demo uses HMAC keys embedded in the file so it can run with the Python
standard library. Replace HMAC with independently managed public-key
signatures (for example COSE or DSSE envelopes) in a real deployment.
"""
from __future__ import annotations

import argparse
import copy
import hashlib
import hmac
import json
import sys
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Tuple

ACTION_DOMAIN = b"gexec/action/v1\x00"
RECORD_DOMAIN = b"gexec/record/v1\x00"
SIGN_DOMAIN = b"gexec/signature/v1\x00"
JOIN_DOMAIN = b"gexec/join/v1\x00"

# Deliberately visible demo secrets. Production keys must live in separate
# control domains and should be asymmetric, rotated and independently audited.
TRUST: Dict[str, Tuple[str, bytes]] = {
    "identity": ("workload-identity.example", b"demo-key-identity"),
    "evidence": ("evidence-sealer.example", b"demo-key-evidence"),
    "authority": ("policy-engine.example", b"demo-key-authority"),
    "execution": ("payment-gateway.example", b"demo-key-execution"),
    "outcome": ("ledger-readback.example", b"demo-key-outcome"),
}
REQUIRED_EVENTS = {
    "identity": {"workload_attested", "actor_bound"},
    "evidence": {"evidence_sealed"},
    "authority": {"policy_evaluated", "approval_granted"},
    "execution": {"request_dispatched", "effect_receipt"},
    "outcome": {"state_readback", "postcondition_verified"},
}


def canonical(value: Any) -> bytes:
    """Deterministic JSON for the demo; use a specified canonical format in production."""
    return json.dumps(
        value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
    ).encode("utf-8")


def digest(domain: bytes, value: Any) -> str:
    return "sha256:" + hashlib.sha256(domain + canonical(value)).hexdigest()


def sign(chain: str, body: Mapping[str, Any]) -> str:
    _, key = TRUST[chain]
    return "hmac-sha256:" + hmac.new(
        key, SIGN_DOMAIN + canonical(body), hashlib.sha256
    ).hexdigest()


def make_record(
    chain: str,
    event: str,
    action_digest: str,
    occurred_at: str,
    previous_hash: str | None,
    payload: Mapping[str, Any],
) -> Dict[str, Any]:
    signer, _ = TRUST[chain]
    body: Dict[str, Any] = {
        "schema": "gexec/record/v1",
        "chain": chain,
        "event": event,
        "action_digest": action_digest,
        "occurred_at": occurred_at,
        "previous_hash": previous_hash,
        "signer": signer,
        "payload": dict(payload),
    }
    record_hash = digest(RECORD_DOMAIN, body)
    return {**body, "record_hash": record_hash, "signature": sign(chain, body)}


def append(
    chains: MutableMapping[str, List[Dict[str, Any]]],
    chain: str,
    event: str,
    action_digest: str,
    occurred_at: str,
    payload: Mapping[str, Any],
) -> None:
    previous = chains[chain][-1]["record_hash"] if chains[chain] else None
    chains[chain].append(
        make_record(chain, event, action_digest, occurred_at, previous, payload)
    )


def build_demo() -> Dict[str, Any]:
    action: Dict[str, Any] = {
        "schema": "gexec/action/v1",
        "operation": "release_supplier_payment",
        "subject": "vendor:V-204",
        "resource": "invoice:INV-742",
        "parameters": {
            "amount_minor": 480000,
            "currency": "GBP",
            "beneficiary_token": "beneficiary:9f7a",
        },
        "purpose": "settle-approved-invoice",
        "evidence_snapshot": "sha256:8f9f3f3c5d8a1c02",
        "idempotency_key": "pay-V204-INV742-v1",
        "requested_at": "2026-08-30T09:00:00Z",
    }
    action_digest = digest(ACTION_DOMAIN, action)
    chains: Dict[str, List[Dict[str, Any]]] = {name: [] for name in TRUST}

    append(
        chains,
        "identity",
        "workload_attested",
        action_digest,
        "2026-08-30T09:00:00Z",
        {
            "workload_id": "spiffe://ops.example/agent/payment-orchestrator",
            "runtime_measurement": "sha256:runtime-71c9",
            "credential_expires_at": "2026-08-30T09:10:00Z",
        },
    )
    append(
        chains,
        "identity",
        "actor_bound",
        action_digest,
        "2026-08-30T09:00:01Z",
        {
            "actor": "user:approver-17",
            "subject": action["subject"],
            "purpose": action["purpose"],
        },
    )
    append(
        chains,
        "evidence",
        "evidence_sealed",
        action_digest,
        "2026-08-30T09:00:01Z",
        {
            "snapshot_digest": action["evidence_snapshot"],
            "cutoff_at": "2026-08-30T08:59:55Z",
            "expires_at": "2026-08-30T09:05:00Z",
            "sources": ["invoice-register:v18", "vendor-master:v42"],
        },
    )
    append(
        chains,
        "authority",
        "policy_evaluated",
        action_digest,
        "2026-08-30T09:00:02Z",
        {
            "decision_id": "dec-4821",
            "allow": True,
            "policy_digest": "sha256:policy-2026-08-17",
            "input_digest": action_digest,
            "expires_at": "2026-08-30T09:02:00Z",
        },
    )
    append(
        chains,
        "authority",
        "approval_granted",
        action_digest,
        "2026-08-30T09:00:03Z",
        {"approver": "user:approver-17", "decision_id": "dec-4821"},
    )
    append(
        chains,
        "execution",
        "request_dispatched",
        action_digest,
        "2026-08-30T09:00:04Z",
        {
            "tool": "payments-api:v3",
            "request_digest": action_digest,
            "idempotency_key": action["idempotency_key"],
        },
    )
    append(
        chains,
        "execution",
        "effect_receipt",
        action_digest,
        "2026-08-30T09:00:06Z",
        {
            "effect_id": "pay-90210",
            "accepted": True,
            "request_digest": action_digest,
            "idempotency_key": action["idempotency_key"],
        },
    )
    append(
        chains,
        "outcome",
        "state_readback",
        action_digest,
        "2026-08-30T09:00:09Z",
        {
            "effect_id": "pay-90210",
            "state": "posted",
            **action["parameters"],
            "observer": "settlement-ledger-read-api:v2",
        },
    )
    append(
        chains,
        "outcome",
        "postcondition_verified",
        action_digest,
        "2026-08-30T09:00:10Z",
        {"effect_id": "pay-90210", "matches_expected_state": True},
    )

    heads = {name: records[-1]["record_hash"] for name, records in chains.items()}
    join = {
        "schema": "gexec/join/v1",
        "action_digest": action_digest,
        "chain_heads": heads,
        "joined_at": "2026-08-30T09:00:11Z",
    }
    return {
        "action": action,
        "action_digest": action_digest,
        "chains": chains,
        "join": {**join, "join_hash": digest(JOIN_DOMAIN, join)},
    }


def parse_time(value: str) -> datetime:
    return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)


def event(package: Mapping[str, Any], chain: str, name: str) -> Mapping[str, Any]:
    for record in package["chains"][chain]:
        if record["event"] == name:
            return record
    raise KeyError(f"missing {chain}.{name}")


def verify(package: Mapping[str, Any]) -> List[str]:
    errors: List[str] = []
    expected_action_digest = digest(ACTION_DOMAIN, package.get("action"))
    if package.get("action_digest") != expected_action_digest:
        errors.append("action digest does not match the canonical action")

    chains = package.get("chains", {})
    if set(chains) != set(TRUST):
        errors.append("the package must contain exactly the five required chains")

    seen_signers = set()
    for chain, expected in TRUST.items():
        records = chains.get(chain, [])
        previous = None
        events = set()
        expected_signer, _ = expected
        for index, record in enumerate(records):
            events.add(record.get("event"))
            body = {k: v for k, v in record.items() if k not in {"record_hash", "signature"}}
            if record.get("chain") != chain:
                errors.append(f"{chain}[{index}] declares the wrong chain")
            if record.get("action_digest") != expected_action_digest:
                errors.append(f"{chain}[{index}] is spliced to another action digest")
            if record.get("previous_hash") != previous:
                errors.append(f"{chain}[{index}] breaks hash continuity")
            computed_hash = digest(RECORD_DOMAIN, body)
            if record.get("record_hash") != computed_hash:
                errors.append(f"{chain}[{index}] record hash is invalid")
            if record.get("signature") != sign(chain, body):
                errors.append(f"{chain}[{index}] signature is invalid")
            if record.get("signer") != expected_signer:
                errors.append(f"{chain}[{index}] signer is outside the trust policy")
            seen_signers.add(record.get("signer"))
            previous = record.get("record_hash")
        missing = REQUIRED_EVENTS[chain] - events
        if missing:
            errors.append(f"{chain} is missing required events: {sorted(missing)}")

    if len(seen_signers) != 5:
        errors.append("the five chains are not independently signed")

    try:
        identity = event(package, "identity", "actor_bound")
        attested = event(package, "identity", "workload_attested")
        evidence = event(package, "evidence", "evidence_sealed")
        policy = event(package, "authority", "policy_evaluated")
        approval = event(package, "authority", "approval_granted")
        dispatch = event(package, "execution", "request_dispatched")
        effect = event(package, "execution", "effect_receipt")
        readback = event(package, "outcome", "state_readback")
        verified = event(package, "outcome", "postcondition_verified")
        action = package["action"]

        if identity["payload"]["subject"] != action["subject"]:
            errors.append("identity subject does not match the action")
        if identity["payload"]["purpose"] != action["purpose"]:
            errors.append("identity purpose does not match the action")
        if evidence["payload"]["snapshot_digest"] != action["evidence_snapshot"]:
            errors.append("evidence snapshot does not match the action")
        if not policy["payload"]["allow"]:
            errors.append("policy decision did not allow the action")
        if policy["payload"]["input_digest"] != expected_action_digest:
            errors.append("policy decision evaluated a different action")
        if approval["payload"]["decision_id"] != policy["payload"]["decision_id"]:
            errors.append("human approval references another policy decision")
        if dispatch["payload"]["request_digest"] != expected_action_digest:
            errors.append("tool dispatch carries another request digest")
        if dispatch["payload"]["idempotency_key"] != action["idempotency_key"]:
            errors.append("tool dispatch changed the idempotency key")
        if effect["payload"]["idempotency_key"] != action["idempotency_key"]:
            errors.append("effect receipt changed the idempotency key")
        if effect["payload"]["effect_id"] != readback["payload"]["effect_id"]:
            errors.append("readback refers to another effect")
        for field, expected_value in action["parameters"].items():
            if readback["payload"].get(field) != expected_value:
                errors.append(f"readback field {field!r} does not match the action")
        if not verified["payload"]["matches_expected_state"]:
            errors.append("postcondition verifier rejected the observed state")

        t_attested = parse_time(attested["occurred_at"])
        t_evidence = parse_time(evidence["occurred_at"])
        t_policy = parse_time(policy["occurred_at"])
        t_approval = parse_time(approval["occurred_at"])
        t_dispatch = parse_time(dispatch["occurred_at"])
        t_effect = parse_time(effect["occurred_at"])
        t_readback = parse_time(readback["occurred_at"])
        if not (t_attested <= t_policy <= t_approval <= t_dispatch <= t_effect <= t_readback):
            errors.append("events violate the required causal time order")
        if parse_time(evidence["payload"]["cutoff_at"]) > t_dispatch:
            errors.append("evidence cutoff is after dispatch")
        if parse_time(evidence["payload"]["expires_at"]) < t_dispatch:
            errors.append("evidence was stale at dispatch")
        if parse_time(policy["payload"]["expires_at"]) < t_dispatch:
            errors.append("authority expired before dispatch")
        if parse_time(attested["payload"]["credential_expires_at"]) < t_dispatch:
            errors.append("workload identity expired before dispatch")
    except (KeyError, TypeError, ValueError) as exc:
        errors.append(f"semantic verification could not complete: {exc}")

    heads = {
        name: records[-1]["record_hash"]
        for name, records in chains.items()
        if records
    }
    join = package.get("join", {})
    join_body = {k: v for k, v in join.items() if k != "join_hash"}
    if join_body.get("action_digest") != expected_action_digest:
        errors.append("join manifest names another action")
    if join_body.get("chain_heads") != heads:
        errors.append("join manifest does not commit to the current chain heads")
    if join.get("join_hash") != digest(JOIN_DOMAIN, join_body):
        errors.append("join manifest hash is invalid")

    return errors


def apply_tamper(package: Dict[str, Any], mode: str) -> None:
    if mode == "none":
        return
    if mode == "unsigned-byte":
        event(package, "outcome", "state_readback")["payload"]["amount_minor"] = 48000
        return
    if mode == "policy-splice":
        record = event(package, "authority", "policy_evaluated")
        record["action_digest"] = "sha256:" + "a" * 64
    elif mode == "stale-evidence":
        record = event(package, "evidence", "evidence_sealed")
        record["payload"]["expires_at"] = "2026-08-30T08:59:59Z"
    elif mode == "duplicate-key":
        record = event(package, "execution", "request_dispatched")
        record["payload"]["idempotency_key"] = "pay-V204-INV742-retry-2"
    elif mode == "self-readback":
        record = event(package, "outcome", "state_readback")
        record["signer"] = TRUST["execution"][0]
    else:
        raise ValueError(f"unknown tamper mode: {mode}")

    # Model a compromised producer that can rewrite its own record consistently.
    chain = record["chain"]
    body = {k: v for k, v in record.items() if k not in {"record_hash", "signature"}}
    record["record_hash"] = digest(RECORD_DOMAIN, body)
    record["signature"] = sign(chain, body)
    records = package["chains"][chain]
    index = records.index(record)
    for later in records[index + 1 :]:
        later["previous_hash"] = record["record_hash"]
        later_body = {k: v for k, v in later.items() if k not in {"record_hash", "signature"}}
        later["record_hash"] = digest(RECORD_DOMAIN, later_body)
        later["signature"] = sign(chain, later_body)
        record = later
    package["join"]["chain_heads"][chain] = records[-1]["record_hash"]
    join_body = {k: v for k, v in package["join"].items() if k != "join_hash"}
    package["join"]["join_hash"] = digest(JOIN_DOMAIN, join_body)


def main(argv: Iterable[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--tamper",
        choices=["none", "unsigned-byte", "policy-splice", "stale-evidence", "duplicate-key", "self-readback"],
        default="none",
        help="inject a failure before verification",
    )
    parser.add_argument("--json", action="store_true", help="print the evidence package")
    args = parser.parse_args(list(argv) if argv is not None else None)

    package = build_demo()
    apply_tamper(package, args.tamper)
    if args.json:
        print(json.dumps(package, indent=2, ensure_ascii=False))
    errors = verify(package)
    if errors:
        print(f"FAIL: {len(errors)} verification error(s)")
        for item in errors:
            print(f"  - {item}")
        return 1
    print("PASS: all five chains join to one authorised action and verified outcome")
    return 0


if __name__ == "__main__":
    sys.exit(main())

A pass permits one narrow conclusion: under the configured trust anchors and invariants, the supplied package is internally consistent with one authorised action and matching observed outcome. It does not prove that the signers’ keys were well protected, that no bypass path existed, that the readback source was truthful, or that all relevant business rules were encoded.

A negative result has operational meaning. A digest mismatch indicates a splice or mutation. An expired window means the action needs a fresh decision. An idempotency mismatch means the system must reconcile before retry. A self-readback failure means the outcome lacks an independent witness. The verifier should return typed failure codes so orchestration can block, contain, investigate or compensate rather than merely write an error string.

The pilot should measure more than pass rate. Record proof-generation latency, verifier latency, storage overhead, false rejection rate, time to classify injected incidents and human review effort. Compare those measures with a control condition using well-structured conventional logs. The five-chain proposal earns adoption only if it improves decisions under failure, not because its cryptography looks sophisticated.

Full tamper matrix and what each test establishes
ModeInjected conditionExpected failureWhat the failure means
unsigned-byteReadback amount changed without re-signingHash, signature and semantic mismatchByte integrity and postcondition both fail.
policy-spliceAuthority chain re-signed for another digestCross-chain action mismatchA compromised authority producer cannot splice itself into this package unnoticed.
stale-evidenceEvidence expiry moved before dispatchFreshness violationAuthentic evidence is not temporally entitled.
duplicate-keyDispatch uses a different idempotency keyExecution semantic mismatchThe retry is a new action, not continuation of the authorised one.
self-readbackOutcome record claims the execution signerSigner-role violationThe effect lacks the configured independent observation path.
Open research hypothesis

Cross-chain proof will reduce expensive manual reconstruction

The proposal predicts that systems which bind five independent chains to one typed action will require fewer human-hours to determine whether a disputed effect was authorised, duplicated, stale or mis-observed. Evidence would strengthen the claim if blinded incident teams reach correct classifications faster with proof packages than with conventional logs. It would weaken the claim if package generation creates equivalent ambiguity, or if operators routinely bypass the effect boundary. A rival explanation is simply better structured logging. The smallest useful study compares both designs on the same injected failures.

Implementation profile for a production pilot

Use a versioned action schema with explicit semantic compatibility rules. Canonicalise with JCS or deterministic CBOR. Sign records with COSE or DSSE using keys issued for one chain role. Propagate trace context for diagnosis, but never treat it as the proof join. Store chain records in append-only repositories with retention, key-rotation and revocation metadata. Create a join manifest after readback, register it with a transparency service when non-repudiation or cross-organisation audit matters, and retain the receipt with the package.

Place a policy enforcement point at the irreversible boundary. It should refuse dispatch unless identity, evidence and authority records validate for the action digest and current time. After dispatch, persist effect-unknown before returning control to the agent. Permit release from that state only through readback, idempotent reconciliation or compensation. Monitor asset-level invariants outside the proof system to detect unlogged bypasses.

The evidence conversation
Chart · sequence
sequenceDiagram
  participant Q as Question
  participant M as Mechanism
  participant E as Evidence
  participant D as Decision
  Q->>M: Five true records and one false conclusion
  M->>E: The identical debit
  E-->>M: compare rival explanations
  E->>D: Require all five chains before the effect
  D-->>Q: revise scope or act
Question, mechanism, evidence and decision exchange the burdens developed in Proof of governed execution and the five-chain ledger. Reading use: follow the relation, then return to the surrounding argument for evidence and limits.

Require all five chains before the effect

When a team asks whether an agent action is auditable, stop asking how many logs, traces or approval screens exist. Ask whether an independent verifier can reconstruct one typed action and prove five joins: the acting identity was entitled; the evidence snapshot was permitted and fresh; the exact action received live authority; the declared request crossed the effect boundary once; and a separate path verified the postcondition.

That test changes architecture reviews. A missing chain is not “future observability work”. It is a missing premise in the governance claim. A shared trace ID is not evidence that policy and execution agreed. A signed gateway receipt is not a verified outcome. A transparency receipt is not proof that the issuer told the truth. A human approval is not reusable authority for a mutated action.

Do not release consequential autonomy on the promise that incidents can be reconstructed later. Require a five-chain proof package at the effect boundary, make unknown outcomes explicit, and give the verifier authority to block, reconcile or compensate.

The five-chain ledger does not make systems infallible. It makes a narrower and more valuable improvement: it converts a persuasive operational story into a set of checkable commitments, and it makes the gaps visible before they become somebody else’s loss.

The practical threshold is simple. When one missing or mismatched premise would change whether an effect is permitted, that premise belongs in the proof path before release, not in an incident checklist after release.

Glossary

Action projection
The material fields that must remain unchanged for an existing authority decision to apply.
Action digest
A domain-separated cryptographic hash of the canonical action projection, used as the cross-chain commitment.
Chain head
The hash of the latest record in one append-only sequence, committing to that chain’s supplied history.
Effect boundary
The enforcement point beyond which a request can create an externally consequential state change.
Effect receipt
An executor’s signed acknowledgement of a request or effect identifier. It is not, by itself, independent outcome evidence.
Join manifest
A record that binds one action digest to the five current chain heads.
Readback
An observation of authoritative post-action state through a path meaningfully independent of execution.
Transparency receipt
A verifiable proof that a signed statement was registered in a declared verifiable data structure.

References

  1. W3C. Trace Context. W3C Recommendation.
  2. OpenTelemetry. Context propagation.
  3. Open Policy Agent. Decision Logs.
  4. SPIFFE. SPIFFE Identity and Verifiable Identity Document.
  5. IETF. RFC 9334: Remote ATtestation procedureS (RATS) Architecture.
  6. IETF. RFC 9711: The Entity Attestation Token (EAT).
  7. W3C. PROV-O: The PROV Ontology.
  8. in-toto. Link attestation predicate.
  9. SLSA. Build provenance specification v1.2.
  10. Secure Systems Lab. Dead Simple Signing Envelope.
  11. RFC Editor. RFC 8785: JSON Canonicalization Scheme.
  12. IETF. RFC 9052: CBOR Object Signing and Encryption structures and process.
  13. IETF. RFC 9162: Certificate Transparency Version 2.0.
  14. IETF. RFC 9942: COSE Receipts.
  15. IETF. RFC 9943: An Architecture for Trustworthy and Transparent Digital Supply Chains.
  16. NIST. SP 800-207: Zero Trust Architecture.