QuantumSink reads WQC1 and WQC2 containers. It reports the capability the magic names and the circuit hash. A refused container throws a message that names its error class, the word reject.json lists. QuantumIde builds in circuit2 by default. set_gateset(1) moves the current circuit and every later build to circuit/1, and gateset() and capability() report it. The debugger's prefix circuit keeps the IDE's gateset. add() stores no parameter on a gate other than P, so an editor's leftover parameter no longer makes a circuit2 circuit invalid. circuit_hash_hex() is empty for an invalid circuit. A circuit/1 circuit with P(k>=8) gets the command-line tool's note on the browser console, once per circuit state, never in a returned value. wai-quantum-serve takes gateset=v1 or v2 (default v2) on /run, /qasm and /receipt. /run names the capability in its JSON, and /run and /receipt name it in a wai-capability header. The /receipt body stays the receipt JSON alone, so it is still the bytes a native seal gives. An unknown gateset or a circuit with no circuit hash is a 400. Native tests cover both corpora, the reject corpus, the IDE's gateset switching, the add() parameter rule, the empty hash, and the serve endpoints against the corpus GHZ3 hashes. The wai-web native test count in CI moves from 24 to 31, and that workflow now triggers on changes to the quantum corpora it reads. Co-Authored-By: Claude Opus 5.5 <[email protected]>
578 lines
25 KiB
Rust
578 lines
25 KiB
Rust
//! The WAI quantum stack as a **WASI-HTTP component** — deterministic quantum
|
|
//! computation as a serverless function.
|
|
//!
|
|
//! This is the same Rust that runs natively and in the browser, exported through
|
|
//! `wasi:http/proxy` so any component-model host can serve it (`wasmtime serve`
|
|
//! and other WASI-HTTP runtimes). The results are not merely "equivalent": the
|
|
//! statevector hashes and the signed receipt bytes are **identical** to a native
|
|
//! run, because the computation is reproducible rather than negotiated.
|
|
//!
|
|
//! ```text
|
|
//! GET / service description
|
|
//! GET /health liveness
|
|
//! GET /algorithms the algorithm catalog
|
|
//! GET /run?algo=ghz&n=5&top=8 simulate; JSON with statevector hash + outcomes
|
|
//! POST /run body = OpenQASM 3.0
|
|
//! GET /qasm?algo=bell&n=2 emit OpenQASM 3.0
|
|
//! GET /receipt?algo=ghz&n=3 seal a signed, energy-accounted receipt
|
|
//! POST /verify body = receipt JSON → verdict
|
|
//! ```
|
|
//!
|
|
//! `/run` (both forms), `/qasm` and `/receipt` take `gateset=v2` (the default,
|
|
//! `wai.quantum.circuit2`) or `gateset=v1` (the frozen `wai.quantum.circuit`),
|
|
//! and build the circuit in it; any other value is a 400. The circuit hash, the
|
|
//! statevector and the receipt follow the gateset. `/run` names the capability
|
|
//! in its JSON as `"capability"`, and `/run` and `/receipt` name it in a
|
|
//! `wai-capability` response header. The `/receipt` body stays the receipt JSON
|
|
//! alone, so it is the same bytes a native run seals. A circuit whose hash
|
|
//! cannot be computed is a 400.
|
|
//!
|
|
//! `POST /verify` accepts a receipt in either form: as sealed, or with its
|
|
//! joule figure's acquisition class inside the signature
|
|
//! (`quantum_energy::Labelled`), and restates the class in its verdict. It
|
|
//! reports `signer_id` as the signer's own label, beside the level a
|
|
//! confirmation of the signer reached (`extensions/jwp-receipts.md` §7): this
|
|
//! handler holds no key log, so a valid receipt's signer is never more than
|
|
//! `unconfirmed`, with the reason it stopped at.
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use wai::quantum::{Circuit, Gateset, Measure};
|
|
use wai::quantum_energy::MaybeLabelled;
|
|
use wai::quantum_receipt::QuantumReceipt;
|
|
use wai::quantum_toolchain as qt;
|
|
use wasi::http::types::{
|
|
Fields, IncomingBody, IncomingRequest, Method, OutgoingBody, OutgoingResponse,
|
|
ResponseOutparam,
|
|
};
|
|
|
|
wasi::http::proxy::export!(Component);
|
|
struct Component;
|
|
|
|
/// The `POST /verify` verdict for a receipt JSON body: an HTTP status and a
|
|
/// JSON body — 200 valid, 409 invalid, 400 unparseable.
|
|
///
|
|
/// Either form verifies: a receipt as sealed, or one whose joule figure
|
|
/// carries its acquisition class inside the signature
|
|
/// (`quantum_energy::Labelled`, as the reference quantum meter emits). The
|
|
/// verdict restates the class and its evidence, or `"energy_provenance":null`
|
|
/// for a figure with no class.
|
|
///
|
|
/// A receipt's `joules_micro` of `0` is its unmetered marker, never a figure of
|
|
/// zero joules (energy-measurement §2), so the verdict renders it as
|
|
/// `"joules_micro":null` with `"metered":false`: a client totalling or comparing
|
|
/// the field cannot read an unmetered run as free work.
|
|
///
|
|
/// Beside `signer_id` the verdict reports `signer_level` and `signer_reason`
|
|
/// (jwp-receipts §7.1, §7.4): `unconfirmed` and `signer_id_not_origin_bound`
|
|
/// for a label or a `did:key`, `refused` and `signer_id_names_another_key`
|
|
/// for a `did:key` of another key, and `unconfirmed` and `origin_not_anchored`
|
|
/// for an origin-bound id, since the handler anchors no origin. Both are
|
|
/// `null` when the receipt is not valid.
|
|
fn verify_response(src: &str) -> (u16, String) {
|
|
let Some(any) = MaybeLabelled::<QuantumReceipt>::from_json(src) else {
|
|
return (400, "{\"valid\":false,\"reason\":\"unparseable receipt\"}".to_string());
|
|
};
|
|
let ok = any.verify();
|
|
let r = any.receipt();
|
|
let (joules, metered) = match r.joules_micro {
|
|
0 => ("null".to_string(), false),
|
|
j => (j.to_string(), true),
|
|
};
|
|
let signer = if ok {
|
|
let (level, reason) = wai::signer_id::level_without_anchors(&r.signer_id, &r.signer_pubkey);
|
|
format!("\"signer_level\":\"{}\",\"signer_reason\":\"{reason}\"", level.label())
|
|
} else {
|
|
"\"signer_level\":null,\"signer_reason\":null".to_string()
|
|
};
|
|
let class = match any.energy_class() {
|
|
None => "\"energy_provenance\":null".to_string(),
|
|
Some(c) => c.json_fields().iter().map(|(k, v)| format!("\"{k}\":{v}")).collect::<Vec<_>>().join(","),
|
|
};
|
|
(
|
|
if ok { 200 } else { 409 },
|
|
format!(
|
|
"{{\"valid\":{},\"circuit_hash\":\"{}\",\"statevector_hash\":\"{}\",\
|
|
\"n_qubits\":{},\"n_ops\":{},\"work_amp_updates\":{},\"joules_micro\":{joules},\"metered\":{metered},\"signer_id\":\"{}\",{signer},{class}}}",
|
|
ok,
|
|
hx(&r.circuit_hash),
|
|
hx(&r.statevector_hash),
|
|
r.n_qubits,
|
|
r.n_ops,
|
|
r.work_amp_updates,
|
|
esc(&r.signer_id)
|
|
),
|
|
)
|
|
}
|
|
|
|
fn hx(b: &[u8]) -> String {
|
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
|
}
|
|
|
|
/// Minimal JSON string escaping for the values we emit (ids, names, prose).
|
|
fn esc(s: &str) -> String {
|
|
let mut o = String::with_capacity(s.len() + 8);
|
|
for c in s.chars() {
|
|
match c {
|
|
'"' => o.push_str("\\\""),
|
|
'\\' => o.push_str("\\\\"),
|
|
'\n' => o.push_str("\\n"),
|
|
'\r' => o.push_str("\\r"),
|
|
'\t' => o.push_str("\\t"),
|
|
c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)),
|
|
c => o.push(c),
|
|
}
|
|
}
|
|
o
|
|
}
|
|
|
|
/// The response header that names the capability a circuit was built in.
|
|
const CAPABILITY_HEADER: &str = "wai-capability";
|
|
|
|
fn send(out: ResponseOutparam, status: u16, ctype: &str, capability: Option<&str>, body: &[u8]) {
|
|
let hdrs = Fields::new();
|
|
let _ = hdrs.append("content-type", &ctype.as_bytes().to_vec());
|
|
if let Some(c) = capability {
|
|
let _ = hdrs.append(CAPABILITY_HEADER, &c.as_bytes().to_vec());
|
|
}
|
|
let resp = OutgoingResponse::new(hdrs);
|
|
let _ = resp.set_status_code(status);
|
|
let Ok(rbody) = resp.body() else { return };
|
|
ResponseOutparam::set(out, Ok(resp));
|
|
if let Ok(stream) = rbody.write() {
|
|
// chunk the write: blocking_write_and_flush caps at the stream's permit
|
|
for chunk in body.chunks(4000) {
|
|
if stream.blocking_write_and_flush(chunk).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
drop(stream);
|
|
}
|
|
let _ = OutgoingBody::finish(rbody, None);
|
|
}
|
|
|
|
fn json(out: ResponseOutparam, status: u16, body: String) {
|
|
send(out, status, "application/json", None, body.as_bytes());
|
|
}
|
|
|
|
/// The JSON body of an error response.
|
|
fn err_body(msg: &str) -> String {
|
|
format!("{{\"error\":\"{}\"}}", esc(msg))
|
|
}
|
|
|
|
fn err(out: ResponseOutparam, status: u16, msg: &str) {
|
|
json(out, status, err_body(msg));
|
|
}
|
|
|
|
/// An answer computed apart from the WASI types, so the native tests can check
|
|
/// it: the status, the JSON body and, for a circuit that was built, the
|
|
/// capability it was built in.
|
|
struct Reply {
|
|
status: u16,
|
|
body: String,
|
|
capability: Option<&'static str>,
|
|
}
|
|
|
|
impl Reply {
|
|
fn error(status: u16, msg: &str) -> Reply {
|
|
Reply { status, body: err_body(msg), capability: None }
|
|
}
|
|
fn send(self, out: ResponseOutparam) {
|
|
send(out, self.status, "application/json", self.capability, self.body.as_bytes());
|
|
}
|
|
}
|
|
|
|
fn read_body(body: IncomingBody) -> Vec<u8> {
|
|
let mut buf = Vec::new();
|
|
if let Ok(stream) = body.stream() {
|
|
loop {
|
|
match stream.blocking_read(8192) {
|
|
Ok(chunk) if chunk.is_empty() => break,
|
|
Ok(chunk) => {
|
|
buf.extend_from_slice(&chunk);
|
|
if buf.len() > 4 << 20 {
|
|
break; // 4 MiB cap
|
|
}
|
|
}
|
|
Err(_) => break, // stream closed = EOF
|
|
}
|
|
}
|
|
}
|
|
buf
|
|
}
|
|
|
|
/// `name=value` pairs from a `path?query` string (percent-decoding `%XX` and `+`).
|
|
fn query(pq: &str, key: &str) -> Option<String> {
|
|
let q = pq.split_once('?')?.1;
|
|
for pair in q.split('&') {
|
|
let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
|
|
if k == key {
|
|
return Some(pct(v));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn pct(s: &str) -> String {
|
|
let b = s.as_bytes();
|
|
let mut o = Vec::with_capacity(b.len());
|
|
let mut i = 0;
|
|
while i < b.len() {
|
|
match b[i] {
|
|
b'%' if i + 2 < b.len() => {
|
|
let hi = (b[i + 1] as char).to_digit(16);
|
|
let lo = (b[i + 2] as char).to_digit(16);
|
|
match (hi, lo) {
|
|
(Some(h), Some(l)) => {
|
|
o.push((h * 16 + l) as u8);
|
|
i += 3;
|
|
}
|
|
_ => {
|
|
o.push(b[i]);
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
b'+' => {
|
|
o.push(b' ');
|
|
i += 1;
|
|
}
|
|
c => {
|
|
o.push(c);
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
String::from_utf8_lossy(&o).into_owned()
|
|
}
|
|
|
|
fn qnum<T: std::str::FromStr>(pq: &str, key: &str, dflt: T) -> T {
|
|
query(pq, key).and_then(|v| v.parse().ok()).unwrap_or(dflt)
|
|
}
|
|
|
|
/// The gateset `?gateset=` names: `v2` (`wai.quantum.circuit2`, the default)
|
|
/// or `v1` (the frozen `wai.quantum.circuit`). Any other value is an error.
|
|
fn gateset(pq: &str) -> Result<Gateset, String> {
|
|
match query(pq, "gateset").as_deref() {
|
|
None | Some("v2") => Ok(Gateset::V2),
|
|
Some("v1") => Ok(Gateset::V1),
|
|
Some(other) => Err(format!("unknown gateset '{other}' (v1, v2)")),
|
|
}
|
|
}
|
|
|
|
/// Move a built circuit into the gateset the request names. The builders make
|
|
/// circuit2 circuits with distinct controls and no parameter on a gate other
|
|
/// than `P`, which are valid circuit/1 circuits too.
|
|
fn in_gateset(mut c: Circuit, pq: &str) -> Result<Circuit, String> {
|
|
c.gateset = gateset(pq)?;
|
|
Ok(c)
|
|
}
|
|
|
|
fn build(pq: &str) -> Result<Circuit, String> {
|
|
let algo = query(pq, "algo").ok_or("missing ?algo= (see /algorithms)")?;
|
|
let n: u8 = qnum(pq, "n", 3);
|
|
let p: u32 = qnum(pq, "p", 0);
|
|
let c = qt::build_algorithm(&algo, n, p).ok_or(format!("unknown algorithm '{algo}'"))?;
|
|
in_gateset(c, pq)
|
|
}
|
|
|
|
/// The simulation result as JSON — the same fields the CLI prints.
|
|
fn run_json(c: &Circuit, top: usize) -> Result<String, String> {
|
|
c.validate().map_err(|e| format!("invalid circuit: {e:?}"))?;
|
|
let id = qt::circuit_id(c).map_err(|e| format!("circuit id: {e}"))?;
|
|
let sv = c.simulate().map_err(|e| format!("simulate: {e:?}"))?;
|
|
let (backend, why) = qt::recommend_backend(c);
|
|
let n = c.n_qubits as usize;
|
|
let items: Vec<String> = qt::top_outcomes(&sv, top)
|
|
.iter()
|
|
.map(|(k, p)| format!("{{\"state\":\"{k:0n$b}\",\"p\":{p}}}", n = n))
|
|
.collect();
|
|
Ok(format!(
|
|
"{{\"circuit_id\":\"{}\",\"capability\":\"{}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend:?}\",\
|
|
\"backend_reason\":\"{}\",\"work_amp_updates\":{},\"statevector_hash\":\"{}\",\"top\":[{}]}}",
|
|
hx(&id),
|
|
c.gateset.capability(),
|
|
c.n_qubits,
|
|
c.ops.len(),
|
|
esc(why),
|
|
qt::statevector_work(c),
|
|
hx(&sv.statevector_hash()),
|
|
items.join(",")
|
|
))
|
|
}
|
|
|
|
/// `/run`: 200 and the run JSON for the circuit the request built, or 400.
|
|
fn run_reply(built: Result<Circuit, String>, top: usize) -> Reply {
|
|
match built.and_then(|c| run_json(&c, top).map(|body| (c.gateset, body))) {
|
|
Ok((gs, body)) => Reply { status: 200, body, capability: Some(gs.capability()) },
|
|
Err(e) => Reply::error(400, &e),
|
|
}
|
|
}
|
|
|
|
/// `/receipt`: 200 and the sealed receipt JSON, or 400 for a request that
|
|
/// names no circuit or an unknown gateset, or a circuit the seal refuses.
|
|
fn receipt_reply(pq: &str) -> Reply {
|
|
let c = match build(pq) {
|
|
Ok(c) => c,
|
|
Err(e) => return Reply::error(400, &e),
|
|
};
|
|
let shots: u64 = qnum(pq, "shots", 0);
|
|
let seed: u64 = qnum(pq, "seed", 0);
|
|
seal_reply(&c, (shots > 0).then_some(Measure { seed, shots }))
|
|
}
|
|
|
|
/// Seal `c`: 200 and the receipt JSON, or 400 when the seal refuses the
|
|
/// circuit. Every refusal is the circuit's own: one its gateset does not allow,
|
|
/// so it has no circuit hash, or one too large to simulate.
|
|
fn seal_reply(c: &Circuit, measure: Option<Measure>) -> Reply {
|
|
// Deterministic demo key so the sealed bytes match a native run.
|
|
let key = SigningKey::from_bytes(&[42u8; 32]);
|
|
match QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", c, measure, 0, None) {
|
|
Ok(r) => Reply { status: 200, body: r.to_json(), capability: Some(c.gateset.capability()) },
|
|
Err(e) => Reply::error(400, &format!("seal: {e}")),
|
|
}
|
|
}
|
|
|
|
impl wasi::exports::http::incoming_handler::Guest for Component {
|
|
fn handle(req: IncomingRequest, out: ResponseOutparam) {
|
|
let method = req.method();
|
|
let pq = req.path_with_query().unwrap_or_else(|| "/".into());
|
|
let path = pq.split('?').next().unwrap_or("/").to_string();
|
|
let is_post = matches!(method, Method::Post);
|
|
|
|
match (is_post, path.as_str()) {
|
|
(false, "/") => json(
|
|
out,
|
|
200,
|
|
"{\"service\":\"wai-quantum\",\"kind\":\"wasi:http/proxy component\",\
|
|
\"deterministic\":true,\"note\":\"statevector hashes and receipt bytes are identical to a native run\",\
|
|
\"gateset\":{\"default\":\"v2\",\"v2\":\"wai.quantum.circuit2\",\"v1\":\"wai.quantum.circuit\"},\
|
|
\"endpoints\":[\"/health\",\"/algorithms\",\"/run?algo=ghz&n=5\",\"/qasm?algo=bell\",\"/receipt?algo=ghz&n=3\",\"POST /run (OpenQASM)\",\"POST /verify (receipt JSON)\"]}"
|
|
.to_string(),
|
|
),
|
|
|
|
(false, "/health") => json(out, 200, "{\"ok\":true}".to_string()),
|
|
|
|
(false, "/algorithms") => {
|
|
let items: Vec<String> = qt::catalog()
|
|
.iter()
|
|
.map(|(id, name, desc)| {
|
|
format!(
|
|
"{{\"id\":\"{}\",\"name\":\"{}\",\"description\":\"{}\"}}",
|
|
esc(id),
|
|
esc(name),
|
|
esc(desc)
|
|
)
|
|
})
|
|
.collect();
|
|
json(out, 200, format!("{{\"algorithms\":[{}]}}", items.join(",")));
|
|
}
|
|
|
|
(false, "/run") => run_reply(build(&pq), qnum(&pq, "top", 8usize)).send(out),
|
|
|
|
(true, "/run") => {
|
|
let top = qnum(&pq, "top", 8usize);
|
|
let Ok(body) = req.consume() else {
|
|
return err(out, 400, "no body");
|
|
};
|
|
let src = String::from_utf8_lossy(&read_body(body)).into_owned();
|
|
let built = qt::from_qasm(&src).map_err(|e| format!("QASM parse: {e}"));
|
|
run_reply(built.and_then(|c| in_gateset(c, &pq)), top).send(out)
|
|
}
|
|
|
|
(false, "/qasm") => match build(&pq) {
|
|
Err(e) => err(out, 400, &e),
|
|
Ok(c) => send(out, 200, "text/plain; charset=utf-8", None, qt::to_qasm(&c).as_bytes()),
|
|
},
|
|
|
|
(false, "/receipt") => receipt_reply(&pq).send(out),
|
|
|
|
(true, "/verify") => {
|
|
let Ok(body) = req.consume() else {
|
|
return err(out, 400, "no body");
|
|
};
|
|
let src = String::from_utf8_lossy(&read_body(body)).into_owned();
|
|
let (status, verdict) = verify_response(&src);
|
|
json(out, status, verdict)
|
|
}
|
|
|
|
_ => err(out, 404, "not found — GET / for the endpoint list"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use wai::quantum::{circuit_hash, BaseGate, Gate};
|
|
use wai::quantum_energy::{CounterFiltering, EnergyClass, Labelled, Uncertainty};
|
|
|
|
fn ghz3() -> Circuit {
|
|
let mut c = Circuit::new(3);
|
|
c.h(0).cx(0, 1).cx(1, 2);
|
|
c
|
|
}
|
|
|
|
/// GHZ3's circuit and statevector hashes in each capability, as the
|
|
/// conformance corpora state them (`v2/expected_hashes.json` and
|
|
/// `expected_hashes.json`).
|
|
const GHZ3_V2: (&str, &str) = (
|
|
"c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f",
|
|
"51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8",
|
|
);
|
|
const GHZ3_V1: (&str, &str) = (
|
|
"34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c",
|
|
"08c5e6fb50b91a00d28795506cc5d99ce1e9a3a8b4c41e4bfd05f5d829af9542",
|
|
);
|
|
|
|
#[test]
|
|
fn the_gateset_parameter_defaults_to_circuit2_and_names_no_other() {
|
|
assert_eq!(gateset("/run?algo=bell"), Ok(Gateset::V2));
|
|
assert_eq!(gateset("/run?algo=bell&gateset=v2"), Ok(Gateset::V2));
|
|
assert_eq!(gateset("/run?gateset=v1&algo=bell"), Ok(Gateset::V1));
|
|
for bad in ["v3", "V1", "", "circuit2", "wai.quantum.circuit"] {
|
|
let e = gateset(&format!("/run?algo=bell&gateset={bad}")).unwrap_err();
|
|
assert!(e.contains("unknown gateset"), "{bad}: {e}");
|
|
}
|
|
// The OpenQASM body of `POST /run` takes the query's gateset too.
|
|
let c = qt::from_qasm("OPENQASM 3.0;\nqubit[3] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\n").unwrap();
|
|
assert_eq!(in_gateset(c.clone(), "/run?gateset=v1").unwrap().gateset, Gateset::V1);
|
|
assert_eq!(in_gateset(c.clone(), "/run").unwrap().gateset, Gateset::V2);
|
|
assert!(in_gateset(c, "/run?gateset=v0").is_err());
|
|
}
|
|
|
|
/// `/run` builds in the gateset the request names and says which, in its
|
|
/// JSON and its header; its hashes are the corpus's for that capability.
|
|
#[test]
|
|
fn run_names_its_capability_and_reproduces_the_corpus() {
|
|
for (q, capability, (circuit, statevector)) in [
|
|
("/run?algo=ghz&n=3", "wai.quantum.circuit2", GHZ3_V2),
|
|
("/run?algo=ghz&n=3&gateset=v2", "wai.quantum.circuit2", GHZ3_V2),
|
|
("/run?algo=ghz&n=3&gateset=v1", "wai.quantum.circuit", GHZ3_V1),
|
|
] {
|
|
let r = run_reply(build(q), 8);
|
|
assert_eq!((r.status, r.capability), (200, Some(capability)), "{q}: {}", r.body);
|
|
assert!(
|
|
r.body.starts_with(&format!("{{\"circuit_id\":\"{circuit}\",\"capability\":\"{capability}\",\"n_qubits\":3,")),
|
|
"{q}: {}",
|
|
r.body
|
|
);
|
|
assert!(r.body.contains(&format!("\"statevector_hash\":\"{statevector}\"")), "{q}: {}", r.body);
|
|
}
|
|
let r = run_reply(build("/run?algo=ghz&n=3&gateset=v9"), 8);
|
|
assert_eq!((r.status, r.capability), (400, None));
|
|
assert_eq!(r.body, "{\"error\":\"unknown gateset 'v9' (v1, v2)\"}");
|
|
}
|
|
|
|
/// A circuit with no circuit hash is a 400 from `/run` and from the seal,
|
|
/// never a 500 or a panic. A repeated control is such a circuit in
|
|
/// circuit2, and an ordinary one in circuit/1.
|
|
#[test]
|
|
fn a_circuit_with_no_hash_is_a_bad_request() {
|
|
let mut c = Circuit::new(3);
|
|
c.h(0);
|
|
c.ops.push(Gate { base: BaseGate::X, controls: vec![0, 0], target: 1, param: 0 });
|
|
assert!(circuit_hash(&c).is_err());
|
|
let r = run_reply(Ok(c.clone()), 8);
|
|
assert_eq!((r.status, r.capability), (400, None), "{}", r.body);
|
|
assert!(r.body.contains("invalid circuit"), "{}", r.body);
|
|
let r = seal_reply(&c, None);
|
|
assert_eq!((r.status, r.capability), (400, None), "{}", r.body);
|
|
assert!(r.body.starts_with("{\"error\":\"seal: invalid circuit: "), "{}", r.body);
|
|
|
|
c.gateset = Gateset::V1;
|
|
assert!(circuit_hash(&c).is_ok());
|
|
assert_eq!(run_reply(Ok(c.clone()), 8).status, 200);
|
|
assert_eq!(seal_reply(&c, None).status, 200);
|
|
}
|
|
|
|
/// `/receipt` seals in the gateset the request names. Its body is the
|
|
/// receipt JSON alone, the bytes a native seal of the same circuit gives;
|
|
/// the capability rides in the header.
|
|
#[test]
|
|
fn receipt_seals_in_the_requested_gateset() {
|
|
for (q, gs, circuit) in [
|
|
("/receipt?algo=ghz&n=3&shots=64&seed=7", Gateset::V2, GHZ3_V2.0),
|
|
("/receipt?algo=ghz&n=3&shots=64&seed=7&gateset=v1", Gateset::V1, GHZ3_V1.0),
|
|
] {
|
|
let r = receipt_reply(q);
|
|
assert_eq!((r.status, r.capability), (200, Some(gs.capability())), "{q}: {}", r.body);
|
|
let mut c = ghz3();
|
|
c.gateset = gs;
|
|
let key = SigningKey::from_bytes(&[42u8; 32]);
|
|
let native = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, Some(Measure { seed: 7, shots: 64 }), 0, None).unwrap();
|
|
assert_eq!(r.body, native.to_json(), "{q}");
|
|
assert!(!r.body.contains("capability"), "{q}");
|
|
let parsed = QuantumReceipt::from_json(&r.body).unwrap();
|
|
assert!(parsed.verify());
|
|
assert_eq!(hx(&parsed.circuit_hash), circuit, "{q}");
|
|
assert!(parsed.verify_reconstruction(&c));
|
|
}
|
|
let r = receipt_reply("/receipt?algo=ghz&n=3&gateset=2");
|
|
assert_eq!((r.status, r.capability), (400, None));
|
|
assert!(r.body.contains("unknown gateset"), "{}", r.body);
|
|
assert_eq!(receipt_reply("/receipt?algo=nope").status, 400);
|
|
}
|
|
|
|
#[test]
|
|
fn verify_accepts_both_forms_and_restates_the_class() {
|
|
let key = SigningKey::from_bytes(&[0x71u8; 32]);
|
|
// As `/receipt` seals it: unmetered, unlabelled.
|
|
let r0 = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &ghz3(), None, 0, None).unwrap();
|
|
let (status, body) = verify_response(&r0.to_json());
|
|
assert_eq!(status, 200, "{body}");
|
|
assert!(body.contains("\"valid\":true") && body.ends_with("\"energy_provenance\":null}"), "{body}");
|
|
// The signer's id is a label: a valid receipt, an unconfirmed signer.
|
|
assert!(
|
|
body.contains("\"signer_id\":\"did:key:wai-quantum-cli\",\"signer_level\":\"unconfirmed\",\"signer_reason\":\"signer_id_not_origin_bound\""),
|
|
"{body}"
|
|
);
|
|
|
|
// As the reference quantum meter emits it: a 1 µJ figure labelled
|
|
// OnChipCounter, declaring its own rounding.
|
|
let r1 = QuantumReceipt::seal(&key, "did:web:meter", &ghz3(), None, 1, None).unwrap();
|
|
let class = EnergyClass::OnChipCounter {
|
|
uncertainty: Uncertainty { relative_ppm: 288_830, window_us: 2_930_000 },
|
|
filtering: CounterFiltering::Unknown,
|
|
};
|
|
let l = Labelled::seal(r1, class, &key).unwrap();
|
|
let j = l.to_json();
|
|
let (status, body) = verify_response(&j);
|
|
assert_eq!(status, 200, "{body}");
|
|
assert!(body.contains("\"energy_provenance\":\"OnChipCounter\""), "{body}");
|
|
assert!(body.contains("\"relative_ppm\":288830") && body.contains("\"energy_counter_filtering\":\"Unknown\""));
|
|
// The unlabelled parse alone refuses it: what this handler did before.
|
|
assert!(!QuantumReceipt::from_json(&j).unwrap().verify());
|
|
|
|
// A restated label is refused, and the signer is then reported with no
|
|
// level; anything unparseable is refused.
|
|
let (status, body) = verify_response(&j.replace("288830", "288831"));
|
|
assert_eq!(status, 409);
|
|
assert!(body.contains("\"signer_level\":null,\"signer_reason\":null"), "{body}");
|
|
assert_eq!(verify_response("not a receipt").0, 400);
|
|
|
|
// An origin-bound id: valid, but no origin is anchored here.
|
|
let r2 = QuantumReceipt::seal(&key, "did:web:publisher.example#q1", &ghz3(), None, 0, None).unwrap();
|
|
let (status, body) = verify_response(&r2.to_json());
|
|
assert_eq!(status, 200, "{body}");
|
|
assert!(body.contains("\"signer_level\":\"unconfirmed\",\"signer_reason\":\"origin_not_anchored\""), "{body}");
|
|
// A did:key of another key contradicts the signer_pubkey beside it.
|
|
let other = wai::signer_id::did_key(&SigningKey::from_bytes(&[0x72u8; 32]).verifying_key().to_bytes());
|
|
let r3 = QuantumReceipt::seal(&key, &other, &ghz3(), None, 0, None).unwrap();
|
|
let (_, body) = verify_response(&r3.to_json());
|
|
assert!(body.contains("\"signer_level\":\"refused\",\"signer_reason\":\"signer_id_names_another_key\""), "{body}");
|
|
}
|
|
|
|
/// An unmetered receipt's `0` is the unmetered marker: the verdict never
|
|
/// presents it as a figure of zero joules.
|
|
#[test]
|
|
fn an_unmetered_receipt_is_never_reported_as_zero_joules() {
|
|
let key = SigningKey::from_bytes(&[0x71u8; 32]);
|
|
let r0 = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &ghz3(), None, 0, None).unwrap();
|
|
let (_, body) = verify_response(&r0.to_json());
|
|
assert!(body.contains("\"joules_micro\":null,\"metered\":false"), "{body}");
|
|
assert!(!body.contains("\"joules_micro\":0"), "{body}");
|
|
let r1 = QuantumReceipt::seal(&key, "did:web:meter", &ghz3(), None, 7, None).unwrap();
|
|
let (_, body) = verify_response(&r1.to_json());
|
|
assert!(body.contains("\"joules_micro\":7,\"metered\":true"), "{body}");
|
|
}
|
|
}
|