Skip to main content
This page is for the counterparty, auditor or integrator who has been handed a bundle or an archive and needs to know what a PASS means. After reading it you will know which anchor to use, what each check establishes, and how to ask what was true at a past block. The verifier is trusset-verifier-cli, wrapped by npm run verify. It reads a bundle directory or an archive, runs every check even after one fails, and prints a report. It needs no keys, no config and no network.

Two anchors

A bundle verified against nothing proves only its own internal consistency, which anyone who rewrote the whole bundle could also produce. The verifier therefore requires at least one anchor, and checks both when both are given.
With both flags, anchor is public_key+root and both must hold. The operator’s key is printed by npm run init and npm run show, and recorded in manifest.issuer.publicKey. Recording it there does not make the signature self-authenticating: an adversary who rewrites the manifest rewrites that field too. The verifier reports when a supplied key disagrees with the recorded one, and the signature check governs.

What neither anchor proves

  • That the input was true. A valid proof of a false record is a valid proof. The tool trusts what was under input/.
  • That the root is on chain. The verifier has no chain access. --root is a string you supply; its authority comes from wherever you read it.
  • That the key is the operator’s. A public key pins whoever holds the private key. Obtaining the right key is out of band.
  • Anything about country or investorType. Those two values travel in cleartext in manifest.passthrough; their proofs bind them, they do not hide them. See Concepts.
  • That the claims are current. overall_pass is cryptography only. Whether the subject could rely on the bundle at a given instant is the second question, below.
  • Recency. epochDays is the operator’s chosen batch date, and the verifier does not compare it with today. A counterparty enforces its own staleness window.
  • Formal zero knowledge. Proof bytes are deterministic in the witness and the nonce; hiding rests on the commitment nonce. Overview states this in full.
  • The registry signatures, when read from a directory. Registries under --registry are validated for structure only. Their signatures are checked when they arrive inside an archive and a --public-key was supplied.

The check ladder

