wai-quantum: receipts say where their counts come from and what was set at the provider, and a second verifier checks them #660
Loading…
Reference in a new issue
No description provided.
Delete branch "wai/quantum-receipt-fields"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacked on #654 (
wai/quantum-cloud-tasks): that branch is merged in here at its current head, and main with it (through #652).This adds the receipt fields WAI Quantum needs before its first hardware receipt, since a signed receipt can't gain fields later. It also adds a second verifier of job receipts, written in another language from the spec alone. This is Phase 0 of the platform plan: the gate is that the second verifier agrees with the reference on every receipt.
The fields
counts_source:shots,vendor_countsorprobabilities. Counts rebuilt from probabilities are derived, not shot records, and now say so.native-turnsalways rebuilds them.cloud-tasksreads a task's rows, and rebuilds only when its result has none.sampler-v2tallies shots.prx-circuitstakes the service's counts.execution: whether each of mitigation, twirling, decoupling, remapping and optimisation isoff,on, ordefault(the request sets nothing for it). It also records whether the gates were to run as written, and what was sent. Each client builds it from the very body it posts (Provider::execution), and a test holds every kind to the body its scripted service received:sampler-v2turns decoupling and twirling off;native-turnsturns debiasing off;cloud-tasksputs a processor's gates in the verbatim box;prx-circuitssends onlycircuitsandshots, so it sets nothing and says so.A provider on the open protocol states both in its
resultanswer (§5.5), and a wrapper around another service must.Simulators are not measured. The class follows the provider's provenance:
measuredfor silicon,simulatedfor a simulator or emulator, plusapproximate: truewhen the provider says so. The old note claimed "no mitigation", which no client turned off or recorded; it no longer does. Receipts sealed before these fields still verify, and a verifier reads their class by provenance and says so.compiled.qasm3: the program's text now sits beside its hash, so the hash can be checked.All of these are additions under the protocol's §10. This PR's own changes to the crates' public API are additions only: provided trait methods,
#[non_exhaustive]structs, a#[non_exhaustive]CountsSource, and functions. Thewai-quantum-qpu0.5.0 in this branch is #654's, for its new kind.The second verifier, and what it found
quantum-conformance/tools/receipt_verify.pywas written in Python from the text alone, by an agent forbidden to read the Rust, the other tools or any reference output. It is standard library only, with its own BLAKE3, Ed25519, RFC 8785 text and strict reader.Its first pass on the old text listed 44 gaps. §8 was rewritten from them, and the next three passes found 30 more, each settled in the text. The rewritten §8 covers the seal byte for byte, the strict reader, the canonical text and the first form's, the strict Ed25519 rules, the per-kind rows, seventeen named reasons, and what a receipt proves. VERSIONING, the receipt schema and the service README now agree with it.
The reference was too lenient. It accepted:
alg, andcanon: null;+;It also panicked on a two-byte character in a key. It now refuses each of these:
Rcanonically;S < ℓitself rather than leaving it to a library feature;wai-quantum verify --jsonand--linesgive the verdict as data.The second review, and what changed
The second review, of
7671774f0, found seven that held, and the planner found one more. Each fix has a test or corpus vector that failed on the old code, and the second verifier agrees on every one, written again from the text by the same clean-room agent.inexact_numberrefused RFC 8785's own spelling of a large whole double (2^60 as 1152921504606847000)a_large_whole_double_verifies_as_its_canonical_text_writes_it;crafted-number-{canonical,exact,exponent}-spelling(valid),-near-canonicaland-padded-past(inexact_number); plantno-canonical-spellingverify --linesjudged a set as a plain job, so every genuine set gotdigest_mismatch; §8 never defined setsset_items,set_item,set_root), the Merkle tree spelled out, and what the tree does not fix stated. Every surface judges a set by ita_set_is_judged_by_its_own_rules,lines_judge_a_set_as_a_set, Pythontest_a_set_s_text_is_judged_as_a_set; two sets made by the command line and fourteen crafted; plantset-seal-onlyjobText); an object is written out only when it is all there is. README shows the text path and the answer's shapeverify-text.test.mjs(the desktop engine and the worker's helper), the browser's and the desktop's tests throughengine_verifyandverifyon textsets::runand the service's set planning screen the whole request. The first-form exemption atsets::verifynow comes from §8.6's check, and §8.4's text names every host that sealed such a numbera_set_request_s_every_number_is_screened;crafted-set-sweep-inexactunreadableor past-u64inexact_number, and their tests skipped exactly thosejob), and their tests hold them to the whole corpus, skipping only what a JSON string cannot hold (bytes that are not UTF-8). §9 says exactly what a parsed job cannot showthe_mcp_tool_given_a_job_s_text_gives_the_corpus_s_verdict,the_service_gives_every_receipt_the_verdict_the_corpus_gives_itcompiled.qubit_nameswas checked only against the layouts$ithatqasm3acts on must be named, in both verifiersqubit_names_name_every_qubit_the_text_acts_on;crafted-kind-tasks-names-short-of-text; plantnames-layout-onlysignerevery_tool_s_answer_follows_its_output_schemafails without itThe clean-room verifier's reading of §8.6 found further gaps, each now settled in the text and in both verifiers: an empty set,
nullmembers, a failed item's numbers (a forgery path: nowset_item, read as a canonical receipt reads them whatever the set's form, since a leaf is the canonical form's digest), an item sealed by another key (compared with the set's receipt'skeymember as it stands, missing included), an item that is itself a set, the path length, and two properties of the inherited tree thatcountand the item's receipt cover. An item is read for itsindex,request,result,errorandpath; itsstatus,method,whyand a sweep'spointare the host's, unsealed, and the differential's oracle now expects a change there to leave the verdict as it was (the full differential first found 26 such one-member changes refused by neither verifier, as they should not be, and flagged them).On this head: the corpus is 156 receipts (47 valid, 109 not), all agreeing three ways; 11,532 one-member and edge changes, all agreeing; ten plants, each caught; the second verifier's self-test 232 of 232.
The views page. The Studio browser step found what no other step did: the committed page was built before
wai-quantum-viewschanged, sobuild-views --checkfailed, and rebuilt it was past its 1 MiB limit, sincejob::verdictcarried the engine's re-derivation of a sweep into the page. The views'verifynow holds the receipt's rules alone (job::check), and the page is 984,188 bytes, built on Linux as CI builds it.The first review, and what changed
An independent review confirmed twenty-three findings on this PR (with #654's, thirty-four). Each has a test that failed on the old code for the stated reason, then the fix. None was disputed.
Every verify surface holds every rule. Before, only
wai-quantum verifyapplied §8.4.POST /v1/verifyand the service's MCP tool checked the seal alone, so a simulator's counts labelledmeasured, counts that do not sum, and a member named twice came back valid.job::verifyskippedinexact_numberthough its doc said it applied every rule, and the command line's MCP tool, the Python package, the desktop app and the browser all called it. Now:job::verifyrunsjob::check;job::verdictandjob::verdict_of_textgive every surface one answer,valid,reasons,key,form,signer./v1/verify's body as text (check_text), so a member named twice isunreadable; its tool runscheck.job::text_refusal), and--jsonon them gives JSON.verifyreads text, bytes and paths as text, and givesreasons.the_service_gives_every_receipt_the_verdict_the_corpus_gives_it(HTTP and the tool, every receipt),the_mcp_tool_gives_the_corpus_s_verdict_on_what_a_parsed_job_shows,a_member_named_twice_is_unreadable_on_every_kind_of_job,json_output_is_json_on_every_kind_of_job,verify_holds_what_check_holds, Pythontest_every_receipt_of_the_corpus_gets_its_verdict_here, the desktop'sverify_reads_a_job_s_text_and_holds_every_rule, the browser'sengine_verify_reads_a_job_s_text_and_holds_every_rule,an_item_is_held_to_the_rules_of_a_job,a_surface_that_takes_a_job_verifies_a_set_as_a_set.A host seals nothing its own verifier refuses.
qectook a seed of any size, sealed its double and printed "receipt verifies";verifythen refused the file. The sweep found compile, pulse, estimate,@Seal, a project's intent check, the service's sealers and set items open the same way. Each now refuses a request with a whole number past ±(2^53 − 1) where it reads it (job::exact_request), andreceipt::seal_intowrites a whole number no double holds in a result as the double sealed. The provider path checks its own receipt before giving it, so a provider's word cannot produce a receipt this host refuses. A receipt already sealed with such a number stays refused: its signature holds, and written as that double's digits it verifies. VERSIONING and §8.4 say so. Tests:a_host_never_seals_what_its_verifier_refuses,a_result_s_number_no_double_holds_is_written_as_the_double_sealed,a_receipt_sealed_with_a_number_no_double_holds_verifies_written_as_what_was_sealed,a_host_gives_no_receipt_its_verifier_refuses_whatever_a_provider_answers.The rest:
executionstated bynobodyis taken on every kind (§8.3, §8.4, §8.5 say so)a_job_recorded_by_the_last_release_is_sealed_with_a_receipt_this_one_accepts(sampler-v2, prx-circuits, native-turns, each record as 0.4.1 wrote it)whole-capplant, and corpus receipts, prove it can see thisa_whole_number_has_no_ceiling_and_only_a_count_has_one,shots_are_the_requests_when_the_two_are_the_same_value,counts_sum_exactly_past_what_a_double_addsverify --json --historyskipped the history--historyon a set, an estimate or an algorithm's answer is refusedjson_output_does_not_skip_the_history_it_was_asked_to_checkcompiled.qasm3is on the client's indices, not the device's qubits (cloud-tasks, prx-circuits)compiled.qubit_namesseals the device's name for each index, as on the day it was sent; required of those two kinds when their client states what it senta_receipt_names_the_device_s_qubits_as_the_program_was_sent_on_them,a_client_that_names_its_qubits_seals_the_namessampler-v2hard-coded siliconsimulatora_simulated_backend_is_not_siliconmade.lock.jsonpins each by SHA-256;--checkfails on one dropped, changed or unnamed;generate.py --self-testshows each waygenerate.py --self-test(5 checks)lax-small-order-rplantan_r_of_small_order_or_ill_spelled_is_refused_where_the_equation_holdsexecution.sentwhat_was_sent_is_checked_in_time_proportional_to_it, and the second verifier's self-testcounts_sourceread as the host's actprovider.kind;verifyadds "in the provider's word" onhttpandprocesswhat_verify_says_is_only_what_the_receipt_shows72d18742d(18 October) and run herethe_earlier_form_is_named_and_no_time_is_claimed_for_itThe second verifier was brought up to the new text by the same clean-room agent, from the text alone. Its reading found one disagreement between §8.1 and §8.4 (does a
cloud-taskssimulator name its qubits: yes, each index itself) and four choices the text left open (a number's value is its double on every form,nullis given,-0is 0,nobodyon any kind is stated in §8.5); each is settled in the text, and the reference agrees.The corpus and the differential
quantum-conformance/receipts/holds 156 receipts (47 valid, 109 not):receipt_differential.pyholds both verifiers and the manifest to each other on every receipt and on every one-member change of each valid one. Each defect planted in the second verifier must be caught. CI runs this beside the CLI's build (wai-quantum-cli.yml), and runs the second verifier against the manifest among the independent verifiers.On this head, run locally on Linux:
The differential's first full run found one more error in the spec, after the second verifier's four passes: §8.2's first-form rule wrote a positive exponent without its
+. The first form was sealed only from 17 to 20 October 2026, by one writer, which writes1e+16. The text now says so. The reference also no longer leans on today's serde_json for that text: it writes the first form by §8.2's rules, and a test pins that writer to serde_json's output.Tests, each workflow's own Linux step replayed on a build host:
-D warnings, and the verdict and answer sets;Not run locally: the other macOS, Windows and hardware legs, and the wide conformance workflows; CI runs those. Emulated and signal-processor legs are not evidence until this branch carries main's cache fix.
Replaying the steps found one more fault before CI did: the job service's committed tool list was stale, since the schemas changed after it was last regenerated. It is regenerated, and the qpu workflow's pinned test counts are raised for this branch's three new tests.
The real
cloud-tasksreceipts from the service's simulator verify under both verifiers, and are read as simulated. They are not in the corpus, because they seal the account's number injob.id(--extra DIRchecks them locally).🤖 Generated with Claude Code
dcharlot referenced this pull request2026-11-06 08:13:23 -05:00
The Studio's English words for an estimate's minimum ("at least") are among the page's sources, and the committed page was built before them. Co-Authored-By: Claude Opus 5.5 <[email protected]>