Verification Specification

Receipt Verification Specification

The exact algorithm behind every governance receipt fingerprint and Merkle inclusion proof Tork issues. Precise enough to reimplement in any language, without reading Tork's source or calling Tork's API — mathematics, not trust.

Overview

Every governed call produces a fingerprint — a short, deterministic hash of the governance decision. Fingerprints for a given org and UTC day are the leaves of a Merkle tree; the tree's root is written on-chain once a day. Given a fingerprint, its Merkle proof, and the on-chain root, anyone can confirm the fingerprint was included in that day's anchor — without asking Tork, and without trusting Tork's word for it.

There are two fingerprint schemes in use, v1 and v2. Only v2 is independently verifiable end-to-end; see v1 vs v2 below for the honest boundary between them.

1. The fingerprint (TORK-DNA-v2)

A v2 fingerprint is a salted, no-secret hash of the governance decision's canonical JSON representation:

Algorithm
digest      = SHA-256( canonical_json + "|" + salt )
fingerprint = "TORK-DNA-v2-" + first 16 hex characters of digest
  • canonical_json is a JSON object with keys in strict alphabetical order: hitl, pii, policies, risk, score, ts, and (when present) autonomy_level. No whitespace beyond what standard JSON serialization with sorted keys produces.
  • salt is 16 random bytes, rendered as 32 lowercase hex characters.
  • The digest is truncated to its first 16 hex characters — not the full 64.

Both canonical_json and salt are exactly the values used at write time, persisted verbatim. Nothing here is re-derived — you hash the literal strings you are given.

Worked example

Given this exact canonical_json string (note: this is the literal string to hash, not a value to re-serialize):

canonical_json
{"hitl":false,"pii":[],"policies":["rate-limit"],"risk":"none","score":100,"ts":1700000000,"v":"1.0.0"}

and this salt:

salt
a1b2c3d4e5f60718293a4b5c6d7e8f90

SHA-256(canonical_json + "|" + salt) is:

digest (full 64-hex SHA-256)
7127811a7a6c52cf5496b7ac0174784e5102702c9f019e8137168cb0de15aa3a

Truncated to 16 hex characters and prefixed, the fingerprint is:

fingerprint
TORK-DNA-v2-7127811a7a6c52cf

Reproduce this yourself: echo -n '{"hitl":false,"pii":[],"policies":["rate-limit"],"risk":"none","score":100,"ts":1700000000,"v":"1.0.0"}|a1b2c3d4e5f60718293a4b5c6d7e8f90' | sha256sum — take the first 16 characters of the output and prefix TORK-DNA-v2-.

2. The Merkle tree

All fingerprints for one org on one UTC day are the leaves of a single binary Merkle tree, built bottom-up:

Algorithm
leaf(fingerprint)  = SHA-256( fingerprint )
hashPair(a, b)     = SHA-256( a + b ), where a and b are ordered so that
                     the LEXICOGRAPHICALLY SMALLER hex string comes first
                     (i.e. sort the two children before concatenating)

Build the tree bottom-up from the leaf hashes, pairing adjacent nodes at
each level with hashPair() until one hash — the root — remains.

If a level has an odd number of nodes, duplicate the LAST node before
pairing (this repeats at every odd level, not just the leaf level).
This is the single most important line in this document. Because hashPair(a, b) always sorts its two inputs before concatenating, hashPair(a, b) === hashPair(b, a) for any a, b. A Merkle proof step therefore carries a position field ("left" or "right") that is informational only — it exists so a UI can show which side a sibling hash sits on, but verification does not depend on it. Correct verification always sorts the current hash and the sibling hash the same way hashPair() does, and ignores position entirely. A reimplementation that branches on position (concatenating without sorting) happens to get the right answer today only because Tork's own tree construction already sorts every pair — it is not verifying anything the position label claims to determine.

