# Verifier rules

Generated from the protocol specification, section 7. For what each check means in plain words, see [Verification packs](/docs/packs).

## The checks, in order

1. Each envelope's schema, and that it names the pack's project and env. A pack with no records fails here with `no records`: an empty pack proves nothing.
2. `record_hash`.
3. The client signature, with a key of the independent agent-key set that was active at the record's `time` and not revoked before its batch's `server_time`. Without independent agent keys this check fails with `NOT INDEPENDENT: agent keys came from the pack`, unless `--trust-embedded-agent-keys` is given.
4. The chain links (`seq`, `prev`) per agent; `time` never goes back along a chain; no record's `time` is more than 300 s after its batch's `server_time`. A gap inside a chain fails. A chain that starts after seq 1 fails unless the export declared a `from` filter that is after the earliest `valid_from` of the agent's keys (otherwise every record of the agent is inside the window).
5. The Merkle proof to the batch root.
6. `batch_hash` and `batch_sig`.
7. The batch chain, and that `server_time` never goes back along it. (A gap between batches is a note: another agent's batches are not in an agent-filtered pack.)
   A `note` named `signafaide.instance_start` or `signafaide.instance_stop` (§8) passes like any record. For each start note the verifier looks at the record right before it in the agent's chain (seq - 1):
   - When that record is in the pack and is a `note` named `signafaide.instance_stop`, the start is a **clean restart**. It adds the note `a new instance continued the chain right after the earlier instance's signed stop note (a clean restart, nothing to warn about): <agent> after seq <n>, ...` and no warning.
   - Otherwise (any other record before it, or the record before it is outside the pack because the export's `from` cut the chain there) it adds the note `a new instance continued the chain, so records an earlier instance wrote but had not sent are not known: <agent> after seq <n>, ...`, so a chain with such a start is never presented as complete without saying so.

   Each note has one entry per such record, in chain order; `<n>` is the start note's seq - 1. Only the envelopes decide this (kind, name, seq, prev), never the notes' content, so it holds for hash-only records. The local `verify` (§8) applies the same rule; for a chain that begins at the start note it uses the stop note kept in `<chain>.base.json`.
8. The export statement and the heads:
   - `export_sig` verifies with a service key active at `generated` over the statement rebuilt from the pack. Any change to `project`, `env`, `generated`, `filter`, `heads` or the records (dropped, added, reordered) fails.
   - Every record is inside the filter: its agent is in `filter.agents` (when set), and `from <= time < to` (each bound when set).
   - The head signatures, in each receipt, the export `heads`, and the `--head` file. An export or client head naming a different record at a held seq fails.
   - Each export head's `server_time` is at or before `generated`, and its agent is in the filter. Every agent with records has an export head; its records never go beyond it, and end exactly at it unless `to` is set and the head's `server_time` + 300 s is at or after `to` (its record may be after `to`). An export head whose agent has no records in the pack fails.
   - A client head (`--head`) for an agent in the filter, with `server_time` at or before `generated`, applies when the agent has records in the pack or the window holds all of the agent's records (no `from`, or `from` at or before the agent's first key), unless `to` is set and the head's `server_time` + 300 s is at or after `to`. When it applies, the agent's records must reach its seq. A client head newer than `generated` is noted, not applied.
9. The drand round: its `server_time` bounds (§3.1), and its BLS signature (quicknet) when the library is available. If BLS is not available, it reports "not checked", never "pass".
10. The anchor paths, `anchor_sig`, `keys_hash` against the supplied service keys, `created` against the covered batches' `server_time`, and the anchor chain. Two different signed anchors with the same `seq` fail. A batch whose `server_time` is more than `ANCHOR_WINDOW` (2 h) before `generated` and that no anchor in the pack covers makes the check `not checked`, with the note "not timestamped". A younger one, or a pack with no anchors and no such older batch, makes the check `pending` ("not yet anchored").
11. OTS: the proof's digest is `anchor_hash` (§4), within the size limits (§4). For each block attestation (§4), the explorer is asked for the block hash at that height (`GET /block-height/{h}`) and the raw 80-byte header (`GET /block/{hash}/header`, hex). The header is trusted only when `sha256(sha256(header))`, read as a big-endian number in display byte order, equals that block hash and is at or below the target of the header's `nBits` (compact form; a negative, zero or overflowing target is invalid). The message the proof's operations lead to must equal the header's Merkle root (bytes 36 to 68, header byte order). It reports `pending` (the check's status is `pending`, and its value `pending`) while a proof is not confirmed yet, and also when the pack has no proofs because its batches wait for their anchor (check 10 `pending`); `confirmed@block` only when every attestation was checked against a trusted header; `not checked` with the claimed block when a header could not be had or trusted (`--offline`, an `http://` explorer, a failed lookup, a header that does not hash to its block or lacks its proof of work); or fails when a trusted header's Merkle root differs. Explorer requests follow no redirects.
12. When content is supplied, either in the pack or as a local content file, `content_hash = sha256(salt || JCS(content))` (§2.1). Content without its salt, or a salt without content, fails: it cannot be matched to the hash. A local content file holds JSON lines (or a JSON array) of `{envelope, content, salt}` objects, as in §8 `chain/<project>/<env>/<agent>.jsonl`; a directory means every `*.jsonl` file below it.

Two different records (or batches) with the same `seq`, or a `prev` that does not match the item before it, fail.


## Output

 one line per check group, a final result line, and exit code 0 or 1 (2 when the inputs cannot be read). Each check is `pass`, `fail`, `pending` (it ran, found nothing wrong, and waits for something that has not happened yet: an anchor, or a public timestamp that has not confirmed) or `not_checked` (it could not run). Whenever a check is pending, the text output says what pending means in one line under the checks: `pending: co-signed, waiting for the public timestamp (an anchor is made every hour and usually confirms within a few hours); never counted as passed`. Every product surface (CLI, console, audit pack, verification report, docs) uses this one word and definition for a record with no public timestamp yet. Only `pass` counts as passed: a pending or unchecked check is never counted as passed, so the result line always says how many checks passed. A pending check does not fail the pack. It is `PASS` or `FAIL`; on `PASS`, when keys were not independent, a parenthesis says why: `(agent keys taken from the pack, not independent)`, `(service keys fetched over plain http, not independent)`, `(service keys and agent keys taken from the pack, not independent)`, or, when the service checks a pack it built with its own key registry (console replay, audit packs), `(checked by the service with its own key registry; not independent)`. Then `: ` and the counts:
- on `PASS`: `<p> of 12 checks passed`, followed, when some are pending, by `, <k> pending (<names>)`, and, when some were not checked, by `, <n> not checked (<names>)`;
- on `FAIL`: `<f> of 12 checks failed (<names>)`, then `, <p> passed`, `, <k> pending` and `, <n> not checked` when not zero.

The names, in check order, are: envelope schema, record hashes, client signatures, agent chains, Merkle proofs, batch signatures, batch chain, signed heads, drand rounds, anchors, public timestamp, content hashes. The local `verify` of a client's own state (§8), while no record has a receipt, writes `nothing co-signed yet` in place of the names of the checks that need the witness (record hashes, Merkle proofs, batch signatures, batch chain, signed heads, drand rounds, anchors, public timestamp), followed by the names of any other unchecked ones. For a readable pack, `; ` and what it covers follow, from the signed statement: `export of <every agent | agent a | agents a, b>, <all records | records from F | records before T | records from F before T>, generated <generated>`. For example `PASS: 11 of 12 checks passed, 1 pending (public timestamp); export of every agent, all records, generated 2026-11-13T12:00:00.000Z`, `PASS: 11 of 12 checks passed, 1 not checked (public timestamp); ...` (offline, a confirmed proof), or, locally, `PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)`. A pack that cannot be read at all gives `FAIL: 1 of 12 checks failed (envelope schema), 11 not checked`. With `--json`, the output is machine-readable, with `result`, `result_line`, `generated`, `filter`, `project_name` (when the pack has one) and `independent: {service_keys, agent_keys}`. The first text line names the pack: `signafAIde pack of project <project_name> (<project>, <env>), <n> records`, or `signafAIde pack <project> (<env>), <n> records` without a name.

**Local state (SDK CLIs).** The local `verify` also reports chains that cannot ship (§8 BLOCKED): one `BLOCKED <chain>: <reason>; <n> records cannot ship. Next: <step>` line each, and the result becomes `BLOCKED` (or `FAIL` when a check failed too), with `<n> chain(s) cannot ship (<chains>); ` before the counts. BLOCKED always exits 1. `--fail-on-pending-send` also fails (exit 1, result `FAIL`, `<n> records not sent yet (--fail-on-pending-send); ` before the counts) while any record has no receipt, for CI. Records set aside by `signafaide repair` (§8 `held/`) are checked for their signatures, links and content hashes, and failures count in checks 3, 4 and 12. Every verifier (Rust, Python, TypeScript) gives the same check ids, statuses and result for the same inputs.

Using the service keys embedded in the pack requires `--trust-embedded-keys`, and the output then warns that it did.
