Skip to content

Verifying a bundle

The checking tool that ships inside every proof bundle is verify.py. This page walks through what it does, how to run it, and what PASS / PENDING / FAIL each mean.

The core property verified end-to-end: this exact manuscript, byte-for-byte, was registered at the public OpenTimestamps calendars on these specific dates, and the public record has since been anchored in the Bitcoin blockchain at a known block height.

What verify.py actually does

sequenceDiagram
    participant Pub as Publisher
    participant V as verify.py
    participant OTSCLI as ots CLI

    Pub->>V: uv run verify.py bundle.json
    V->>V: load bundle, verify Ed25519 signature
    V->>V: re-derive Merkle roots from leaves
    V->>OTSCLI: ots verify -d <digest> <receipt>
    OTSCLI->>OTSCLI: check against Bitcoin headers
    OTSCLI-->>V: PASS / PENDING / FAIL
    V->>Pub: Anchored in Bitcoin block N<br/>on date D

Critically, there is no arrow to BlindProof. The verifier talks to the ots CLI, which in turn talks to the public OpenTimestamps calendars and to Bitcoin. It does not authenticate to BlindProof, does not fetch anything from our servers, and does not need us to be online for the check to succeed.

Running it

Minimum setup

The publisher needs:

  • Python 3.12 or later.
  • opentimestamps-client installed (the ots CLI).
  • The bundle zip, unzipped, and the manuscript file.
  • Internet access (to reach the public OTS calendars and Bitcoin headers).

With uv:

cd /path/to/unzipped/bundle
uv run verify.py bundle.json

With plain pip:

pip install opentimestamps-client
python verify.py bundle.json

Matching against a manuscript

To confirm that a specific manuscript file is the one covered by the bundle, the publisher runs:

uv run verify.py bundle.json /path/to/manuscript.md

verify.py needs a way to compute the same HMAC commitment the client computed at save-time. Since mac_key is derived from the author's passphrase (which the verifier has no access to), the commitments can't be recomputed from scratch — instead, the bundle ships one-time reveal tokens (a per-snapshot derivative of the HMAC) that let the verifier confirm a given plaintext matches a listed commitment, without the reveal enabling offline hypothesis testing against the database.

In practice, the publisher's run is:

$ uv run verify.py bundle.json manuscript.md
Loading bundle.json ... ok
Signature (Ed25519) ... PASS
Merkle root #1 (2026-03-14) ... re-derived, matches bundle
Merkle root #2 (2026-03-15) ... re-derived, matches bundle
  ...
OpenTimestamps anchor for root #1 ... PASS (Bitcoin block 891204, 2026-03-15 04:23 UTC)
OpenTimestamps anchor for root #2 ... PASS (Bitcoin block 891362, 2026-03-16 03:07 UTC)
  ...
Manuscript match ... PASS (HMAC matches snapshot at 2026-03-19 15:42 UTC,
                          anchored in root #6 / block 892104)

Overall: PASS.  This manuscript existed, unchanged, on 2026-03-19 at 15:42 UTC.
Anchor point: Bitcoin block 892104, confirmed 2026-03-20 08:11 UTC.

Interpreting the results

PASS

Every component of the chain verified. The manuscript matches a snapshot in the timeline, its enclosing Merkle root is internally consistent, and the OpenTimestamps receipt anchors that root to a specific Bitcoin block.

This is the desired outcome. The publisher now has an artefact that no one (including BlindProof) can retroactively tamper with — backdating would require breaking Bitcoin.

PENDING for an OTS receipt

The calendar has recorded the submission but Bitcoin has not yet anchored it. OpenTimestamps calendars batch submissions, and anchoring typically takes 2–6 hours. A very recent save (last day or two) may legitimately be PENDING at the time of verification.

The publisher's options:

  • Wait a day and re-run verify.py — the pending receipt will upgrade to PASS once the calendar has a Bitcoin attestation.
  • Ask the author to regenerate the bundle after a day has passed; the new bundle will include the now-anchored receipt.
  • Accept the overall verdict as provisional if the delivery deadline is tight.

Note that a PENDING receipt is not worse than a missing receipt — the calendar's timestamp on its own is already public and unforgeable; the Bitcoin anchor is the final-form proof.

FAIL for Manuscript match

The file the publisher has does not match any snapshot in the bundle. Something is wrong: perhaps the wrong file was sent, or the manuscript was modified after the final save BlindProof captured.

This is a strong signal — false positives are essentially impossible. The publisher should ask the author to clarify which file they intended to deliver, and (if needed) to regenerate the bundle with that file's capture included.

FAIL for signature or Merkle root

Either the bundle was tampered with after generation, or the bundle was produced by a rogue signing key. Treat as invalid. Contact BlindProof.

Auditing verify.py

The script is designed to be read and audited — that's the point of keeping it stdlib-only. A reasonable audit looks at:

  1. The signature check. Is the public-key fingerprint actually used to validate, or is it just displayed? (It's used — the embedded fingerprint is compared against the hardcoded production public key.)
  2. The Merkle re-derivation. Does the tree construction match the spec? (Bitcoin-style, odd-level duplication.)
  3. The OTS invocation. Does the script pass the receipt bytes and digest correctly? (See the call-site: ots verify -d <digest_hex> <receipt_file>.)
  4. The plaintext match path. How is the manuscript mapped to a snapshot, and what prevents offline hypothesis testing against the bundle?

None of these are expected to be subtle. If something in the audit reads as clever, it's probably wrong — file an issue at github.com/tomdyson/blindproof.

Offline verification

Everything except the Bitcoin anchor check can run fully offline:

  • Signature verification — offline (pure Ed25519).
  • Merkle re-derivation — offline.
  • Manuscript match — offline (given the reveal tokens in the bundle).
  • OTS receipt → Bitcoin anchor — requires network to fetch Bitcoin block headers.

If a publisher runs verify.py on an air-gapped machine, they get PASS for the first three and a clear "network unavailable" for the last. They can re-run the Bitcoin check later, against a trusted header source, and reach a final verdict.

Known limitations

  • Fake OTS receipts from earlier demo data are explicitly detected by verify.py (they begin FAKE-OTS-RECEIPT-FOR-…) and flagged as NOT ANCHORED. This is intentional — we do not want a development fake to ever be reported as a real anchor.
  • Verify against bundle.json only, never against bundle.pdf. The PDF is human-readable; the JSON is the machine-readable canonical payload.
  • No partial bundles. If parts of the timeline are missing (e.g. incomplete sync before generation), the publisher sees fewer anchored days. This is correct behaviour — the bundle only claims what it was given.

See also