open-standards/wai/wai-quantum/src/quantum_qbom.rs
Transaction Science e531c2ddd7
Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (push) Successful in 3s
standards workspaces build + test / aac-rs (push) Successful in 37s
standards workspaces build + test / joule-code-rs (push) Successful in 42s
standards workspaces build + test / eoc-rs (push) Successful in 1m47s
standards workspaces build + test / jcp-rs (push) Successful in 1m14s
standards workspaces build + test / proof-rs (push) Successful in 1m9s
standards workspaces build + test / jouleclaw-rs (push) Successful in 4m6s
standards workspaces build + test / joulecontract-rs (push) Successful in 38s
standards workspaces build + test / sandbox-rs (push) Successful in 2m1s
standards workspaces build + test / map-rs (push) Successful in 36s
standards workspaces build + test / mesh-rs (push) Successful in 34s
standards workspaces build + test / openpay (push) Successful in 1m23s
standards workspaces build + test / smart-byte-rs (push) Successful in 2m28s
WAI conformance / cargo-class corpora · linux-arm64 (push) Successful in 33s
WAI conformance / cargo-class corpora · linux (push) Successful in 28s
WAI conformance / cross-arch byte-equality · linux-arm64 (push) Successful in 17s
WAI conformance / cross-arch byte-equality · linux (push) Successful in 14s
standards workspaces build + test / arl-rs (push) Successful in 8m41s
standards workspaces build + test / joule-ui-rs (push) Successful in 7m56s
standards workspaces build + test / jouledesk-rs (push) Successful in 8m24s
standards workspaces build + test / joulehook-rs (push) Successful in 8m34s
standards workspaces build + test / sense-rs (push) Successful in 7m58s
WAI conformance / cargo-class corpora · macos (push) Successful in 1m4s
WAI conformance / cross-arch byte-equality · macos (push) Successful in 23s
WAI conformance / cargo-class corpora · windows (push) Has been cancelled
WAI conformance / cross-arch byte-equality · windows (push) Has been cancelled
wai: extract the quantum stack into a standalone wai-quantum crate
crates.io publish was blocked: the name `wai` is taken (an unrelated 2017 crate),
and `default = ["codecs"]` would make `cargo add` pull libjxl/libavif/opus/FLAC/
rav1e/dav1d system libraries — so most builds would fail for someone who only
wants quantum. Extracting the quantum stack solves both.

`wai-quantum` is dependency-light and pure Rust (blake3 + serde_json, with
ed25519-dalek only for the sealing features). No system libraries, so it builds
everywhere the stack already runs: native, browser wasm, and wasm32-wasip2.

The split follows the seam that was already there — 12,975 lines moved with only
four coupling points:
- `world:🧾:merkle_root` (used by 3 modules) is reproduced verbatim in
  `wai-quantum::merkle`, domain separators included, so every previously issued
  receipt and published conformance vector still verifies;
- `wrap_envelope` is a WAI-envelope concern, not a quantum one, so it stays in
  `wai` as `quantum_envelope` (it had no external callers).

`wai` keeps every quantum feature name and re-exports the modules unchanged, so
wai-web, wai-quantum-serve and the deployed site build without edits. Also
re-exports `ed25519_dalek` from wai-quantum: the sealing API takes a `SigningKey`,
and a consumer picking their own version could get an incompatible one.

Verified the extraction is observably a no-op — identical to the values recorded
before it:
  ghz -n 5      circuit-id 355d1166…  state-hash 0b5f4201…
  grover -n 4 -p 9            state-hash 25a7d930…
  receipt JSON sha256         117ff588…
208 tests pass (173 wai-quantum + 35 wai); wai-web and the WASI-HTTP component
build unchanged, and the edge endpoint still returns the identical hash.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-03 15:29:50 -04:00

370 lines
14 KiB
Rust

