Verifier rules
Generated from the protocol specification, section 7. For what each check means in plain words, see Verification packs.
The checks, in order #
- 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. record_hash.- The client signature, with a key of the independent agent-key set that was active at the record's
timeand not revoked before its batch'sserver_time. Without independent agent keys this check fails withNOT INDEPENDENT: agent keys came from the pack, unless--trust-embedded-agent-keysis given. - The chain links (
seq,prev) per agent;timenever goes back along a chain; no record'stimeis more than 300 s after its batch'sserver_time. A gap inside a chain fails. A chain that starts after seq 1 fails unless the export declared afromfilter that is after the earliestvalid_fromof the agent's keys (otherwise every record of the agent is inside the window). - The Merkle proof to the batch root.
batch_hashandbatch_sig.- The batch chain, and that
server_timenever goes back along it. (A gap between batches is a note: another agent's batches are not in an agent-filtered pack.) Anotenamedsignafaide.instance_startorsignafaide.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
notenamedsignafaide.instance_stop, the start is a clean restart. It adds the notea 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
fromcut the chain there) it adds the notea 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.
- The export statement and the heads:
export_sigverifies with a service key active atgeneratedover the statement rebuilt from the pack. Any change toproject,env,generated,filter,headsor the records (dropped, added, reordered) fails.- Every record is inside the filter: its agent is in
filter.agents(when set), andfrom <= time < to(each bound when set). - The head signatures, in each receipt, the export
heads, and the--headfile. An export or client head naming a different record at a held seq fails. - Each export head's
server_timeis at or beforegenerated, 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 unlesstois set and the head'sserver_time+ 300 s is at or afterto(its record may be afterto). An export head whose agent has no records in the pack fails. - A client head (
--head) for an agent in the filter, withserver_timeat or beforegenerated, applies when the agent has records in the pack or the window holds all of the agent's records (nofrom, orfromat or before the agent's first key), unlesstois set and the head'sserver_time+ 300 s is at or afterto. When it applies, the agent's records must reach its seq. A client head newer thangeneratedis noted, not applied. - The drand round: its
server_timebounds (§3.1), and its BLS signature (quicknet) when the library is available. If BLS is not available, it reports "not checked", never "pass". - The anchor paths,
anchor_sig,keys_hashagainst the supplied service keys,createdagainst the covered batches'server_time, and the anchor chain. Two different signed anchors with the sameseqfail. A batch whoseserver_timeis more thanANCHOR_WINDOW(2 h) beforegeneratedand that no anchor in the pack covers makes the checknot checked, with the note "not timestamped". A younger one, or a pack with no anchors and no such older batch, makes the checkpending("not yet anchored"). - 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 whensha256(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'snBits(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 reportspending(the check's status ispending, and its valuepending) while a proof is not confirmed yet, and also when the pack has no proofs because its batches wait for their anchor (check 10pending);confirmed@blockonly when every attestation was checked against a trusted header;not checkedwith the claimed block when a header could not be had or trusted (--offline, anhttp://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. - 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 §8chain/<project>/<env>/<agent>.jsonl; a directory means every*.jsonlfile 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> pendingand, <n> not checkedwhen 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.