Skip to the content
nullroute
Browse the docs

3. The verification report

make verify performs five checks and writes verification-report.json. The daemon refuses to start if that report is missing, stale, or failing (INV-BUILD-1), so a spec that stops matching its code stops the device booting.

Check What it asserts
Coverage Every runtime export from the entry points of packages/core, the daemon and the ui appears in some spec's covers list; type-only exports are exempt. An uncovered export, or a covers entry naming a symbol that no longer exists, is a hard failure.
Invariant binding Every declared invariant names at least one test, that test exists, and it passed. make verify runs the whole suite first, so failing vector and differential tests fail here.
Vectors Every vector file a spec declares in spec/vectors/ exists and still matches its pinned SHA-256. The vectors themselves run in the suite.
Differential core.address.derive, core.derive.hd and core.psbt.sign each still declare a differential oracle, and critical-tier oracles declare at least 100 cases. Agreement with bitcoinjs-lib is tested in the suite.
Integrity Every file MANIFEST.lock lists exists and matches its hash, and the lock is sorted with no paths differing only by case. A tracked file missing from the lock is not caught here. The root hash is computed for the report, not compared with anything.

It then writes the report: assurance tier, root hash, spec and invariant counts, specs per assurance tier, each check's status, coverage, the invariant bindings, a summary of each spec, and an overall passed. It holds no test results and no dependency tree hash. That is output, not a sixth check: a report cannot certify itself.

Vectors and differential report not-applicable, never passed, when no spec declares any. Coverage, invariant binding and integrity report only passed or failed, so an empty run of one of those would read as passed.

The report's tier is not measured from the build: the verifier writes the literal signer, and the tier is not an input to the root hash. There is one tier anyway: packages/ holds core, daemon, ui and verify, packages/wallet does not exist, and phase 5 has not opened. The field cannot hold a second value, and the lock screen distinguishes nothing by it.

Reading it yourself

verification-report.json is plain JSON. The fields that matter:

  • rootHash: compare with the lock screen, and with a published release once one exists
  • tier: signer, and only signer, today
  • coverage.uncovered: must be empty
  • invariants[].status: every one must be passed. Each entry is one binding, an invariant id and one test selector in test, so an invariant bound to several tests has several entries; there is no tests array

No field covers how node_modules resolved. The report has never carried a dependencyTreeHash, and nothing in packages/, tools/ or the Makefile computes one. That is a gap; standing in for it are make repro-check, which builds twice from a clean tree and compares the output, and the SBOM.

A passing report is only as trustworthy as the build that produced it, which is why step 2 comes first.


Rendered from docs/VERIFICATION.md, the same bytes that ship with the code. SHA-256 15ae2ba12417b4691f63d9a4777eba2c328ff694961efea68f5e7dec4ae2e0bd