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
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]>
370 lines
14 KiB
Rust
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());
|
|
}
|
|
}
|