Every check runs regardless of earlier results, so the report is complete.
  1. layout_ok: manifest.json, manifest.sig, kycHash.hex and every proofs/*.bin named in the manifest are present, each proofPath is a plain proofs/<name> entry, and version is 2.0.0, 2.1.0 or 3.0.0.
  2. mode_acceptable: mode is production, or stub with --accept-stub.
  3. kyc_hash_file_matches: kycHash.hex equals manifest.kycHash.
  4. signature_valid: with a key, the ed25519 signature verifies over the canonical signing view, createdAt replaced by the sentinel.
  5. root_anchor_valid: with a root, it equals manifest.kycHash, case-insensitively.
  6. Per leaf, in order:
    • bindingDigest recomputes from binding, and neither is present without the other.
    • A declared listVersion.root equals the proof’s setRoot.
    • The validity interval is well formed.
    • commitment and publicDigest decode.
    • The proof file exists and proofDigest recomputes from it.
    • circuit and circuitId agree.
    • paramsHash recomputes from params.
    • An empty proof is allowed only for commitment_only or a stub bundle.
    • The circuit’s verify() accepts the proof against the public inputs, with the AIR selected by circuitsVersion.
    • leafHash recomputes from all of the above.
  7. merkle_root_valid: the leaf hashes recompute to manifest.kycHash under merkleVersion.
  8. overall_pass: every check above, every leaf passed, and every supplied anchor held.
A leaf that fails records a reason, such as proof digest mismatch, leaf hash mismatch, paramsHash mismatch: ... or empty proof on a proving leaf in a non-stub bundle.

Verifying and being entitled

overall_pass answers whether the bundle holds together. temporal.entitled answers whether the subject could rely on it at the instant asked about. It is true only when four things hold. The bundle passes. The instant could be resolved. The root live at that instant is this bundle’s root, where that was checkable. Every leaf’s claim is effective then.
That pairing is what a lapsed, voided or expired claim looks like. The proof is a true record of what was asserted; the entitlement is what has changed.

The instant

--as-of-block requires --chain-id and is exclusive with --as-of-date. The root history and the revocation log are read from --registry <DIR>, default config/registry, or from the archive. An as-of block question has three possible answers, and only one may be reported as entitlement. When no history was supplied, or the subject had nothing published at or before that height, entitled is false and a note says the answer is unknown rather than false:
When the history resolves to a different root, the note says this bundle was not the live issuance then, and entitled is false.

Claim status

Each leaf of a version 3 bundle reports a status at the instant: A voidance wins over everything, then the earliest applicable lapse, then the window. Without a revocation log the statuses reflect only what the bundle asserts, and the report says so in notes.

The report

--json prints the report as one JSON object, or an array when an archive holds several subjects.
string
public_key, root, public_key+root, or none. A report with none never passes.
boolean
false whenever the corresponding anchor was not supplied; the human report prints those as [n/a].
string
As read from the bundle, for callers that branch on it.
object | null
From the manifest: name, identifier_scheme, identifier, jurisdiction, key_id, public_key, and public_key_disagrees_with_anchor, true when a supplied --public-key differs from the recorded one. null on a version 2 bundle.
object
The instant and its outcome. as_of is now, date or block, with as_of_epoch_days and as_of_date. A block query adds as_of_chain_id and as_of_block, and resolved_root with resolved_root_matches when a history was searched. root_history_available and revocations_available say what was supplied. entitled is the answer. notes lists everything the verifier could not establish, stated rather than omitted. unpinned_epoch, manifest_as_of_date and epoch_days_source come from the manifest.
object
Per leaf: field, circuit, pass, reason (null on success), and claim.
object | null
The descriptor as recorded, resolved at the instant. status and detail give the outcome. legal_basis, legal_reference, assertion and evidence_class repeat the descriptor. issuing_party and issuing_party_identifier name the warranting party as scheme:identifier. validity_from, validity_until and basis_as_of each come with a _date rendering. For set_membership, list_id, list_version, list_resolved and list_entry_count say whether the registry or archive could resolve the proof’s root back to the list’s values. null on a leaf with no binding block.
The human report prints the same content, with each leaf’s claim descriptor, window, list resolution and status under the leaf line, then the notes, then Verifies and Entitled. In the claim line the tool separates the reference from the assertion with an em dash, which these pages render as a hyphen.

Exit codes

npm run verify checks for a missing anchor itself and exits 1 with an explanation before the binary runs; the binary called directly exits 64 for the same mistake. An archive with several subjects exits 2 if any fails and 3 if any is not entitled.

Verifying from an archive

An archive carries the bundles, the list registries their proofs depend on, the root history and the revocation log, so it verifies on a machine that has nothing else:
Every subject in the archive is verified and reported in turn; --subject <0xADDR> selects one. With a public key, the archived registry signatures are verified under it before anything else is believed. The npm wrapper takes the same path with npm run verify -- <wallet> --archive 2026Q3.tka --public-key <hex>, mapping the wallet to --subject. An archive whose contents no longer hash to its recorded digest is refused before parsing continues:

Older and development bundles

A version 2 bundle verifies exactly as before. issuer is null, every leaf’s claim is null, and entitled depends only on overall_pass. A note says that what each predicate stands for and how long it was asserted are not recorded. A manifest with circuitsVersion: 2 is checked against the frozen set_membership AIR of that version. A bundle built with --skip-proofs carries mode: "stub" and zero-byte proofs. It fails mode_acceptable unless --accept-stub is passed, and --strict is accepted as a no-op for older scripts. A production bundle with an empty proof on a proving leaf is rejected outright, so the two cannot be confused. A bundle built with --dev-unpinned-epoch verifies, and the report adds reproducible: [FAIL] built with an unpinned epoch with a note that it is a development artifact, not evidence.

The WASM build

scripts/build.sh also compiles the verifier to target/wasm32-unknown-unknown/release/trusset_verifier.wasm when the target is installed. The wasm feature exposes the whole-bundle entry points verify_strict, verify_permissive, verify_with_anchors and verify_as_of. It also exposes granular ones for a consumer that drives its own check ladder: proof_digest_hex, public_digest_hex, leaf_hash_hex, leaf_hash_hex_bound, binding_digest_hex, merkle_root_hex, merkle_root_hex_v, manifest_signing_bytes, verify_manifest_signature, verify_leaf_stark and verify_leaf_stark_v. scripts/rebuild-wasm.sh produces a reproducible Node.js binding with a provenance record, and tests/e2e/wasm_parity.sh asserts the native and WASM verifiers produce byte-identical reports. A consumer that vendors the WASM must rebuild it whenever the manifest schema gains a field, or its recomputed signing view will drop the field and every signature will fail.

In the repository