Specification · version 1.0 · 23 September 2026
Open evidence specification
Every byte RiskRouter’s evidence commits to, written down so that anyone can check it without us, and anyone can build a second implementation. Two already exist that share no code: the JavaScript verifier and a Python one, held to identical verdicts by the repository’s tests. Anyone may implement this specification without asking us.
0 · Conventions
Conventions
- Hashes are SHA-256, written as 64 lowercase hexadecimal characters.
- Strings are UTF-8. Fields are joined with
|, with no spaces and no escaping; no field may contain|. - Timestamps inside hashed strings are UTC,
YYYY-MM-DDTHH:MM:SS.ffffffZ, six fraction digits. A reader that receives+00:00, a space instead ofT, or fewer digits must normalise to that form first. - Signatures are ECDSA over P-256 with SHA-256, encoded as the 64-byte
r||s(IEEE P1363), base64. Public keys are JWK. The published key is in the public verifier repository, key iddd4b4b394c95586a, and stays there after any rotation. - “Frozen” means: changing it would invalidate evidence people already hold, so it will never change within a version.
1 · v1 quote ledger
The quote ledger: a hash chain
1.1 Canonical form (frozen)
chain_index|id|created_at|active_vertical|components|premium|compliance_status|matrix_version|distributor_id|prev_hash
components: every component key in ascending order, askey=trueorkey=false, joined with commas. A value istrueonly if it is the JSON literaltrue.premium: exactly two decimals, such as2.00.matrix_versionanddistributor_id: empty when absent.row_hash= SHA-256 of the canonical form. The first entry’sprev_hashis 64 zeros; every other entry’s is the previous entry’srow_hash. The lastrow_hashis the head.
1.2 Attestation (frozen payload)
riskrouter-ledger-attestation|v1|as_of|entries|head_hash
The signed statement that the ledger had entries entries and that head at as_of. A verifier
rebuilds this payload from the attestation’s own fields; it never trusts a signed_payload field alone.
1.3 Export and single-entry proof
An export (riskrouter-ledger-export|v1) carries the holder’s entries in full, the
(chain_index, row_hash, prev_hash) of every entry, and the signed attestation. A single-entry proof
(riskrouter-entry-proof|v1) carries one entry, the links from it to the head, and the attestation.
What these prove, precisely: the holder’s entries are rebuilt from content; other entries are followed by
digest only and bind because anyone recomputing the full ledger must reach the signed head. A rebuilt ledger is
caught by comparing with a head recorded earlier, which is why heads are anchored (section 4).
2 · v2 evidence log
The evidence log: digests of any regulated decision, in a Merkle tree
2.1 What the client sends
The client hashes its own record and sends only record_digest and a kind tag
(^[a-z0-9][a-z0-9.-]{0,39}$). The record never leaves the client.
The client must salt the record. Hash a random value of at least 128 bits together with the record,
and keep both. A digest of a guessable record, such as “approved”, can be confirmed by anyone who guesses it.
The salted construction this specification recommends is SHA-256(salt || record), but any construction the
client can reproduce is valid: the log commits to the digest, not to how it was made.
2.2 Leaf (frozen)
leaf string = riskrouter-evidence-leaf|v2|leaf_index|created_at|distributor_id|kind|record_digest
leaf hash = SHA-256(0x00 || leaf string)
leaf_index counts from 0 with no gaps. created_at is set by the log, never by the client.
2.3 Tree
RFC 6962 / RFC 9162, unchanged: node hash = SHA-256(0x01 || left || right), with the split at the largest
power of two smaller than the number of leaves; the root of the empty tree is SHA-256 of the empty string. Inclusion
and consistency proofs are the RFC 9162 section 2.1.3 and 2.1.4 constructions, verified by the algorithms given there.
The implementation is checked against RFC 6962’s own reference roots.
2.4 Signed tree head (frozen payload)
riskrouter-evidence-head|v2|tree_size|root_hash|timestamp
Signed with the same key as v1 attestations. A verifier must take tree_size and
root_hash from the same signed head: an inclusion proof does not bind the tree size on its own.
A head with no key bound is returned unsigned and says so; it is never given a signature it does not have.
2.5 Endpoints
POST /api/v2/evidence, GET /api/v2/evidence/head, GET /api/v2/evidence/proof and
GET /api/v2/evidence/consistency, described in the API reference and
openapi.json. A consistency proof is checked against the root of a head the verifier
saved, never a root the log supplies.
3 · Witnesses
Independent witnesses
A witness is anyone who is not the operator. Each time it runs it fetches the current signed head, checks the operator’s signature, requests a consistency proof from the last head it saved, and checks that proof against the root it saved. Only if every check passes does it co-sign:
riskrouter-evidence-cosign|v2|witness_id|tree_size|root_hash|cosigned_at
If a check fails, the witness holds two heads, both signed by the operator, that cannot both be true. That pair is
evidence of a rewritten history that the operator cannot disown, produced by someone the operator does not control.
The reference witness is tools/witness.mjs; it saves the pair and exits with status 2.
There are no independent witnesses yet. How to become one is in
witnesses/README.md.
4 · Anchoring
Anchoring heads in time
Signed v1 heads are committed to OpenTimestamps calendars and upgraded to Bitcoin attestations; the 13 September 2026 head is in block 967830 and later. This bounds when a head existed, from above. It says nothing about the moment of each entry before it.
5 · Test vectors and implementations
Check your implementation
spec-vectors.json holds deterministic inputs and expected outputs for every frozen form above: timestamp normalisation, a v1 canonical form and row hash, the attestation payload, eight v2 leaves and the roots of every prefix, an inclusion proof, a consistency proof, a head payload and a cosignature payload. It is regenerated from the reference code and both verifiers must agree with it before any change ships.
- JavaScript:
tools/verify-ledger.mjs,tools/merkle.mjs,tools/witness.mjs(Node, standard library). - Python: riskrouter_verify.py (standard library only, including its own P-256 check).
- In the browser: the checker on /verify, which sends nothing anywhere.
A defect in this specification, or a disagreement between it and any implementation, is a bug we want to hear about: security explains how to report one.