Tracefold

Verify a receipt

Drop a receipt and the public key it was signed with. The check runs in this tab, against the same function the command-line tool calls. Nothing is uploaded, nothing is stored, and there is no account and no toolchain to install.

Requests made by this page's own code since it loaded: 0. fetch, XMLHttpRequest, WebSocket and sendBeacon are wrapped and counted — the count excludes the request that fetched this document, which your browser made before any of this ran. Save the file, disconnect, and reopen it: it works the same.

1. The document

Drop receipt.json, a key file, or a checkpoint here — files are read in the page and routed by shape. Or paste below.

Exactly what gx receipt show or GET /receipts/{tid} hands back. Paste the file's text, not a parsed object.

This is the key you are checking the receipt against. Getting it from the same party that gave you the receipt proves nothing about that party; it proves the file has not changed since they signed it.

Without a checkpoint, the ledger half of the answer is not checked and the verdict says so. The two checkpoint-key boxes move together: one without the other is refused rather than half-checked.

No receipt of your own? These three load real files out of the test suite and produce the three different answers. None of them is staged for this page:

2. The answer

Nothing checked yet

Give the page a receipt and a public key and press Verify.

The exact JSON the engine returned, unedited — the page's words above are a reading of these bytes and nothing more:

// no verification has run in this tab yet

Three answers, not two

A verifier with only pass and fail has to file "I could not read this" under one of them, and both filings are wrong. Filed as a pass it is fail-open. Filed as a fail it accuses somebody of editing a document when the real cause may be that this build is older than the file, or that you pasted the wrong key. This page keeps the third answer separate, and never says refuted about an input it merely could not read.

What the engine returnedWhat this page calls itWhy
valid: trueVerifiedEvery check that could run, ran and passed.
no valid signature under key …RefutedAC-019. The signed bytes and the signature disagree: the document was edited, or this is not the key that signed it. The engine deliberately does not say which part failed — naming it would tell a forger what to fix.
checks.canonical_cid: falseRefutedThe receipt's own content address does not match the bytes it commits to.
checks.inclusion: "refuted"RefutedThe inclusion proof was walked against the checkpoint and contradicted.
checks.inclusion: "unanchored"Not checkableThe signature held. No checkpoint was supplied, so the ledger claim was not checked at all. A skip and a pass must not share a face.
checks.inclusion: "unbridged"Not checkableThe checkpoint names a tree size the receipt's proof does not, and nothing bridged them. Not a pass, and not a refutation either.
the receipt is not a legal one: …Not checkableWell-formed, but not a shape this build accepts. Very often an older or newer receipt format — an accusation of tampering here would send you hunting for an attacker who does not exist.
receipt: … / public_key_base64: … / checkpoint_json: …Not checkableThe page could not read one of the inputs you gave it. That is a fact about the input, not about anybody's honesty.
anything elseNot checkableAn unrecognised refusal is reported as unread, never as refuted. The engine's exact words are always printed above so you can see what this page could not classify.

What this page does not check

Every check has a blind spot, and one that hides its blind spots is worse than one that has more of them.

How to check this page

Taking a verification page on trust is the failure it exists to prevent, so here is what to compare and how.

The verifier is the shipped engine, not a re-implementation

The WebAssembly module below is built from sdk/wasm-verify, which calls gx_witness::receipt::verify_offline — the one function gx receipt verify --offline calls. That crate constructs no verdict and mints no content address of its own; it is a projection of the engine through wasm-bindgen. The JavaScript in this page moves strings in and renders JSON out. It contains no cryptography.

wasm module sha256  8f7064b675cc89e502ae63422b08532f28a83616fe60968e3b66345a0d772776
bindgen glue sha256 4a925bdb24c740ad231db54b677da0d575bffe7821f701dbddcc817a63cdeebd

# reproduce both, from a checkout:
sdk/wasm-verify/build.sh
sha256sum sdk/typescript/src/wasm-gen/wasm_verify_bg.wasm
node web_verify/build/build_verify.mjs
Get the same answer a second way

Run the same three files through the command-line tool and compare. If the two disagree, one of them is wrong and we want to know which — that disagreement is a bug report we would rather receive than not.

gx receipt verify --offline receipt.json \
  --key-id <KEY_ID> --public-key <PUBLIC_KEY_BASE64> \
  [--checkpoint checkpoint.json --checkpoint-key ledger.pub]

One known difference, stated rather than hidden. The command-line tool reports through an exit code, and exit 7 covers both "this build cannot read the document" and "somebody edited it" — the two answers this page keeps apart. The information is in the tool's JSON body (checks.signature separates them) but not in its exit code. This page reads the body, so where the exit code collapses three answers into two, the page does not.

Watch it work with the network off

Save this page (it is a single file, with the verifier inside it), disconnect, reopen it from disk and verify again. The counter at the top of the page stays at zero because the page has nothing to call: the module imports exactly one function from the host, __wbindgen_init_externref_table, and the build refuses to produce this page if that ever stops being true. No entropy source, no clock, no socket.