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";
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)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QbomEntry {
pub kind: String,
pub receipt_hash: [u8; 32],
pub joules_micro: u64,
}
#[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],
}
#[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(),
}
}
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
}
}
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 {
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
}
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
}
pub fn total_joules_micro(&self) -> u64 {
self.entries.iter().map(|e| e.joules_micro).sum()
}
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()
}
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()
}
pub fn binds(&self, receipts: &[([u8; 32], bool)]) -> bool {
if !self.verify() || receipts.len() != self.entries.len() {
return false;
}
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(),
)
}
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)>) {
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();
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();
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; 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());
}
}