Odd leaf counts duplicate the last leaf rather than leaving it unpaired. Known caveat: because the duplicate is a real, valid preimage (the same leaf hash appears twice), this is a standard unbalanced-Merkle-tree construction, not a second-preimage weakness — but it does mean a lone trailing leaf and its duplicate produce the same proof up to that pairing point, which a verifier should not mistake for two distinct included fingerprints.

Worked example — 3 fingerprints

Leaves (fingerprints, in leaf order)
0: TORK-DNA-v2-aaaaaaaaaaaaaaaa
1: TORK-DNA-v2-bbbbbbbbbbbbbbbb
2: TORK-DNA-v2-cccccccccccccccc
Leaf hashes — SHA-256(fingerprint)
leaf(0) = e1205ccb6871b64595da93b14d9d1279435e5bb398a385e2838b025f1fba9945
leaf(1) = 37927e75c47cd0e2af7ec578e3aa2bc4164ad1b219327fddea380d4390b30532
leaf(2) = 7d03a6bb1cea3b6116dcc62f029fd8b4d551501416e39ebef0d0ceb36062d21a

3 leaves is odd, so leaf 2 is duplicated to pair with itself:

Level 1
node(0,1) = hashPair(leaf(0), leaf(1)) = cd71ca709f9b729e9cde7fbc609edf376af13eef192b3f77badb2cf1a410cdfe
node(2,2) = hashPair(leaf(2), leaf(2)) = cb0fdd8630dfb2b5333fd285436951c98d6af22abe8fa994f5d161c60154c320
Root
root = hashPair(node(0,1), node(2,2))
     = 5758f1c37e416f429c6c84e67e89d1b343880ffe607c1782cbfff8b7d0a96731

The inclusion proof for leaf 1 (TORK-DNA-v2-bbbbbbbbbbbbbbbb) is its sibling at each level, leaf-to-root:

Proof for leaf 1
[
  { hash: "e1205ccb6871b64595da93b14d9d1279435e5bb398a385e2838b025f1fba9945", position: "left" },
  { hash: "cb0fdd8630dfb2b5333fd285436951c98d6af22abe8fa994f5d161c60154c320", position: "right" }
]

To verify: start at leaf(1), and for each proof step compute hashPair(current, step.hash) — sorted, regardless of step.position — carrying the result forward. The final value must equal root. Doing that here reproduces 5758f1c37e416f429c6c84e67e89d1b343880ffe607c1782cbfff8b7d0a96731 exactly.

3. The on-chain anchor

Once a day, the Merkle root for an org's calls is written to Solana as a memo (via the SPL Memo Program), on an ordinary transaction anyone can look up.

  • From 2026-08-01 onward: the memo is the bare 64-character hex root — nothing else.
  • Before 2026-08-01 (~29 mainnet anchors): the memo carries a legacy tork:anchor: prefix before the same 64-hex root. Those transactions are immutable and will read this way forever. Both forms are valid — a verifier should accept the root with or without that prefix.

Confirming the root on Solana, without Tork

  1. Take the tx_hash (transaction signature) from your receipt or proof-export row.
  2. Look it up on any Solana explorer or RPC node you trust — https://explorer.solana.com/tx/<tx_hash>, or getTransaction against any public/mainnet RPC endpoint.
  3. Find the SPL Memo Program instruction in that transaction and read its memo text.
  4. Strip a leading tork:anchor: if present; what remains must equal the 64-hex merkle_root you were given.

None of this requires an API key, a Tork account, or Tork's cooperation — it is a public Solana transaction, readable by any RPC node.

Attested receipts — a client's claim, not Tork's decision

Every receipt above this section describes a decision Tork itself executed — the server received content, ran governance, and produced the fingerprint. POST /api/v1/attestations is different: a client SDK operating at the edge executes a governance decision locally, computes its own TORK-DNA-v2 fingerprint (no server round trip, no secret required), and reports the outcome back to Tork. Tork records it, tamper-evidently, at the time it was received — but Tork did not make the decision and cannot verify it was made correctly.

