Skip to content

Spec

ActionReceipt v0.2 — verifiable without our source. A second implementer reads only this.

A receipt records one consequential agent action: what was done, under whose authority, within what bounds, with what recomputable verdict, and how it is contested. The wire spec is sufficient on its own — the acceptance test is a zero-dependency checker that reproduces every hash from the spec alone, with zero bulla imports, against 7 published golden vectors.

Canonicalization — the one rule

Every hash is over the canonical JSON of a value:

the whole hashing story
canonical(x) = json.dumps(x, sort_keys=True, separators=(",", ":"))   # UTF-8, no spaces
H(x)         = "sha256:" + hex(sha256(canonical(x)))

Object keys sorted, arrays in given order, no insignificant whitespace. Every hash carries the sha256: prefix so an algorithm bump is detectable. As of CANON_VERSION 2 this one rule covers every layer — including the WitnessReceipt measurement layer, which previously hashed a spaced form; a verifier accepts the legacy form and names it (a format change is a version difference, not tampering).

Four hashes — each answers one question

  • contentrecompute the verdict. The recomputable claim, free of the envelope, the clock, and the signature. Identical on any machine — this is the receipt’s identity, and the field a bond’s slash condition recomputes.
  • eventwhich occurrence. Content hash bound to the timestamp; two re-derivations of the same claim share content but differ here.
  • attestationwho vouched. Commits {content, signature, the recourse envelope} — well-defined even unsigned.
  • log_leafwhere logged. The RFC 6962 leaf of the attestation hash, ready to append to any RFC 6962 log.

Verify one now

This runs the digest rung of the reference checker in your browser — the four hashes and the modality law, recomputed locally. Load a golden vector, or paste a receipt your own agent emitted. Nothing leaves the page.

Verify a receipt

valid-release verifies to digest. tampered-evidence has one evidence hash altered — the content hash no longer recomputes. blank-remedy-anchor names a remedy with no stateful anchor, which the modality law rejects.

The recourse envelope and the modality law

mandate / remedy / retention are named views over one recourse envelope; the envelope is what the attestation hash commits. The modality law exists because recourse has no stateful respondent — the actor is gone at contest time. A verifier MUST enforce: every remedy names a non-empty verifier and a non-empty anchor (a remedy with no stateful anchor is process theater); escalate requires an authority (its anchor is the delegation chain); forum.trusted_root_ref is required (verifying against the host’s own served root is self-consistency, not recourse); and remedy rungs come from a closed set: recompute, challenge, cure, revert, slash, escalate.

Verification levels — honest about depth

A verifier reports the highest rung it reached, never a lying boolean: digest (hashes recompute, envelope re-validates — zero dependencies), attestation (the detached ed25519 signature over content verifies; skipped, not failed, when unsigned), and log_inclusion (an external inclusion proof binds the receipt to a public log — named, never faked; v0.2 carries no inline proof). When the relying party requires a signature, an adversary who alters content and recomputes the hashes still fails at attestation: the signature is over content and cannot be re-forged without the key. An unsigned receipt passes digest only — which is why a verifier reports the rung, and a relying party decides what rung an action requires.

The log, and whose root you trust

Receipts append to an RFC 6962 Merkle log with inclusion and consistency proofs in the library; the read-only wire serves /root, /inclusion, and /by-composition. The trust rule is strict: an inclusion proof counts only against a root you obtained independently of the host — your own log, or a pinned anchored checkpoint. A remote registry’s bare claim about its own root is the operator’s word, and the verifier refuses it.

bash
bulla key gen -o key.json                    # a did:key the agent signs under
bulla certify --sign comp.yaml --key key.json --output deed.json --format json
bulla registry append deed.json --log log.jsonl
bulla verify deed.json --registry log.jsonl --trusted-root <hash>
#              integrity · authenticity · anchor · inclusion

Timestamps can additionally anchor to the Bitcoin timechain via OpenTimestamps (opt-in, bulla[ots]), so an anchored record cannot be backdated and the proof survives without us staying online.

Open format, operated commons

The formats are public and stay that way; anything that emits them can be verified by anything else. The standing model behind that is an architecture decision, not a promise: any party can recompute a counterparty’s standing from published receipts alone, and records exit with their subjects. The business is operating the layer well — never holding a score the public record cannot reproduce.

v0.2: grounding classes and coined conventions

Spec v0.2 (NORMATIVE, 2026-07-13) requires a grounding class per evidence item — self_asserted</code>, <code>counterparty_signed</code>, <code>third_party_anchored</code>, <code>execution_verified — with the display rule that a receipt’s grounding is the minimum over its necessary evidence, so “recomputable” is never heard as “true.” It also adds conventions: rules two agents coin at their seam, committed inside the content hash so a coined rule can be neither altered nor silently dropped. The kind discriminator is the decidability boundary — executable definitions (a closed JSON-schema + quantum form) are recomputed by any verifier; semantic definitions are hash-pinned with a named forum and reported pinned, never “checked.”