open-standards/wai/wai-quantum-service
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science db65d20be0 sec: close the advisory backlog across every workspace, and move the WASM sandbox off a runtime with three critical escapes
`cargo audit` over all 47 workspace lockfiles reported 142 vulnerability
instances. The dangerous ones were not exotic: the S2 executor was pinned to
wasmtime 43.0.2, which carries eight advisories including RUSTSEC-2026-0379
(9.9), -0327 (9.3) and -0269 (8.8). That crate is the one component whose whole
job is to execute untrusted guest code, so a sandbox-escape class defect there
is not a dependency hygiene matter — it is the confinement claim itself.

wasmtime moves 43.0.2 -> 49.0.4, the single version every one of the eight
advisories names as patched (50.0.0 exists but no advisory lists it as fixed,
so it is not yet the safe choice). WASMTIME_VERSION is updated with it: the
constant is attested into the mechanism commitment under SPEC §7.9, so leaving
it stale would attest confinement to a runtime that did not run. The crate's
own `wasmtime_version_matches_the_lockfile` test enforces that pairing.

The rest are semver-compatible bumps carrying no API change: rustls 0.23.46,
rustls-webpki 0.103.16, webpki-roots/webpki-root-certs 1.0.10, quinn-proto
0.11.18 (RUSTSEC-2026-0340/-0343, both 7.5), tokio 1.54.1, crossbeam-epoch
0.9.20, rtrb 0.3.5.

Not fixed here, and deliberately so rather than silently: zip (>=9.0.0),
hpke-rs (>=0.6.0), quick-xml (>=0.41.0) and libcrux-kem (>=0.0.10) are all
semver-major moves, three of them transitive, so each needs its dependent
assessed on its own. rsa RUSTSEC-2023-0071 has no fixed version at all.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-22 09:44:59 -04:00
..
src wai-quantum 0.6.14: design critique, equivalence, alternatives and classical shortcuts across every surface; contraction trees that sweep along the line 2026-10-22 02:40:04 -04:00
Cargo.lock sec: close the advisory backlog across every workspace, and move the WASM sandbox off a runtime with three critical escapes 2026-10-22 09:44:59 -04:00
Cargo.toml wai-quantum: the cut-rank table on the GPU, and the engine's cutrank_single method 2026-10-20 17:34:17 -04:00
README.md wai-quantum 0.6.14: design critique, equivalence, alternatives and classical shortcuts across every surface; contraction trees that sweep along the line 2026-10-22 02:40:04 -04:00
smoke.sh wai-quantum 0.6.14: design critique, equivalence, alternatives and classical shortcuts across every surface; contraction trees that sweep along the line 2026-10-22 02:40:04 -04:00

wai-quantum-service

The WAI quantum stack as a public job service. Send an OpenQASM program; get back measurement counts, Pauli expectation values or amplitudes. Each result names the method that produced it and how accurate it is, and carries a signature you can check.

No account and no key. The public instance is at https://wai.transaction.science/quantum-jobs/v1.

# A Bell pair, 1000 shots.
curl -s https://wai.transaction.science/quantum-jobs/v1/run \
  -H 'content-type: application/json' \
  -d '{"qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", "shots": 1000}'

Jobs

A job is JSON:

field meaning
qasm the program: OpenQASM 3, or OpenQASM 2 (required)
method auto (the default) or a method below
shots measurement shots (default 1024 when nothing else is asked for; at most 1,000,000)
seed the sampling seed (default 1); the same job and seed give the same counts
observables Pauli strings such as "Z0 Z1", or {"pauli": "X3 Y4", "coeff": 0.5}
amplitudes bit strings, one character per qubit, qubit n − 1 first
noise {"depolarizing_1q", "depolarizing_2q", "amplitude_damping", "dephasing", "readout"}, each a probability
options per method: threshold, max_weight and ladder (pauli), max_bond and cutoff (mps), fusion (statevector), trajectories, memory_gb

Counts are keyed by the program's classical bits, the highest bit first. With no measurement in the program, every qubit is measured into the bit of its own index.