These rows are marked capture_mode: "edge" and attested_by: "client" everywhere a receipt is read back — the receipt page, receipts/list, receipts/export, proof-export, and verify-fingerprint. A row with any other combination of those two fields was executed by Tork, not merely reported to it.

What Tork verifies on an attested claim

  • The fingerprint genuinely equals sha256(canonical_json + "|" + fingerprint_salt) — the claim is internally self-consistent, not merely well-formed.
  • The claimed risk and policies inside canonical_json genuinely follow from the claimed action and PII types, using the same classification rules the server applies to its own decisions.
  • The claimed decision time (decided_at) agrees with the timestamp embedded in the fingerprinted claim, and falls within a bounded clock-skew window of when Tork received the request.
  • Only TORK-DNA-v2 fingerprints are accepted. A TORK-DNA-v1 fingerprint requires the server-held HMAC secret to compute — a client claiming v1 is claiming something definitionally impossible, and is rejected outright.

What this does not prove

None of the checks above verify that the underlying governance call was executed correctly, that the client's policy engine matched what it claims to run, or that the content it evaluated was what it says it evaluated. Tork verified only that the claim is self-consistent and that this record of it has not been altered since Tork received it — the same tamper-evidence a Merkle inclusion proof gives any other receipt, applied to a claim rather than to a server-executed decision.

attested canonical_json example
{"hitl":false,"pii":["EMAIL"],"policies":["pii-redact","rate-limit"],"risk":"low","score":100,"ts":1785931200,"v":"1.0.0"}

Idempotency: receipt_id for an attested row is deterministic — tork_rcpt_attest_{sha256(org_id|client_event_id)} — so a retried attestation with the same client_event_id collides with itself rather than creating a second row.

v1 vs v2 — the honest boundary

TORK-DNA-v1TORK-DNA-v2
AlgorithmHMAC-SHA256, keyed on a server-held secretSHA-256 of canonical_json + salt (no secret)
Merkle inclusionIndependently verifiable — the leaf hash is publicIndependently verifiable — the leaf hash is public
Uniqueness to one receiptNot guaranteed. Derived from policy-outcome fields only (hitl, pii, policies, risk, score, ts, v) — no receipt_id, org_id or payload hash. Two receipts with the same outcome in the same second produce the identical fingerprint. Measured across the live table: 973 v1 fingerprints map to more than one receipt, worst case 50-way.Effectively 1:1 — salt and the full canonical_json (including receipt_id) are part of the input.
Leaf-to-decision bindingTork's attestation only. Recomputing the fingerprint from the governance context requires the HMAC key, which only Tork holds.Independently reproducible by anyone holding canonical_json and salt — no secret, no server round trip.

In plain terms: for a v1 receipt, an auditor can confirm that a receipt with that outcome shape was anchored on the stated day (that part is math) — but not that this specific receipt is the one anchored, since a same-second, same-outcome receipt would produce an identical fingerprint. Confirming the fingerprint corresponds to the claimed decision at all additionally requires trusting Tork's re-derivation — the HMAC key is server-held. Neither gap is softened or hidden: v1 rows are always included in the bulk proof export, always labeled fingerprint_version: "v1" and independently_verifiable: false, with fingerprint_salt and fingerprint_canonical_json permanently null — there is nothing to recompute against, and there never will be for v1 rows.

Sampling a population

GET /api/v1/receipts/proof-export?org_id=…&from=YYYY-MM-DD&to=YYYY-MM-DD (authenticated, org-scoped) returns one row per governed call in the date range — receipt_id, fingerprint (+ version, salt and canonical_json when v2), leaf_index, the full Merkle proof, merkle_root, anchor_date, anchored_at, tx_hash, decision, PII types and count, and timestamp. Rows with no fingerprint, or a fingerprint that hasn't been anchored yet, are included with an honest anchor_status rather than dropped. This is the population to sample from — pull a page, pick 25-60 rows, and verify each one using nothing but this document.

Try it against a real receipt at /verify — the same endpoint and the same math described on this page, no account required.