//! Quantum Bill of Materials — `wai.quantum.qbom`
//! (extensions/quantum-ops § Provenance).
//!
//! An open quantum operations stack runs the job — submits the program, manages the
//! device, returns counts. What it does not do is **sign what the result is made
//! of**: which compiled circuit ran (and that it was equivalent to the source),
//! against which calibration, under which learned noise model, with which error
//! mitigation, on a register prepared how. Each of those stages, in this repo,
//! already emits a signed receipt. A **Quantum Bill of Materials** assembles the
//! stage receipts of one computation into a single signed, content-addressed
//! **provenance manifest** — the audit sidecar the operations layer doesn't itself
//! produce.
//!
//! The QBOM lists each stage by `(kind, receipt_hash, joules)`, binds them to a
//! job identity and program hash under one signature, and exposes a **provenance
//! root** (a content hash over the whole manifest) plus the **total energy** across
//! stages. Verification is layered: the QBOM verifies its own signature and root;
//! presented alongside the actual stage receipts, [`Qbom::binds`] confirms each
//! listed hash matches a receipt that itself verifies — so a swapped or forged
//! stage breaks the bill. It is byte-exact and reproducible: the same computation
//! rebuilds the same root from the git repo.
//!
//! This is a manifest, not a new physics claim — it inherits exactly the honesty
//! of the receipts it bundles, and adds the property that a quantum result now
//! carries a single verifiable statement of everything it was made of.
use crate::quantum_ops::content_hash;
use ed25519_dalek::{Signature, SigningKey, Verifier, VerifyingKey};
const DOMAIN_QBOM: &[u8] = b"wai:quantum-qbom\x01";
const DOMAIN_QBOM_ID: &[u8] = b"wai:quantum-qbom-id\x01";
/// The canonical stages of a quantum computation, in pipeline order. A QBOM need
/// not carry all of them; `Qbom::completeness` reports which are present.
pub const CANONICAL_STAGES: &[&str] = &[
"compile",
"rearrange",
"calibrate",
"noise_learn",
"execute",
"mitigate",
"decode",
];
fn hx(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn from_hex32(s: &str) -> Option<[u8; 32]> {
if s.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, c) in s.as_bytes().as_chunks::<2>().0.iter().enumerate() {
out[i] = u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok()?;
}
Some(out)
}
fn from_hex64(s: &str) -> Option<[u8; 64]> {
if s.len() != 128 {
return None;
}
let mut out = [0u8; 64];
for (i, c) in s.as_bytes().as_chunks::<2>().0.iter().enumerate() {
out[i] = u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok()?;
}
Some(out)
}
/// One line item: a stage of the computation, referenced by its receipt hash.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QbomEntry {
pub kind: String,
pub receipt_hash: [u8; 32],
pub joules_micro: u64,
}
/// A signed Quantum Bill of Materials for one computation.
#[derive(Clone, Debug, PartialEq)]
pub struct Qbom {
pub job_id: String,
pub program_hash: [u8; 32],
pub device_id: String,
pub entries: Vec<QbomEntry>,
pub signer_pubkey: [u8; 32],
pub signer_id: String,
pub sig: [u8; 64],
}
/// Builder for a QBOM before it is sealed.
#[derive(Clone, Debug)]
pub struct QbomBuilder {
job_id: String,
program_hash: [u8; 32],
device_id: String,
entries: Vec<QbomEntry>,
}
impl QbomBuilder {
pub fn new(job_id: impl Into<String>, program_hash: [u8; 32], device_id: impl Into<String>) -> QbomBuilder {
QbomBuilder {
job_id: job_id.into(),
program_hash,
device_id: device_id.into(),
entries: Vec::new(),
}
}
/// Add a stage by its receipt hash and metered energy. Chainable.
pub fn stage(mut self, kind: impl Into<String>, receipt_hash: [u8; 32], joules_micro: u64) -> QbomBuilder {
self.entries.push(QbomEntry { kind: kind.into(), receipt_hash, joules_micro });
self
}
pub fn seal(self, signer: &SigningKey, signer_id: impl Into<String>) -> Qbom {
let mut q = Qbom {
job_id: self.job_id,
program_hash: self.program_hash,
device_id: self.device_id,
entries: self.entries,
signer_pubkey: signer.verifying_key().to_bytes(),
signer_id: signer_id.into(),
sig: [0u8; 64],
};
q.sig = signer.sign_payload(&q.signing_payload());
q
}
}
// small helper so we don't need the Signer trait import name-clash
trait SignPayload {
fn sign_payload(&self, msg: &[u8]) -> [u8; 64];
}
impl SignPayload for SigningKey {
fn sign_payload(&self, msg: &[u8]) -> [u8; 64] {
use ed25519_dalek::Signer;
self.sign(msg).to_bytes()
}
}
impl Qbom {
/// Canonical bytes of the manifest body (everything but the signature).
fn body(&self) -> Vec<u8> {
let mut o = DOMAIN_QBOM.to_vec();
o.extend_from_slice(&(self.job_id.len() as u32).to_le_bytes());
o.extend_from_slice(self.job_id.as_bytes());
o.extend_from_slice(&self.program_hash);
o.extend_from_slice(&(self.device_id.len() as u32).to_le_bytes());
o.extend_from_slice(self.device_id.as_bytes());
o.extend_from_slice(&(self.entries.len() as u32).to_le_bytes());
for e in &self.entries {
o.extend_from_slice(&(e.kind.len() as u32).to_le_bytes());
o.extend_from_slice(e.kind.as_bytes());
o.extend_from_slice(&e.receipt_hash);
o.extend_from_slice(&e.joules_micro.to_le_bytes());
}
o
}
/// The provenance root: a content hash over the whole manifest body. Stable
/// and reproducible — the same computation rebuilds the same root.
pub fn provenance_root(&self) -> [u8; 32] {
content_hash(&self.body())
}
fn signing_payload(&self) -> Vec<u8> {
let mut o = self.body();
o.extend_from_slice(&self.signer_pubkey);
o.extend_from_slice(self.signer_id.as_bytes());
o
}
/// Total metered energy across all stages (µJ).
pub fn total_joules_micro(&self) -> u64 {
self.entries.iter().map(|e| e.joules_micro).sum()
}
/// Which canonical stages this bill carries, in pipeline order.
pub fn completeness(&self) -> Vec<String> {
CANONICAL_STAGES
.iter()
.filter(|s| self.entries.iter().any(|e| e.kind == **s))
.map(|s| s.to_string())
.collect()
}
/// The manifest's own signature is valid.
pub fn verify(&self) -> bool {
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
return false;
};
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
.is_ok()
}
/// Given the actual stage receipts as `(receipt_hash, receipt_verifies)`, does
/// the bill bind exactly them? Every entry must match a presented receipt that
/// itself verifies, and every presented receipt must be listed. A swapped,
/// forged, or failed-to-verify stage breaks it.
pub fn binds(&self, receipts: &[([u8; 32], bool)]) -> bool {
if !self.verify() || receipts.len() != self.entries.len() {
return false;
}
// each entry hash appears among the presented receipts, and that receipt verifies
self.entries.iter().all(|e| {
receipts.iter().any(|(h, ok)| *h == e.receipt_hash && *ok)
})
}
pub fn receipt_hash(&self) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(DOMAIN_QBOM_ID);
h.update(&self.signing_payload());
h.update(&self.sig);
*h.finalize().as_bytes()
}
pub fn to_json(&self) -> String {
let entries: Vec<String> = self
.entries
.iter()
.map(|e| {
format!(
"{{\"joules_micro\":{},\"kind\":{},\"receipt_hash\":\"{}\"}}",
e.joules_micro,
serde_json::to_string(&e.kind).unwrap(),
hx(&e.receipt_hash)
)
})
.collect();
format!(
"{{\"kind\":\"quantum-qbom\",\"device_id\":{},\"entries\":[{}],\"job_id\":{},\
\"program_hash\":\"{}\",\"provenance_root\":\"{}\",\"receipt_hash\":\"{}\",\"sig\":\"{}\",\
\"signer_id\":{},\"signer_pubkey\":\"{}\",\"total_joules_micro\":{}}}",
serde_json::to_string(&self.device_id).unwrap(),
entries.join(","),
serde_json::to_string(&self.job_id).unwrap(),
hx(&self.program_hash),
hx(&self.provenance_root()),
hx(&self.receipt_hash()),
hx(&self.sig),
serde_json::to_string(&self.signer_id).unwrap(),
hx(&self.signer_pubkey),
self.total_joules_micro(),
)
}
/// An operator-envelope sidecar: the provenance a job/result exchange can carry
/// alongside the operations stack's own job record. Names the job + device it
/// belongs to and the provenance root that anchors the bill.
pub fn to_operator_envelope(&self) -> String {
format!(
"{{\"job_id\":{},\"device_id\":{},\"program_hash\":\"{}\",\"provenance\":{{\
\"kind\":\"wai.quantum.qbom\",\"root\":\"{}\",\"stages\":{},\"total_joules_micro\":{},\
\"signer_id\":{}}}}}",
serde_json::to_string(&self.job_id).unwrap(),
serde_json::to_string(&self.device_id).unwrap(),
hx(&self.program_hash),
hx(&self.provenance_root()),
self.entries.len(),
self.total_joules_micro(),
serde_json::to_string(&self.signer_id).unwrap(),
)
}
pub fn from_json(s: &str) -> Option<Qbom> {
let v: serde_json::Value = serde_json::from_str(s).ok()?;
let o = v.as_object()?;
let mut entries = Vec::new();
for e in o.get("entries")?.as_array()? {
let eo = e.as_object()?;
entries.push(QbomEntry {
kind: eo.get("kind")?.as_str()?.to_owned(),
receipt_hash: from_hex32(eo.get("receipt_hash")?.as_str()?)?,
joules_micro: eo.get("joules_micro")?.as_u64()?,
});
}
Some(Qbom {
job_id: o.get("job_id")?.as_str()?.to_owned(),
program_hash: from_hex32(o.get("program_hash")?.as_str()?)?,
device_id: o.get("device_id")?.as_str()?.to_owned(),
entries,
signer_pubkey: from_hex32(o.get("signer_pubkey")?.as_str()?)?,
signer_id: o.get("signer_id")?.as_str()?.to_owned(),
sig: from_hex64(o.get("sig")?.as_str()?)?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quantum_ops::{CompileReceipt, GrantRef, RearrangeReceipt};
fn key(s: u8) -> SigningKey {
SigningKey::from_bytes(&[s; 32])
}
fn sample_qbom() -> (Qbom, Vec<([u8; 32], bool)>) {
// two real signed stage receipts + one directly-sealed stage
let cr = CompileReceipt::seal(
&key(9), "did:key:op", "clifford-tableau",
content_hash(b"src"), content_hash(b"tgt"), content_hash(b"cpl"),
true, 4, 2048, 120_000, GrantRef::unbounded("quantum.compile"), None,
);
let rr = RearrangeReceipt::seal(
&key(9), "did:key:op", "hungarian-lsap",
content_hash(b"init"), content_hash(b"tg"), content_hash(b"plan"),
true, 40, 300, 90_000, GrantRef::unbounded("quantum.rearrange"), None,
);
let q = QbomBuilder::new("job-0xABCD", content_hash(b"program"), "sim:transmon:cluster-a")
.stage("compile", cr.receipt_hash(), cr.joules_micro)
.stage("rearrange", rr.receipt_hash(), rr.joules_micro)
.seal(&key(9), "did:key:op");
let receipts = vec![
(cr.receipt_hash(), cr.verify()),
(rr.receipt_hash(), rr.verify()),
];
(q, receipts)
}
#[test]
fn qbom_seals_and_verifies() {
let (q, _) = sample_qbom();
assert!(q.verify());
assert_eq!(q.total_joules_micro(), 120_000 + 90_000);
assert_eq!(q.completeness(), vec!["compile", "rearrange"]);
}
#[test]
fn qbom_binds_its_real_receipts() {
let (q, receipts) = sample_qbom();
assert!(q.binds(&receipts), "the bill must bind exactly its stage receipts");
}
#[test]
fn swapped_stage_breaks_the_bill() {
let (q, mut receipts) = sample_qbom();
// swap one receipt hash for an unrelated one → no longer binds
receipts[0].0 = content_hash(b"a forged stage");
assert!(!q.binds(&receipts));
}
#[test]
fn failed_stage_breaks_the_bill() {
let (q, mut receipts) = sample_qbom();
// a listed stage whose receipt does not verify breaks the bill
receipts[1].1 = false;
assert!(!q.binds(&receipts));
}
#[test]
fn tampering_with_the_manifest_breaks_signature() {
let (mut q, _) = sample_qbom();
q.entries[0].joules_micro += 1; // alter the bill after sealing
assert!(!q.verify());
}
#[test]
fn provenance_root_is_reproducible() {
let (a, _) = sample_qbom();
let (b, _) = sample_qbom();
assert_eq!(a.provenance_root(), b.provenance_root());
}
#[test]
fn json_round_trips() {
let (q, _) = sample_qbom();
let back = Qbom::from_json(&q.to_json()).unwrap();
assert_eq!(back, q);
assert!(back.verify());
}
}