EXIT evidence log - specification, version 1.2 (2026-10-27: daily anchoring; private records) 1. Records A record is a JSON object with exactly these fields: kind research_observation, measurement_result, case_note or service_answer chain solana-mainnet or ethereum-mainnet subject the address the record is about: Solana base58, or 0x followed by 40 lowercase hex digits observed_at the chain position the evidence was read at: a Solana slot or an Ethereum block number known_at the UTC time EXIT acquired the evidence (ISO 8601 with +00:00). This is EXIT's own statement; an anchor bounds it from above, nothing bounds it from below. evidence the sorted, distinct SHA-256 digests (hex) of the exact evidence bytes, for example raw RPC responses result_sha256 optional: the SHA-256 digest of the result EXIT derived from that evidence There is no free text and no user data in a record. 2. Leaf d = SHA-256("EXIT-LEAF-v1" || salt || canonical_json(record)) salt is 16 random bytes per record. canonical_json is JSON with sorted keys, the separators "," and ":" with no spaces, UTF-8, and no NaN. The salt stops anyone from testing a leaf against a guessed record. Whoever receives a record's proof bundle receives its salt. 3. Tree: RFC 6962 (RFC 9162, section 2.1), with H = SHA-256 The leaf hash is H(0x00 || d), the node hash is H(0x01 || left || right), and a tree of n leaves is split at the largest power of two below n. Historical v1 appends followed known_at order. New roots carry ordering=append-sequence/2 in roots.json: append position is insertion order, and a later entry may state an earlier known_at without backdating its inclusion. That field is EXIT's declaration; it is not part of the anchored memo. A reader can check directly in leaves.json whether known_at is monotonic in any prefix, and inclusion is bounded only by the block time of the first anchor whose root contains the leaf. The log is append-only: roots.json gives, for each root, an RFC 9162 consistency proof (consistency_from_prev) showing that the tree extends the previous root's tree without removing or changing any leaf. 4. Anchor A Solana transaction with one SPL Memo instruction whose text is EXIT-ROOT-v1 The last field is the size of the previous root in roots.json, the root that consistency_from_prev starts from. It does not refer to a previous anchor. The first anchor (tree size 68) names 66, the log's first root, which was never anchored itself. The transaction is signed by a notary key listed in notaries.json. An anchor proves that its root existed no later than the transaction's block time, nothing more. Notary keys. notaries.json lists the keys that may sign anchors. A second copy is kept apart from this log, in the public repository: https://github.com/ExitEvidenceLayer/exit-protocol-public/blob/main/NOTARIES.md. A key is added or retired only by (1) a commit to that file, (2) an anchored memo EXIT-NOTARY-ADD-v1 or EXIT-NOTARY-RETIRE-v1 signed by a key that is already listed and not retired, and (3) the updated notaries.json. The two keys listed on 28 October 2026 are the initial set. An anchor signed by a key outside this chain is not an EXIT anchor; retiring a key does not invalidate the anchors it signed while it was listed. exit_log_verify.py checks only that an anchor's signer is listed in notaries.json of the same log, which EXIT builds from its own anchors; that is not an authorization check. The key rule is checked against NOTARIES.md, which lists the transaction signature of every ADD and RETIRE memo. 5. Proof bundle (format exit.evidence-proof/1) {format, record, salt (hex), leaf_index, tree_size, audit_path [hex digests], root (hex), anchor {size, root, signature, slot, block_time, notary}} To verify one: recompute d from the record and salt, run RFC 9162 inclusion verification (section 2.1.3.2) up to root, then fetch the anchor transaction from any Solana provider and check its memo, its signer and its slot. exit_log_verify.py does all of this with the Python standard library only. 5a. Private records (service answers) A record of kind service_answer (a radar event, a recorded watch change or, where offered, a research answer) belongs to one account. Its record and salt are given only to that account, as log: {format: exit.log-binding/1, leaf, record, salt}. The log publishes only its leaf and known_at; The notary log keeps no record or salt after append; account-private service payloads retain their published bounded retention. Pending/quarantined/replayed private submissions are covered by the same seven-day maximum (the nightly job removes them, also on a night when a previous run's lock is still held; while the bounded drop is not mounted, its files stay sealed in the drop's image and the first run after it is mounted again removes the expired ones). Radar events keep a reserved share of each run's room (the last 16 files), so other bindings (watch changes, answers) can be NOT_BOUND ADMISSION_CAPACITY while radar events are still bound. The holder verifies it with exit_log_verify.py leaf --log : the verifier recomputes the leaf, finds it in leaves.json, computes the audit path from the published leaves (RFC 6962 PATH), checks it against the first anchored root that contains it, and with --rpc checks that anchor's transaction. Add --event to verify the actual radar event content against result_sha256; leaf-only verification proves record inclusion, not a match to a separately presented event. For an answer binding (the X-EXIT-Log-Binding header of a research answer, where offered), result_sha256 is the SHA-256 of the exact response body bytes (after any HTTP content decoding, as the service produced them): save the body bytes your HTTP client returns after content decoding and the header value, then run leaf --log --answer . result_sha256 of such a record: for a radar event, SHA-256 of the complete canonical event excluding log and delivery-only webhook_id (reassemble all multipart parts first); for a recorded watch change, SHA-256 of canonical {watch, changes}, the changes of that check without their log field. observed_at is the event's at_or_before (or the check's slot or block); evidence holds the evidence hashes before and after. 6. Files under https://exit-protocol.io/log/ (also served at https://api.exit-protocol.io/v0/log/) leaves.json every leaf in order, with its known_at roots.json every root with its consistency proof anchors.json every anchor: transaction signature, slot, block time, notary key, memo notaries.json the keys that sign anchors, each with the anchors it has signed (a key listed with none has not signed yet) proofs.json which proof bundles are published, for every anchored size proofs//<8-digit leaf index>.json SPEC.txt, exit_log_verify.py, SHA256SUMS 7. Not in this version - Service answers: when they enter the log, only their salted leaves and known_at times are public; their records, salts and proofs go only to the account that asked. proofs.json lists which bundles are published. - The evidence bytes are not published here; each record names them by SHA-256. - Anchors are made once a day at 00:30 UTC when the log has grown, signed by the server notary key listed in notaries.json (the first anchor, tree size 68, was signed by a different key, also listed there); a day without new records sends nothing. - Some records have their proof bundles held until their own publication (proofs.json names them). Their leaves are in the tree, so every root is complete. 8. What the log does not prove It does not prove that the chain state a record describes is true (check the evidence at its own slot or block), that EXIT knew something earlier than an anchor, that nothing was left out, or who controls any address. Privacy boundary: public leaf count and known_at expose activity volume and timing. Salting does not hide these metadata or guarantee anonymity against correlation. No raw evidence-byte possession follows from a hash commitment. A watch change produced by automatic polling carries log_binding_scope=RADAR_EVENT and event_id. Its proof binds the complete CHANGED event, not the watch-view row alone. Obtain the account-private event/delivery and use --event to verify that content. Pull-watch checks bind their {watch, changes} value directly. Service writes newer than 600 seconds at the daily scan wait for a later batch; inclusion in the immediately next daily root is not guaranteed. A private submission older than seven days is refused. Under append-sequence/2 a record whose known_at is earlier than the newest entry is still appended (its inclusion time is its own anchor); a record dated after the scan (the same host clock) is refused. P6.2 admission boundary: operator public records enter only through the notary-only public inbox. Service-written records are salted service_answer envelopes admitted through a separate hostile drop; they cannot create public research records. Reads and permanent private additions are bounded (128 files/run, 1 MiB/run, 128 new private leaves/UTC day with the single daily run, 10000 lifetime private leaves); capacity deferral is not inclusion. A dedicated bounded filesystem is required for the drop before activation. Source compromise may forge private submissions within these bounds; a commitment does not authenticate EXIT research. Only the chain anchor bounds insertion; a root's appended_at and a leaf's admitted_at are EXIT's own clock statements, outside the anchored memo, and record.known_at remains a statement. Existing leaf bytes, root prefixes and proof algorithms are unchanged.