Skip to content

ActionReceipt specification

Version 0.2 defines the JSON file, canonicalization rules, and checks needed to implement a compatible verifier.

An ActionReceipt records one transaction for the counterparty to keep: the action, declared authority and limits, supplied evidence, and challenge path. The specification defines how another implementation can parse the file and reproduce its local checks. Evidence from the systems that performed or observed the action is still needed to establish what happened.

The format does not decide whether the counterparty should deploy, pay, grant access, or take another consequential action. That decision belongs to a separately supplied receiver policy. The receipt preserves the transaction facts that policy is allowed to inspect.

The conformance test is a zero-dependency checker that imports no Bulla code and reproduces the specified hashes across 17 published examples.

What this specification lets a verifier check

This checks the receipt file. Evidence from the systems that performed or observed the action is still needed to establish what happened.

Technical evidence

Verification establishes only the reported digest, identity, or inclusion depth; it does not establish the truth of the underlying process.

Canonicalization: one rule

Every hash is over the canonical JSON of a value:

Canonical JSON and digest
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, four questions

  • 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 therefore the stable field an external recourse policy can reference.
  • 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 JSON file, select a golden vector, or paste a receipt your own agent emitted. The raw parser rejects duplicate members, invalid UTF-8, and oversized or deeply nested input before verification. Nothing leaves the page.

Verify a receipt

The preloaded release receipt is the displayed current artifact. It 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.

What this check cannot tell you

The browser and packaged checker agree on the published examples. That does not show how a separate implementation or live system will behave.

Technical evidence

Passing the supplied vectors establishes fixture parity, not independent implementation or live-system correctness.

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

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.

Checking inclusion in a separately trusted log

Bulla currently ships an RFC 6962 Merkle log for signed composition deeds, with inclusion and consistency proofs; its 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. ActionReceipt-aware witnessing is a private-beta implementation surface, not part of the v0.2 wire spec.

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 the issuer staying online.

What a log proof still depends on

A host's assertion about its own root is not independent grounding; the relying party must obtain or anchor the root separately.

An open format does not require one operator

The specification, schema, examples, and reference checker are public. A provider may create a compatible receipt with Bulla or another implementation. The counterparty can keep that file and check it with compatible software under its own control. No current feature establishes a complete public reputation score or a universally trusted log.

v0.2: grounding classes and coined conventions

Spec v0.2 (NORMATIVE, 2026-07-13) requires a grounding class per evidence item — self_asserted, counterparty_signed, third_party_anchored, 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.”