The program may use the standard gates, user gate definitions, and the control modifiers ctrl @, ctrl(n) @, negctrl @ and negctrl(n) @ (on x y z h s sdg t tdg p and cx cy cz ch cp ccx ccz). Measurement and reset may come anywhere, and if acts on measured bits (if (c[0]) x q[1];, if (c == 3) { … } else { … }, OpenQASM 2's if(c==1) x q;). Loops, inv @ and pow @ are refused, each with its line and what to write instead (src/qasm.rs lists everything read).

Methods

method takes answers accuracy
exact angles that are dyadic multiples of 2π, up to 26 qubits counts, expectations, amplitudes, a state hash bit-exact
statevector any circuit, up to 32 qubits counts, expectations, amplitudes exact
statevector_single any circuit with gates on at most four qubits, up to 33 qubits, where the host has a GPU counts, expectations, amplitudes single
stabilizer Clifford circuits (rotations by whole quarter turns), up to 32768 qubits, optionally with depolarizing, dephasing and readout noise counts, expectations exact; with noise, an estimate
pauli any circuit, up to 1024 qubits expectations bounded
mps any circuit, up to 4096 qubits, at bond dimension up to 256; best when entanglement stays low counts, expectations, amplitudes exact when nothing is truncated, otherwise heuristic
tensor_network any circuit amplitudes exact
cutrank a Clifford circuit, then one layer of one-qubit gates at any angles, up to 4096 qubits, as wide as the state's cut-rank allows counts, amplitudes exact; single when only a single-precision table fits
cutrank_single as cutrank, with the table on the host's GPU, where it has one; only when asked for counts, amplitudes single
density_matrix noisy circuits, up to 16 qubits within a job's memory counts, expectations exact
trajectories noisy circuits, up to 32 qubits counts, expectations estimate

How the methods run:

  • statevector groups consecutive gates into passes over the state. Each pass applies its gates tile by tile, in a buffer that fits a core's cache. The grouping never changes a bit of the result.
  • cutrank takes a Clifford circuit followed, on each qubit, by one-qubit gates at any angles (a qubit's gates from its first non-Clifford one on act on that qubit alone).
    • It writes the Clifford part's state as a graph state with local Cliffords, and sums the amplitude one qubit at a time along an order of the qubits, in a table of 2^cut entries. Here cut is the state's cut-rank, its entanglement in bits, across the cut the sum has reached.
    • The planner makes one short search for an order with narrower cuts, enough to bound the job's memory and work. The run searches longer, from several seeds on threads of their own, and keeps the cheapest order that is no wider than the planner's. The cost is set by the widest cut, max_cut in the result, not by the number of qubits.
    • Counts are exact samples, drawn qubit by qubit from the table's conditional probabilities.
    • Amplitudes carry the circuit's own global phase. A stabilizer tableau does not fix it, so one amplitude of the Clifford part is tracked exactly, gate by gate, and the summed amplitudes are scaled to agree with it, together with the phase the lowering to Clifford gates drops. A circuit with so many Hadamards that the tracking would take more than about a minute reports global_phase as not fixed instead.
    • The table is held in double precision when it fits the job's memory, and in single precision otherwise (accuracy class single).
  • pauli reports each value with its truncation bound. It holds at most as many Pauli terms as the job's memory allows; a sum that would outgrow it raises its threshold until it fits (threshold_reached), and the bound covers everything that dropped. Unless ladder is false, it also reports a ladder: the value and bound at up to three thresholds a decade above the one asked for, so you can see the value settle as the threshold drops.
  • stabilizer, with noise, samples by Pauli frames against one noiseless outcome of the tableau, 64 shots to a machine word. Its counts have the law of independent noisy shots. Its expectation values are estimates with standard errors, from at least 10,000 shots unless trajectories sets the number. Amplitude damping is not a Pauli channel and is refused.
  • trajectories treats every shot as an independent noisy trajectory in law. Shots that took the same errors share one state, so the error-free part of a circuit, and the error-free majority of the shots, run once. histories in the result counts the distinct error histories that ran.

Programs that measure in the middle

A program that measures a qubit and then acts on it, resets a qubit after its first gate, or uses if on measured bits runs on statevector or stabilizer. auto picks the stabilizer tableau when every gate is Clifford, and the state vector otherwise.

  • Counts split their shots. The shots enter together, and at each measurement or reset they divide between the outcomes as independent shots would: binomially, in the outcome's probability. Each outcome then runs on in its own copy of the state. Shots that read the same outcomes share one state, and the counts have the law of independent runs.
  • Expectation values are exact: the mixture over every branch of non-zero probability, at most 4096 branches. As for any program, they describe the state before the final measurements. A measurement whose qubit nothing touches afterwards, and whose bit no later condition reads, is not made for them.
  • The result's dynamic member reports what the walks did:
    • measurements_and_resets;
    • branches_run and states_at_once for the counts;
    • branches_weighed for the expectation values.

Noise in such a program, and amplitudes of one, are refused, each with its reason. The state vector holds a copy for each branch still running, so it takes one qubit fewer than for a program that measures only at the end.

# Teleportation: ⟨Z2⟩ = cos 0.7 and ⟨X2⟩ = sin 0.7, exactly.
curl -s https://wai.transaction.science/quantum-jobs/v1/run -H 'content-type: application/json' -d '{
  "qasm": "OPENQASM 3.0; qubit[3] q; bit[3] c; ry(0.7) q[0]; h q[1]; cx q[1], q[2]; cx q[0], q[1]; h q[0]; c[0] = measure q[0]; c[1] = measure q[1]; if (c[1]) x q[2]; if (c[0]) z q[2]; c[2] = measure q[2];",
  "observables": ["Z2", "X2"], "shots": 1000 }'

auto picks the first method that takes the job. With noise, it tries density_matrix, then stabilizer, then trajectories. For counts, it tries stabilizer, exact, statevector, statevector_single, cutrank, then mps. For expectations alone, it tries stabilizer, exact, statevector, statevector_single, pauli, then mps. For amplitudes alone, it tries exact, statevector, statevector_single, cutrank, tensor_network, then mps. The job's why names the method chosen and the ones that could not take it.

The accuracy classes:

  • bit-exact: the same bits on every machine. Amplitudes are held in 30-bit fixed point, and every output is an exact function of them.
  • exact: no approximation beyond double-precision rounding.
  • single: no approximation beyond single-precision rounding, about 10⁻⁷ relative per gate; sums are taken in double precision.
  • bounded: an approximation, with a rigorous bound on the error (bound) beside each value.
  • estimate: a statistical estimate, with its standard error (stderr).
  • heuristic: an approximation, reported with the standard estimate of its error but not a bound. A matrix product state that truncated reports its discarded weight and its fidelity estimate, and a warning when the estimate is below 0.9.

Counts are always seeded samples.

Endpoints

GET /v1 the service: its methods, limits, load and receipt key
POST /v1/run submit a job and wait up to 25 s: the finished job, or 202 and the job if it is still running
POST /v1/jobs submit a job: 202 and the job, with its id
GET /v1/jobs/{id} the job: its status, the request as submitted, and its result when it is done
DELETE /v1/jobs/{id} cancel a queued or running job
POST /v1/verify {"request": ..., "result": ...}: whether the result's receipt seals that request and that result
POST /v1/critique {"qasm"}: the program's measures (gates by kind, two-qubit gates, depth, T count, whether it is Clifford, a modelled fidelity) and its smells, each with its lines
POST /v1/equivalent {"a", "b"}: whether two programs are the same circuit up to a global phase, and how that was decided
POST /v1/improve {"qasm"}: the program with its smells fixed, each candidate with its measures and checked the same circuit
POST /v1/shortcuts {"qasm"}: each exact classical route to simulating the program, priced on it, and the cheapest for each question

Every refusal says why: 400 for a program or job that does not read, 422 for one larger than a job may be, 429 past the per-client limits, 503 when the queue is full. GET /v1 states the limits.

The four checks answer at once, in the service's own process. Each counts against the client's submissions, as a job does. At most two run at a time, and a check that waits ten seconds for its turn is refused (503). Some checks are refused (422) with the command that runs them on your own machine:

  • an equivalence check that would apply more than 4·10⁹ gate amplitudes;
  • an improvement whose candidates' checks would apply more than that;
  • a thorough search for shortcuts.

Receipts

Every finished result carries a receipt: an Ed25519 signature, by the key GET /v1 publishes, over the BLAKE3 digest of the request as submitted and the result as returned. POST /v1/verify checks one, and so can any Ed25519 implementation:

  • digest = BLAKE3(wai:quantum-service-receipt ‖ 0x02 ‖ len(R) ‖ R ‖ len(S) ‖ S), each len a big-endian u64;
  • R and S are the RFC 8785 canonical texts ("canon": "rfc8785") of the request and of the result without its receipt: keys sorted, no whitespace, every number written as ECMAScript writes the double it reads.

So a result verifies after any JSON reader has carried it, a browser's included. Whole numbers in a request must be within ±(2^53 − 1), so that every reader reads them alike. Receipts issued before the canonical form carry no canon and still verify (see src/receipt.rs).

Energy

Every finished result carries energy, inside the body the receipt seals: the CPU energy the job's worker process spent, from its start to its exit. The service reads it from the kernel's per-process count (proc_pid_rusage, ri_energy_nj, on macOS) after the worker exits and before it is reaped. An exited process has left every core, so its count is complete.

"energy": {
  "joules_micro": 2540113,
  "energy_provenance": "OnChipCounter",
  "energy_uncertainty": { "relative_ppm": 1, "window_us": 2603412 },
  "energy_counter_filtering": "Unknown",
  "coverage": "whole",
  "covers": "the CPU energy of the job's worker process, from its start to its exit, with the CPU energy system services billed to it",
  "excludes": ["memory", "the host's idle draw"],
  "cpu_ms": 31680,
  "source": "proc_pid_rusage ri_energy_nj + ri_billed_energy − ri_serviced_energy, read after the worker exited and before it was reaped"
}

The fields follow WAI's energy-measurement extension (§2):

  • energy_provenance is the acquisition class. OnChipCounter means the silicon's own accounting, with no calibration behind it.
  • energy_uncertainty.relative_ppm declares the part of the uncertainty that can be evaluated: here, the rounding to whole microjoules. It is not the figure's total error, because the counter's own systematic error is not evaluated.
  • window_us is the worker's lifetime.
  • energy_counter_filtering is Unknown, because the platform does not document whether the count is filtered.

A statevector_single or cutrank_single job runs on the GPU, which this counter does not read, so its figure is partial and says so in excludes. So is a figure that includes CPU energy system services billed to the worker, since such credits land late.

Where the service cannot read the count (off macOS), energy is {"unmetered": "<why>"}. It is never a figure of zero. wai-quantum-service run FILE runs a job in its own process and reports no energy.

Running it

cargo build --release
WAI_QSVC_ADDR=127.0.0.1:8241 ./target/release/wai-quantum-service serve
bash smoke.sh http://127.0.0.1:8241

Each job runs in a child process of the same binary (worker), at a lower scheduling priority than the host's other work, and is watched for its resident memory and its time.

These variables set the limits:

variable default meaning
WAI_QSVC_BUDGET_GB 96 memory all running jobs share
WAI_QSVC_JOB_GB 72 memory one job may take
WAI_QSVC_WORKERS 4 jobs running at once
WAI_QSVC_MIN_FREE_PCT 15 share of the host's memory a starting job must leave free
WAI_QSVC_JOB_SECS 900 the longest a job may run
WAI_QSVC_CLIENT_ACTIVE 3 queued or running jobs one client may hold
WAI_QSVC_CLIENT_BURST 30 submissions one client may make per window
WAI_QSVC_CLIENT_WINDOW_SECS 600 the window
WAI_QSVC_MAX_QUEUE 256 jobs waiting at once
WAI_QSVC_RETAIN_SECS 86400 how long results are kept
WAI_QSVC_TRUSTED_PROXIES 127.0.0.1,::1 peers whose X-Forwarded-For names the client
WAI_QSVC_KEY ~/.config/wai-quantum-service/signing.key the signing key, created on first start

wai-quantum-service run job.json runs one job in the current process and prints its plan and its result.