Build your own verifier
The specification is written so a third party can implement an independent verifier — conformance vectors, the mistakes the format is designed to catch, and where reimplementations actually fail.
The operator's verifier verifying the operator's signatures proves nothing about implementability. If you want that proof, implement your own — this is encouraged, and the specification exists for it: precise enough to implement without reading the operator's code, with a way for an implementor to find out whether they got it right.
A useful calibration: a signature verifier fits in about a hundred lines of Python. An independent reimplementation at that scale was used in an internal audit of the linkex.ai instance — and surfaced documentation gaps that only an independent implementation could have exposed, which were then fixed in the spec. That is the feedback loop working as designed.
The conformance vectors
Every rule an implementation can plausibly get wrong has a vector: the exact input bytes, the exact expected output bytes, and their SHA-256. An implementation conforms when:
- every accept case produces exactly the stated canonical bytes and hash;
- every reject case is refused, not repaired — this is the condition a
JSON.parse-based verifier fails, because mainstream parsers silently repair the very inputs the format rejects; - every published section payload hashes to the commitment in its envelope, and the envelope's own digest matches.
The suite includes a chained run of receipts with a key-rotation history and the key resolutions it implies — and it publishes a fixed private-key seed on purpose: without one, an implementor can verify the reference signatures but cannot check that their own signing produces the same bytes, and signing is the half where a domain-tag mistake hides silently (a wrong tag still yields a valid-looking signature that only fails against someone else's verifier).
The reference implementation is itself cross-checked against a second, independent implementation on every accept vector.
Where reimplementations actually fail
These are the named traps — each one a place where two implementations that both look correct diverge:
Canonicalization
- Object keys sort by UTF-16 code unit, not by code point and not by UTF-8 byte. An emoji (a surrogate pair) sorts before many lower code points; Go's native string comparison and Rust's
Ordare both code-point order, and both wrong here. - String escaping follows ECMAScript
JSON.stringify: seven short escapes, lowercase\u00xxfor other C0 controls — and no escaping of<,>,&, U+2028/U+2029. Go'sencoding/jsonHTML-escapes by default and always escapes U+2028/U+2029; a Go implementation reusing the standard encoder fails, which is why the reference implementation writes its own string emitter. - Four rules must be checked on the raw input bytes, before any parser runs: duplicate keys, unpaired surrogate escapes, malformed UTF-8, trailing data. Parsers resolve all four silently, so validating the parsed value passes everything the format is designed to reject.
Hashing and signing
- The hash domain and the signing domain are different things. A commitment is hashed under its own domain tag but signed under the receipt signing tag — what is signed is always a 32-byte hash the hashing domain already made unambiguous.
- The
hashandsignaturefields are not part of the hashed bytes. They travel alongside the object, for the same reason a receipt'sself_hashisn't inside its own envelope. - Reject non-canonical base64. Most decoders ignore the unused trailing bits of the final character, so a 64-byte signature has 16 distinct base64 spellings that decode identically and all verify (a 32-byte key has 4). Re-encode the decoded bytes and compare against the input — that is the entire check. It stops being cosmetic the moment a signature string is itself hashed, as it is in a Merkle leaf's receipt row: sixteen spellings would be sixteen different leaves for one receipt.
Merkle
- Odd nodes are promoted, never duplicated. The vectors publish both the correct root and the CVE-2012-2459-shaped wrong one for a three-leaf tree, because the wrong root looks perfectly reasonable on its own.
- Node hashing is not commutative; proofs record the sibling's side.
Sampling
Four details of the draw that no prose description would pin down, all normative:
seqis encoded as eight big-endian bytes — not decimal text, not JSON;- the HMAC key is the seed's raw 32 bytes (hex-decode the part after
sha256:) — keying with the display string produces a different, self-consistent sample, the worst kind of disagreement because both sides think they're right; - only the first 8 bytes of the HMAC are compared, big-endian unsigned;
- the threshold is computed in IEEE-754 binary64 with a stated multiplier and truncation — not because floating point is elegant, but because a rule two correct implementations can answer differently is not a rule, so one arithmetic had to be named.
What you need
| Input | Where |
|---|---|
| The receipt specification | delivered to counterparties; publishing under opentallyprotocol |
Conformance vectors (canonical_vectors.json) | ships with the specification |
| The anchoring specification | served live: /api/anchor/spec — hash the exact bytes to reproduce the spec_hash that goes on-chain |
| Live data to test against | the public ledger endpoints, no account needed |
Start with canonicalization against the vectors, then signature verification over the live key history and a real bundle. By the time your implementation reproduces the reference hashes on every vector and verifies live production receipts, you have something better than trust in anyone's binary: an independent check of the books, in code you read.
Proof bundles and sampling
The artifacts handed over when questions arise — single-dispute bundles, period bundles, deterministic sampling with stated miss rates, and full recomputation.
linkex.ai, the first Open Tally instance
The production deployment — what the gateway is, and the engineering that makes "what was recorded is what happened" credible.