1use crate::merkle::merkle_root;
30use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
31
32pub const CAP_QUANTUM_CALIBRATION: &str = "wai.quantum.calibration";
35pub const CAP_QUANTUM_JOB: &str = "wai.quantum.job";
38pub const CAP_QUANTUM_MITIGATE: &str = "wai.quantum.mitigate";
42pub const CAP_QUANTUM_DECODE: &str = "wai.quantum.decode";
46pub const CAP_QUANTUM_COMPILE: &str = "wai.quantum.compile";
49pub const CAP_QUANTUM_REARRANGE: &str = "wai.quantum.rearrange";
52pub const CAP_QUANTUM_QBOM: &str = "wai.quantum.qbom";
55pub const CAP_QUANTUM_FABRIC: &str = "wai.quantum.fabric";
59
60const DOMAIN_CAL: &[u8] = b"wai:quantum-calibration\x01";
61const DOMAIN_CAL_ID: &[u8] = b"wai:quantum-calibration-id\x01";
62const DOMAIN_JOB: &[u8] = b"wai:quantum-job\x01";
63const DOMAIN_JOB_ID: &[u8] = b"wai:quantum-job-id\x01";
64const DOMAIN_MIT: &[u8] = b"wai:quantum-mitigate\x01";
65const DOMAIN_MIT_ID: &[u8] = b"wai:quantum-mitigate-id\x01";
66const DOMAIN_MIT_RESULT: &[u8] = b"wai:quantum-mitigate-result\x01";
67const DOMAIN_DEC: &[u8] = b"wai:quantum-decode\x01";
68const DOMAIN_DEC_ID: &[u8] = b"wai:quantum-decode-id\x01";
69const DOMAIN_CMP: &[u8] = b"wai:quantum-compile\x01";
70const DOMAIN_CMP_ID: &[u8] = b"wai:quantum-compile-id\x01";
71const DOMAIN_REA: &[u8] = b"wai:quantum-rearrange\x01";
72const DOMAIN_REA_ID: &[u8] = b"wai:quantum-rearrange-id\x01";
73
74fn hx(b: &[u8]) -> String {
75 b.iter().map(|x| format!("{x:02x}")).collect()
76}
77
78fn from_hex32(s: &str) -> Option<[u8; 32]> {
79 if s.len() != 64 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
80 return None;
81 }
82 let mut out = [0u8; 32];
83 for (i, c) in s.as_bytes().chunks(2).enumerate() {
84 out[i] = u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok()?;
85 }
86 Some(out)
87}
88
89fn from_hex64(s: &str) -> Option<[u8; 64]> {
90 if s.len() != 128 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
91 return None;
92 }
93 let mut out = [0u8; 64];
94 for (i, c) in s.as_bytes().chunks(2).enumerate() {
95 out[i] = u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok()?;
96 }
97 Some(out)
98}
99
100pub fn content_hash(bytes: &[u8]) -> [u8; 32] {
104 *blake3::hash(bytes).as_bytes()
105}
106
107#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct GrantRef {
113 pub grant_hash: [u8; 32],
115 pub capability: String,
117 pub joule_ceiling_micro: u64,
119 pub funds_ceiling: Option<u64>,
121}
122
123impl GrantRef {
124 pub fn unbounded(capability: impl Into<String>) -> GrantRef {
127 GrantRef {
128 grant_hash: [0u8; 32],
129 capability: capability.into(),
130 joule_ceiling_micro: u64::MAX,
131 funds_ceiling: None,
132 }
133 }
134
135 fn append_to(&self, o: &mut Vec<u8>) {
136 o.extend_from_slice(&self.grant_hash);
137 o.extend_from_slice(self.capability.as_bytes());
138 o.push(0); o.extend_from_slice(&self.joule_ceiling_micro.to_be_bytes());
140 match self.funds_ceiling {
141 Some(f) => {
142 o.push(1);
143 o.extend_from_slice(&f.to_be_bytes());
144 }
145 None => o.push(0),
146 }
147 }
148
149 fn to_json(&self) -> String {
150 let funds = match self.funds_ceiling {
151 Some(f) => f.to_string(),
152 None => "null".into(),
153 };
154 format!(
155 "{{\"capability\":{},\"funds_ceiling\":{},\"grant_hash\":\"{}\",\"joule_ceiling_micro\":{}}}",
156 serde_json::to_string(&self.capability).unwrap(),
157 funds,
158 hx(&self.grant_hash),
159 self.joule_ceiling_micro,
160 )
161 }
162
163 fn from_json(v: &serde_json::Value) -> Option<GrantRef> {
164 let o = v.as_object()?;
165 Some(GrantRef {
166 grant_hash: from_hex32(o.get("grant_hash")?.as_str()?)?,
167 capability: o.get("capability")?.as_str()?.to_owned(),
168 joule_ceiling_micro: o.get("joule_ceiling_micro")?.as_u64()?,
169 funds_ceiling: match o.get("funds_ceiling") {
170 Some(serde_json::Value::Number(n)) => Some(n.as_u64()?),
171 _ => None,
172 },
173 })
174 }
175}
176
177#[derive(Clone, Debug, PartialEq, Eq)]
181pub struct Evidence {
182 pub kind: String,
184 pub blob_hash: [u8; 32],
186 pub summary: String,
188}
189
190impl Evidence {
191 fn leaf(&self) -> [u8; 32] {
192 let mut h = blake3::Hasher::new();
193 h.update(b"wai:qcal-evidence\x01");
194 h.update(self.kind.as_bytes());
195 h.update(&[0]);
196 h.update(&self.blob_hash);
197 *h.finalize().as_bytes()
198 }
199}
200
201#[derive(Clone, Debug, PartialEq)]
209pub struct CalibrationReceipt {
210 pub device_id: String,
212 pub target: String,
214 pub config_hash: [u8; 32],
216 pub evidence: Vec<Evidence>,
218 pub merkle_root: [u8; 32],
220 pub joules_micro: u64,
222 pub grant: GrantRef,
224 pub parent_receipt_hash: Option<[u8; 32]>,
226 pub signer_pubkey: [u8; 32],
227 pub signer_id: String,
228 pub sig: [u8; 64],
229}
230
231impl CalibrationReceipt {
232 fn leaves(&self) -> Vec<[u8; 32]> {
233 let mut v = vec![self.config_hash];
234 v.extend(self.evidence.iter().map(|e| e.leaf()));
235 v
236 }
237
238 fn signing_payload(&self) -> Vec<u8> {
239 self.payload_with(None)
240 }
241
242 fn payload_with(&self, label: Option<&crate::quantum_energy::EnergyClass>) -> Vec<u8> {
246 let mut o = crate::quantum_energy::labelled_domain(DOMAIN_CAL, label);
247 o.extend_from_slice(self.device_id.as_bytes());
248 o.push(0);
249 o.extend_from_slice(self.target.as_bytes());
250 o.push(0);
251 o.extend_from_slice(&self.config_hash);
252 o.extend_from_slice(&self.merkle_root);
253 o.extend_from_slice(&(self.evidence.len() as u64).to_be_bytes());
254 o.extend_from_slice(&self.joules_micro.to_be_bytes());
255 crate::quantum_energy::write_label(label, &mut o);
256 self.grant.append_to(&mut o);
257 match self.parent_receipt_hash {
258 Some(h) => {
259 o.push(1);
260 o.extend_from_slice(&h);
261 }
262 None => o.push(0),
263 }
264 o.extend_from_slice(&self.signer_pubkey);
265 o.extend_from_slice(self.signer_id.as_bytes());
266 o
267 }
268
269 #[allow(clippy::too_many_arguments)]
271 pub fn seal(
272 signer: &SigningKey,
273 signer_id: impl Into<String>,
274 device_id: impl Into<String>,
275 target: impl Into<String>,
276 config_hash: [u8; 32],
277 evidence: Vec<Evidence>,
278 joules_micro: u64,
279 grant: GrantRef,
280 parent_receipt_hash: Option<[u8; 32]>,
281 ) -> CalibrationReceipt {
282 let mut r = CalibrationReceipt {
283 device_id: device_id.into(),
284 target: target.into(),
285 config_hash,
286 evidence,
287 merkle_root: [0u8; 32],
288 joules_micro,
289 grant,
290 parent_receipt_hash,
291 signer_pubkey: signer.verifying_key().to_bytes(),
292 signer_id: signer_id.into(),
293 sig: [0u8; 64],
294 };
295 r.merkle_root = merkle_root(&r.leaves());
296 r.sig = signer.sign(&r.signing_payload()).to_bytes();
297 r
298 }
299
300 pub fn honors_budget(&self) -> bool {
303 self.joules_micro <= self.grant.joule_ceiling_micro
304 }
305
306 fn body_verifies(&self) -> bool {
309 if merkle_root(&self.leaves()) != self.merkle_root {
310 return false;
311 }
312 if !self.honors_budget() {
313 return false;
314 }
315 true
316 }
317
318 pub fn verify(&self) -> bool {
322 if !self.body_verifies() {
323 return false;
324 }
325 let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
326 return false;
327 };
328 k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
329 .is_ok()
330 }
331
332 pub fn evidence_matches(&self, i: usize, dataset_bytes: &[u8]) -> bool {
336 self.evidence
337 .get(i)
338 .is_some_and(|e| content_hash(dataset_bytes) == e.blob_hash)
339 }
340
341 pub fn receipt_hash(&self) -> [u8; 32] {
343 let mut h = blake3::Hasher::new();
344 h.update(DOMAIN_CAL_ID);
345 h.update(&self.signing_payload());
346 h.update(&self.sig);
347 *h.finalize().as_bytes()
348 }
349
350 pub fn to_json(&self) -> String {
351 let ev: Vec<String> = self
352 .evidence
353 .iter()
354 .map(|e| {
355 format!(
356 "{{\"blob_hash\":\"{}\",\"kind\":{},\"summary\":{}}}",
357 hx(&e.blob_hash),
358 serde_json::to_string(&e.kind).unwrap(),
359 serde_json::to_string(&e.summary).unwrap(),
360 )
361 })
362 .collect();
363 let parent = match self.parent_receipt_hash {
364 Some(h) => format!("\"{}\"", hx(&h)),
365 None => "null".into(),
366 };
367 format!(
368 "{{\"kind\":\"quantum-calibration\",\"config_hash\":\"{}\",\"device_id\":{},\
369 \"evidence\":[{}],\"grant\":{},\"joules_micro\":{},\"merkle_root\":\"{}\",\
370 \"parent_receipt_hash\":{},\"receipt_hash\":\"{}\",\"sig\":\"{}\",\
371 \"signer_id\":{},\"signer_pubkey\":\"{}\",\"target\":{}}}",
372 hx(&self.config_hash),
373 serde_json::to_string(&self.device_id).unwrap(),
374 ev.join(","),
375 self.grant.to_json(),
376 self.joules_micro,
377 hx(&self.merkle_root),
378 parent,
379 hx(&self.receipt_hash()),
380 hx(&self.sig),
381 serde_json::to_string(&self.signer_id).unwrap(),
382 hx(&self.signer_pubkey),
383 serde_json::to_string(&self.target).unwrap(),
384 )
385 }
386
387 pub fn from_json(s: &str) -> Option<CalibrationReceipt> {
388 let v: serde_json::Value = serde_json::from_str(s).ok()?;
389 let o = v.as_object()?;
390 let mut evidence = Vec::new();
391 for ev in o.get("evidence")?.as_array()? {
392 let eo = ev.as_object()?;
393 evidence.push(Evidence {
394 kind: eo.get("kind")?.as_str()?.to_owned(),
395 blob_hash: from_hex32(eo.get("blob_hash")?.as_str()?)?,
396 summary: eo.get("summary")?.as_str()?.to_owned(),
397 });
398 }
399 let parent = match o.get("parent_receipt_hash") {
400 Some(serde_json::Value::String(s)) => Some(from_hex32(s)?),
401 _ => None,
402 };
403 Some(CalibrationReceipt {
404 device_id: o.get("device_id")?.as_str()?.to_owned(),
405 target: o.get("target")?.as_str()?.to_owned(),
406 config_hash: from_hex32(o.get("config_hash")?.as_str()?)?,
407 evidence,
408 merkle_root: from_hex32(o.get("merkle_root")?.as_str()?)?,
409 joules_micro: o.get("joules_micro")?.as_u64()?,
410 grant: GrantRef::from_json(o.get("grant")?)?,
411 parent_receipt_hash: parent,
412 signer_pubkey: from_hex32(o.get("signer_pubkey")?.as_str()?)?,
413 signer_id: o.get("signer_id")?.as_str()?.to_owned(),
414 sig: from_hex64(o.get("sig")?.as_str()?)?,
415 })
416 }
417}
418
419#[derive(Clone, Debug, PartialEq)]
426pub struct JobReceipt {
427 pub backend_id: String,
429 pub circuit_hash: [u8; 32],
431 pub calibration_ref: Option<[u8; 32]>,
435 pub result_hash: [u8; 32],
437 pub shots: u64,
439 pub merkle_root: [u8; 32],
441 pub joules_micro: u64,
443 pub grant: GrantRef,
444 pub parent_receipt_hash: Option<[u8; 32]>,
445 pub signer_pubkey: [u8; 32],
446 pub signer_id: String,
447 pub sig: [u8; 64],
448}
449
450impl JobReceipt {
451 fn leaves(&self) -> Vec<[u8; 32]> {
452 let mut v = vec![self.circuit_hash, self.result_hash];
453 if let Some(c) = self.calibration_ref {
454 v.push(c);
455 }
456 v
457 }
458
459 fn signing_payload(&self) -> Vec<u8> {
460 self.payload_with(None)
461 }
462
463 fn payload_with(&self, label: Option<&crate::quantum_energy::EnergyClass>) -> Vec<u8> {
467 let mut o = crate::quantum_energy::labelled_domain(DOMAIN_JOB, label);
468 o.extend_from_slice(self.backend_id.as_bytes());
469 o.push(0);
470 o.extend_from_slice(&self.circuit_hash);
471 o.extend_from_slice(&self.result_hash);
472 match self.calibration_ref {
473 Some(c) => {
474 o.push(1);
475 o.extend_from_slice(&c);
476 }
477 None => o.push(0),
478 }
479 o.extend_from_slice(&self.shots.to_be_bytes());
480 o.extend_from_slice(&self.merkle_root);
481 o.extend_from_slice(&self.joules_micro.to_be_bytes());
482 crate::quantum_energy::write_label(label, &mut o);
483 self.grant.append_to(&mut o);
484 match self.parent_receipt_hash {
485 Some(h) => {
486 o.push(1);
487 o.extend_from_slice(&h);
488 }
489 None => o.push(0),
490 }
491 o.extend_from_slice(&self.signer_pubkey);
492 o.extend_from_slice(self.signer_id.as_bytes());
493 o
494 }
495
496 #[allow(clippy::too_many_arguments)]
497 pub fn seal(
498 signer: &SigningKey,
499 signer_id: impl Into<String>,
500 backend_id: impl Into<String>,
501 circuit_hash: [u8; 32],
502 result_hash: [u8; 32],
503 calibration_ref: Option<[u8; 32]>,
504 shots: u64,
505 joules_micro: u64,
506 grant: GrantRef,
507 parent_receipt_hash: Option<[u8; 32]>,
508 ) -> JobReceipt {
509 let mut r = JobReceipt {
510 backend_id: backend_id.into(),
511 circuit_hash,
512 calibration_ref,
513 result_hash,
514 shots,
515 merkle_root: [0u8; 32],
516 joules_micro,
517 grant,
518 parent_receipt_hash,
519 signer_pubkey: signer.verifying_key().to_bytes(),
520 signer_id: signer_id.into(),
521 sig: [0u8; 64],
522 };
523 r.merkle_root = merkle_root(&r.leaves());
524 r.sig = signer.sign(&r.signing_payload()).to_bytes();
525 r
526 }
527
528 pub fn honors_budget(&self) -> bool {
529 self.joules_micro <= self.grant.joule_ceiling_micro
530 }
531
532 fn body_verifies(&self) -> bool {
535 if merkle_root(&self.leaves()) != self.merkle_root {
536 return false;
537 }
538 if !self.honors_budget() {
539 return false;
540 }
541 true
542 }
543
544 pub fn verify(&self) -> bool {
546 if !self.body_verifies() {
547 return false;
548 }
549 let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
550 return false;
551 };
552 k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
553 .is_ok()
554 }
555
556 pub fn verify_against_calibration(&self, cal: &CalibrationReceipt) -> bool {
561 self.calibration_ref == Some(cal.receipt_hash()) && cal.verify() && self.verify()
562 }
563
564 pub fn result_matches(&self, result_bytes: &[u8]) -> bool {
566 content_hash(result_bytes) == self.result_hash
567 }
568
569 pub fn receipt_hash(&self) -> [u8; 32] {
570 let mut h = blake3::Hasher::new();
571 h.update(DOMAIN_JOB_ID);
572 h.update(&self.signing_payload());
573 h.update(&self.sig);
574 *h.finalize().as_bytes()
575 }
576
577 pub fn to_json(&self) -> String {
578 let cal = match self.calibration_ref {
579 Some(c) => format!("\"{}\"", hx(&c)),
580 None => "null".into(),
581 };
582 let parent = match self.parent_receipt_hash {
583 Some(h) => format!("\"{}\"", hx(&h)),
584 None => "null".into(),
585 };
586 format!(
587 "{{\"kind\":\"quantum-job\",\"backend_id\":{},\"calibration_ref\":{},\
588 \"circuit_hash\":\"{}\",\"grant\":{},\"joules_micro\":{},\"merkle_root\":\"{}\",\
589 \"parent_receipt_hash\":{},\"receipt_hash\":\"{}\",\"result_hash\":\"{}\",\
590 \"shots\":{},\"sig\":\"{}\",\"signer_id\":{},\"signer_pubkey\":\"{}\"}}",
591 serde_json::to_string(&self.backend_id).unwrap(),
592 cal,
593 hx(&self.circuit_hash),
594 self.grant.to_json(),
595 self.joules_micro,
596 hx(&self.merkle_root),
597 parent,
598 hx(&self.receipt_hash()),
599 hx(&self.result_hash),
600 self.shots,
601 hx(&self.sig),
602 serde_json::to_string(&self.signer_id).unwrap(),
603 hx(&self.signer_pubkey),
604 )
605 }
606
607 pub fn from_json(s: &str) -> Option<JobReceipt> {
608 let v: serde_json::Value = serde_json::from_str(s).ok()?;
609 let o = v.as_object()?;
610 let opt_hash = |k: &str| match o.get(k) {
611 Some(serde_json::Value::String(s)) => from_hex32(s),
612 _ => None,
613 };
614 Some(JobReceipt {
615 backend_id: o.get("backend_id")?.as_str()?.to_owned(),
616 circuit_hash: from_hex32(o.get("circuit_hash")?.as_str()?)?,
617 calibration_ref: opt_hash("calibration_ref"),
618 result_hash: from_hex32(o.get("result_hash")?.as_str()?)?,
619 shots: o.get("shots")?.as_u64()?,
620 merkle_root: from_hex32(o.get("merkle_root")?.as_str()?)?,
621 joules_micro: o.get("joules_micro")?.as_u64()?,
622 grant: GrantRef::from_json(o.get("grant")?)?,
623 parent_receipt_hash: opt_hash("parent_receipt_hash"),
624 signer_pubkey: from_hex32(o.get("signer_pubkey")?.as_str()?)?,
625 signer_id: o.get("signer_id")?.as_str()?.to_owned(),
626 sig: from_hex64(o.get("sig")?.as_str()?)?,
627 })
628 }
629}
630
631#[derive(Clone, Debug, PartialEq)]
654pub struct MitigationReceipt {
655 pub backend_id: String,
657 pub method: String,
660 pub observable: String,
662 pub input_hash: [u8; 32],
665 pub noise_model_hash: Option<[u8; 32]>,
669 pub raw_estimate_fx: i64,
671 pub mitigated_estimate_fx: i64,
673 pub error_bar_fx: i64,
675 pub shots: u64,
677 pub merkle_root: [u8; 32],
680 pub joules_micro: u64,
682 pub grant: GrantRef,
683 pub parent_receipt_hash: Option<[u8; 32]>,
684 pub signer_pubkey: [u8; 32],
685 pub signer_id: String,
686 pub sig: [u8; 64],
687}
688
689impl MitigationReceipt {
690 fn result_leaf(&self) -> [u8; 32] {
691 let mut h = blake3::Hasher::new();
692 h.update(DOMAIN_MIT_RESULT);
693 h.update(self.method.as_bytes());
694 h.update(&[0]);
695 h.update(&self.raw_estimate_fx.to_be_bytes());
696 h.update(&self.mitigated_estimate_fx.to_be_bytes());
697 h.update(&self.error_bar_fx.to_be_bytes());
698 *h.finalize().as_bytes()
699 }
700
701 fn leaves(&self) -> Vec<[u8; 32]> {
702 let mut v = vec![self.input_hash];
703 if let Some(m) = self.noise_model_hash {
704 v.push(m);
705 }
706 v.push(self.result_leaf());
707 v
708 }
709
710 fn signing_payload(&self) -> Vec<u8> {
711 self.payload_with(None)
712 }
713
714 fn payload_with(&self, label: Option<&crate::quantum_energy::EnergyClass>) -> Vec<u8> {
718 let mut o = crate::quantum_energy::labelled_domain(DOMAIN_MIT, label);
719 o.extend_from_slice(self.backend_id.as_bytes());
720 o.push(0);
721 o.extend_from_slice(self.method.as_bytes());
722 o.push(0);
723 o.extend_from_slice(self.observable.as_bytes());
724 o.push(0);
725 o.extend_from_slice(&self.input_hash);
726 match self.noise_model_hash {
727 Some(m) => {
728 o.push(1);
729 o.extend_from_slice(&m);
730 }
731 None => o.push(0),
732 }
733 o.extend_from_slice(&self.raw_estimate_fx.to_be_bytes());
734 o.extend_from_slice(&self.mitigated_estimate_fx.to_be_bytes());
735 o.extend_from_slice(&self.error_bar_fx.to_be_bytes());
736 o.extend_from_slice(&self.shots.to_be_bytes());
737 o.extend_from_slice(&self.merkle_root);
738 o.extend_from_slice(&self.joules_micro.to_be_bytes());
739 crate::quantum_energy::write_label(label, &mut o);
740 self.grant.append_to(&mut o);
741 match self.parent_receipt_hash {
742 Some(h) => {
743 o.push(1);
744 o.extend_from_slice(&h);
745 }
746 None => o.push(0),
747 }
748 o.extend_from_slice(&self.signer_pubkey);
749 o.extend_from_slice(self.signer_id.as_bytes());
750 o
751 }
752
753 #[allow(clippy::too_many_arguments)]
755 pub fn seal(
756 signer: &SigningKey,
757 signer_id: impl Into<String>,
758 backend_id: impl Into<String>,
759 method: impl Into<String>,
760 observable: impl Into<String>,
761 input_hash: [u8; 32],
762 noise_model_hash: Option<[u8; 32]>,
763 raw_estimate_fx: i64,
764 mitigated_estimate_fx: i64,
765 error_bar_fx: i64,
766 shots: u64,
767 joules_micro: u64,
768 grant: GrantRef,
769 parent_receipt_hash: Option<[u8; 32]>,
770 ) -> MitigationReceipt {
771 let mut r = MitigationReceipt {
772 backend_id: backend_id.into(),
773 method: method.into(),
774 observable: observable.into(),
775 input_hash,
776 noise_model_hash,
777 raw_estimate_fx,
778 mitigated_estimate_fx,
779 error_bar_fx,
780 shots,
781 merkle_root: [0u8; 32],
782 joules_micro,
783 grant,
784 parent_receipt_hash,
785 signer_pubkey: signer.verifying_key().to_bytes(),
786 signer_id: signer_id.into(),
787 sig: [0u8; 64],
788 };
789 r.merkle_root = merkle_root(&r.leaves());
790 r.sig = signer.sign(&r.signing_payload()).to_bytes();
791 r
792 }
793
794 pub fn honors_budget(&self) -> bool {
795 self.joules_micro <= self.grant.joule_ceiling_micro
796 }
797
798 fn body_verifies(&self) -> bool {
801 if merkle_root(&self.leaves()) != self.merkle_root {
802 return false;
803 }
804 if !self.honors_budget() {
805 return false;
806 }
807 true
808 }
809
810 pub fn verify(&self) -> bool {
815 if !self.body_verifies() {
816 return false;
817 }
818 let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
819 return false;
820 };
821 k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
822 .is_ok()
823 }
824
825 pub fn input_matches(&self, input_bytes: &[u8]) -> bool {
828 content_hash(input_bytes) == self.input_hash
829 }
830
831 pub fn noise_model_matches(&self, model_bytes: &[u8]) -> bool {
833 self.noise_model_hash == Some(content_hash(model_bytes))
834 }
835
836 pub fn receipt_hash(&self) -> [u8; 32] {
837 let mut h = blake3::Hasher::new();
838 h.update(DOMAIN_MIT_ID);
839 h.update(&self.signing_payload());
840 h.update(&self.sig);
841 *h.finalize().as_bytes()
842 }
843
844 pub fn to_json(&self) -> String {
845 let nm = match self.noise_model_hash {
846 Some(m) => format!("\"{}\"", hx(&m)),
847 None => "null".into(),
848 };
849 let parent = match self.parent_receipt_hash {
850 Some(h) => format!("\"{}\"", hx(&h)),
851 None => "null".into(),
852 };
853 format!(
854 "{{\"kind\":\"quantum-mitigate\",\"backend_id\":{},\"error_bar_fx\":{},\
855 \"grant\":{},\"input_hash\":\"{}\",\"joules_micro\":{},\"merkle_root\":\"{}\",\
856 \"method\":{},\"mitigated_estimate_fx\":{},\"noise_model_hash\":{},\
857 \"observable\":{},\"parent_receipt_hash\":{},\"raw_estimate_fx\":{},\
858 \"receipt_hash\":\"{}\",\"shots\":{},\"sig\":\"{}\",\"signer_id\":{},\
859 \"signer_pubkey\":\"{}\"}}",
860 serde_json::to_string(&self.backend_id).unwrap(),
861 self.error_bar_fx,
862 self.grant.to_json(),
863 hx(&self.input_hash),
864 self.joules_micro,
865 hx(&self.merkle_root),
866 serde_json::to_string(&self.method).unwrap(),
867 self.mitigated_estimate_fx,
868 nm,
869 serde_json::to_string(&self.observable).unwrap(),
870 parent,
871 self.raw_estimate_fx,
872 hx(&self.receipt_hash()),
873 self.shots,
874 hx(&self.sig),
875 serde_json::to_string(&self.signer_id).unwrap(),
876 hx(&self.signer_pubkey),
877 )
878 }
879
880 pub fn from_json(s: &str) -> Option<MitigationReceipt> {
881 let v: serde_json::Value = serde_json::from_str(s).ok()?;
882 let o = v.as_object()?;
883 let opt_hash = |k: &str| match o.get(k) {
884 Some(serde_json::Value::String(s)) => from_hex32(s),
885 _ => None,
886 };
887 Some(MitigationReceipt {
888 backend_id: o.get("backend_id")?.as_str()?.to_owned(),
889 method: o.get("method")?.as_str()?.to_owned(),
890 observable: o.get("observable")?.as_str()?.to_owned(),
891 input_hash: from_hex32(o.get("input_hash")?.as_str()?)?,
892 noise_model_hash: opt_hash("noise_model_hash"),
893 raw_estimate_fx: o.get("raw_estimate_fx")?.as_i64()?,
894 mitigated_estimate_fx: o.get("mitigated_estimate_fx")?.as_i64()?,
895 error_bar_fx: o.get("error_bar_fx")?.as_i64()?,
896 shots: o.get("shots")?.as_u64()?,
897 merkle_root: from_hex32(o.get("merkle_root")?.as_str()?)?,
898 joules_micro: o.get("joules_micro")?.as_u64()?,
899 grant: GrantRef::from_json(o.get("grant")?)?,
900 parent_receipt_hash: opt_hash("parent_receipt_hash"),
901 signer_pubkey: from_hex32(o.get("signer_pubkey")?.as_str()?)?,
902 signer_id: o.get("signer_id")?.as_str()?.to_owned(),
903 sig: from_hex64(o.get("sig")?.as_str()?)?,
904 })
905 }
906}
907
908#[derive(Clone, Debug, PartialEq)]
932pub struct DecodeReceipt {
933 pub code_id: String,
935 pub decoder: String,
937 pub check_matrix_hash: [u8; 32],
939 pub syndrome_hash: [u8; 32],
941 pub correction_hash: [u8; 32],
943 pub converged: bool,
945 pub rounds: u32,
947 pub work_messages: u64,
950 pub joules_micro: u64,
952 pub grant: GrantRef,
953 pub parent_receipt_hash: Option<[u8; 32]>,
954 pub signer_pubkey: [u8; 32],
955 pub signer_id: String,
956 pub sig: [u8; 64],
957}
958
959impl DecodeReceipt {
960 fn leaves(&self) -> Vec<[u8; 32]> {
961 vec![
962 self.check_matrix_hash,
963 self.syndrome_hash,
964 self.correction_hash,
965 ]
966 }
967
968 fn signing_payload(&self) -> Vec<u8> {
969 self.payload_with(None)
970 }
971
972 fn payload_with(&self, label: Option<&crate::quantum_energy::EnergyClass>) -> Vec<u8> {
976 let mut o = crate::quantum_energy::labelled_domain(DOMAIN_DEC, label);
977 o.extend_from_slice(self.code_id.as_bytes());
978 o.push(0);
979 o.extend_from_slice(self.decoder.as_bytes());
980 o.push(0);
981 o.extend_from_slice(&self.check_matrix_hash);
982 o.extend_from_slice(&self.syndrome_hash);
983 o.extend_from_slice(&self.correction_hash);
984 o.push(self.converged as u8);
985 o.extend_from_slice(&self.rounds.to_be_bytes());
986 o.extend_from_slice(&self.work_messages.to_be_bytes());
987 o.extend_from_slice(&self.merkle_root_calc());
988 o.extend_from_slice(&self.joules_micro.to_be_bytes());
989 crate::quantum_energy::write_label(label, &mut o);
990 self.grant.append_to(&mut o);
991 match self.parent_receipt_hash {
992 Some(h) => {
993 o.push(1);
994 o.extend_from_slice(&h);
995 }
996 None => o.push(0),
997 }
998 o.extend_from_slice(&self.signer_pubkey);
999 o.extend_from_slice(self.signer_id.as_bytes());
1000 o
1001 }
1002
1003 fn merkle_root_calc(&self) -> [u8; 32] {
1004 merkle_root(&self.leaves())
1005 }
1006
1007 #[allow(clippy::too_many_arguments)]
1009 pub fn seal(
1010 signer: &SigningKey,
1011 signer_id: impl Into<String>,
1012 code_id: impl Into<String>,
1013 decoder: impl Into<String>,
1014 check_matrix_hash: [u8; 32],
1015 syndrome_hash: [u8; 32],
1016 correction_hash: [u8; 32],
1017 converged: bool,
1018 rounds: u32,
1019 work_messages: u64,
1020 joules_micro: u64,
1021 grant: GrantRef,
1022 parent_receipt_hash: Option<[u8; 32]>,
1023 ) -> DecodeReceipt {
1024 let mut r = DecodeReceipt {
1025 code_id: code_id.into(),
1026 decoder: decoder.into(),
1027 check_matrix_hash,
1028 syndrome_hash,
1029 correction_hash,
1030 converged,
1031 rounds,
1032 work_messages,
1033 joules_micro,
1034 grant,
1035 parent_receipt_hash,
1036 signer_pubkey: signer.verifying_key().to_bytes(),
1037 signer_id: signer_id.into(),
1038 sig: [0u8; 64],
1039 };
1040 r.sig = signer.sign(&r.signing_payload()).to_bytes();
1041 r
1042 }
1043
1044 pub fn honors_budget(&self) -> bool {
1045 self.joules_micro <= self.grant.joule_ceiling_micro
1046 }
1047
1048 fn body_verifies(&self) -> bool {
1051 if !self.honors_budget() {
1052 return false;
1053 }
1054 true
1055 }
1056
1057 pub fn verify(&self) -> bool {
1060 if !self.body_verifies() {
1061 return false;
1062 }
1063 let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
1064 return false;
1065 };
1066 k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
1067 .is_ok()
1068 }
1069
1070 pub fn correction_matches(&self, correction_bytes: &[u8]) -> bool {
1072 content_hash(correction_bytes) == self.correction_hash
1073 }
1074
1075 pub fn code_matches(&self, matrix_bytes: &[u8]) -> bool {
1077 content_hash(matrix_bytes) == self.check_matrix_hash
1078 }
1079
1080 pub fn receipt_hash(&self) -> [u8; 32] {
1081 let mut h = blake3::Hasher::new();
1082 h.update(DOMAIN_DEC_ID);
1083 h.update(&self.signing_payload());
1084 h.update(&self.sig);
1085 *h.finalize().as_bytes()
1086 }
1087
1088 pub fn to_json(&self) -> String {
1089 let parent = match self.parent_receipt_hash {
1090 Some(h) => format!("\"{}\"", hx(&h)),
1091 None => "null".into(),
1092 };
1093 format!(
1094 "{{\"kind\":\"quantum-decode\",\"check_matrix_hash\":\"{}\",\"code_id\":{},\
1095 \"converged\":{},\"correction_hash\":\"{}\",\"decoder\":{},\"grant\":{},\
1096 \"joules_micro\":{},\"merkle_root\":\"{}\",\"parent_receipt_hash\":{},\
1097 \"receipt_hash\":\"{}\",\"rounds\":{},\"sig\":\"{}\",\"signer_id\":{},\
1098 \"signer_pubkey\":\"{}\",\"syndrome_hash\":\"{}\",\"work_messages\":{}}}",
1099 hx(&self.check_matrix_hash),
1100 serde_json::to_string(&self.code_id).unwrap(),
1101 self.converged,
1102 hx(&self.correction_hash),
1103 serde_json::to_string(&self.decoder).unwrap(),
1104 self.grant.to_json(),
1105 self.joules_micro,
1106 hx(&self.merkle_root_calc()),
1107 parent,
1108 hx(&self.receipt_hash()),
1109 self.rounds,
1110 hx(&self.sig),
1111 serde_json::to_string(&self.signer_id).unwrap(),
1112 hx(&self.signer_pubkey),
1113 hx(&self.syndrome_hash),
1114 self.work_messages,
1115 )
1116 }
1117
1118 pub fn from_json(s: &str) -> Option<DecodeReceipt> {
1119 let v: serde_json::Value = serde_json::from_str(s).ok()?;
1120 let o = v.as_object()?;
1121 let parent = match o.get("parent_receipt_hash") {
1122 Some(serde_json::Value::String(s)) => Some(from_hex32(s)?),
1123 _ => None,
1124 };
1125 Some(DecodeReceipt {
1126 code_id: o.get("code_id")?.as_str()?.to_owned(),
1127 decoder: o.get("decoder")?.as_str()?.to_owned(),
1128 check_matrix_hash: from_hex32(o.get("check_matrix_hash")?.as_str()?)?,
1129 syndrome_hash: from_hex32(o.get("syndrome_hash")?.as_str()?)?,
1130 correction_hash: from_hex32(o.get("correction_hash")?.as_str()?)?,
1131 converged: o.get("converged")?.as_bool()?,
1132 rounds: o.get("rounds")?.as_u64()? as u32,
1133 work_messages: o.get("work_messages")?.as_u64()?,
1134 joules_micro: o.get("joules_micro")?.as_u64()?,
1135 grant: GrantRef::from_json(o.get("grant")?)?,
1136 parent_receipt_hash: parent,
1137 signer_pubkey: from_hex32(o.get("signer_pubkey")?.as_str()?)?,
1138 signer_id: o.get("signer_id")?.as_str()?.to_owned(),
1139 sig: from_hex64(o.get("sig")?.as_str()?)?,
1140 })
1141 }
1142}
1143
1144#[derive(Clone, Debug, PartialEq)]
1165pub struct CompileReceipt {
1166 pub source_hash: [u8; 32],
1168 pub target_hash: [u8; 32],
1170 pub coupling_hash: [u8; 32],
1172 pub method: String,
1174 pub equivalent: bool,
1176 pub swaps: u32,
1178 pub work: u64,
1180 pub joules_micro: u64,
1182 pub grant: GrantRef,
1183 pub parent_receipt_hash: Option<[u8; 32]>,
1184 pub signer_pubkey: [u8; 32],
1185 pub signer_id: String,
1186 pub sig: [u8; 64],
1187}
1188
1189impl CompileReceipt {
1190 fn leaves(&self) -> Vec<[u8; 32]> {
1191 vec![self.source_hash, self.target_hash, self.coupling_hash]
1192 }
1193 fn merkle(&self) -> [u8; 32] {
1194 merkle_root(&self.leaves())
1195 }
1196
1197 fn signing_payload(&self) -> Vec<u8> {
1198 self.payload_with(None)
1199 }
1200
1201 fn payload_with(&self, label: Option<&crate::quantum_energy::EnergyClass>) -> Vec<u8> {
1205 let mut o = crate::quantum_energy::labelled_domain(DOMAIN_CMP, label);
1206 o.extend_from_slice(&self.source_hash);
1207 o.extend_from_slice(&self.target_hash);
1208 o.extend_from_slice(&self.coupling_hash);
1209 o.extend_from_slice(self.method.as_bytes());
1210 o.push(0);
1211 o.push(self.equivalent as u8);
1212 o.extend_from_slice(&self.swaps.to_be_bytes());
1213 o.extend_from_slice(&self.work.to_be_bytes());
1214 o.extend_from_slice(&self.merkle());
1215 o.extend_from_slice(&self.joules_micro.to_be_bytes());
1216 crate::quantum_energy::write_label(label, &mut o);
1217 self.grant.append_to(&mut o);
1218 match self.parent_receipt_hash {
1219 Some(h) => {
1220 o.push(1);
1221 o.extend_from_slice(&h);
1222 }
1223 None => o.push(0),
1224 }
1225 o.extend_from_slice(&self.signer_pubkey);
1226 o.extend_from_slice(self.signer_id.as_bytes());
1227 o
1228 }
1229
1230 #[allow(clippy::too_many_arguments)]
1231 pub fn seal(
1232 signer: &SigningKey,
1233 signer_id: impl Into<String>,
1234 method: impl Into<String>,
1235 source_hash: [u8; 32],
1236 target_hash: [u8; 32],
1237 coupling_hash: [u8; 32],
1238 equivalent: bool,
1239 swaps: u32,
1240 work: u64,
1241 joules_micro: u64,
1242 grant: GrantRef,
1243 parent_receipt_hash: Option<[u8; 32]>,
1244 ) -> CompileReceipt {
1245 let mut r = CompileReceipt {
1246 source_hash,
1247 target_hash,
1248 coupling_hash,
1249 method: method.into(),
1250 equivalent,
1251 swaps,
1252 work,
1253 joules_micro,
1254 grant,
1255 parent_receipt_hash,
1256 signer_pubkey: signer.verifying_key().to_bytes(),
1257 signer_id: signer_id.into(),
1258 sig: [0u8; 64],
1259 };
1260 r.sig = signer.sign(&r.signing_payload()).to_bytes();
1261 r
1262 }
1263
1264 pub fn honors_budget(&self) -> bool {
1265 self.joules_micro <= self.grant.joule_ceiling_micro
1266 }
1267
1268 fn body_verifies(&self) -> bool {
1271 if !self.honors_budget() {
1272 return false;
1273 }
1274 true
1275 }
1276
1277 pub fn verify(&self) -> bool {
1281 if !self.body_verifies() {
1282 return false;
1283 }
1284 let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
1285 return false;
1286 };
1287 k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
1288 .is_ok()
1289 }
1290
1291 pub fn source_matches(&self, bytes: &[u8]) -> bool {
1292 content_hash(bytes) == self.source_hash
1293 }
1294 pub fn target_matches(&self, bytes: &[u8]) -> bool {
1295 content_hash(bytes) == self.target_hash
1296 }
1297
1298 pub fn receipt_hash(&self) -> [u8; 32] {
1299 let mut h = blake3::Hasher::new();
1300 h.update(DOMAIN_CMP_ID);
1301 h.update(&self.signing_payload());
1302 h.update(&self.sig);
1303 *h.finalize().as_bytes()
1304 }
1305
1306 pub fn to_json(&self) -> String {
1307 let parent = match self.parent_receipt_hash {
1308 Some(h) => format!("\"{}\"", hx(&h)),
1309 None => "null".into(),
1310 };
1311 format!(
1312 "{{\"kind\":\"quantum-compile\",\"coupling_hash\":\"{}\",\"equivalent\":{},\
1313 \"grant\":{},\"joules_micro\":{},\"merkle_root\":\"{}\",\"method\":{},\
1314 \"parent_receipt_hash\":{},\"receipt_hash\":\"{}\",\"sig\":\"{}\",\
1315 \"signer_id\":{},\"signer_pubkey\":\"{}\",\"source_hash\":\"{}\",\"swaps\":{},\
1316 \"target_hash\":\"{}\",\"work\":{}}}",
1317 hx(&self.coupling_hash),
1318 self.equivalent,
1319 self.grant.to_json(),
1320 self.joules_micro,
1321 hx(&self.merkle()),
1322 serde_json::to_string(&self.method).unwrap(),
1323 parent,
1324 hx(&self.receipt_hash()),
1325 hx(&self.sig),
1326 serde_json::to_string(&self.signer_id).unwrap(),
1327 hx(&self.signer_pubkey),
1328 hx(&self.source_hash),
1329 self.swaps,
1330 hx(&self.target_hash),
1331 self.work,
1332 )
1333 }
1334
1335 pub fn from_json(s: &str) -> Option<CompileReceipt> {
1336 let v: serde_json::Value = serde_json::from_str(s).ok()?;
1337 let o = v.as_object()?;
1338 let parent = match o.get("parent_receipt_hash") {
1339 Some(serde_json::Value::String(s)) => Some(from_hex32(s)?),
1340 _ => None,
1341 };
1342 Some(CompileReceipt {
1343 source_hash: from_hex32(o.get("source_hash")?.as_str()?)?,
1344 target_hash: from_hex32(o.get("target_hash")?.as_str()?)?,
1345 coupling_hash: from_hex32(o.get("coupling_hash")?.as_str()?)?,
1346 method: o.get("method")?.as_str()?.to_owned(),
1347 equivalent: o.get("equivalent")?.as_bool()?,
1348 swaps: o.get("swaps")?.as_u64()? as u32,
1349 work: o.get("work")?.as_u64()?,
1350 joules_micro: o.get("joules_micro")?.as_u64()?,
1351 grant: GrantRef::from_json(o.get("grant")?)?,
1352 parent_receipt_hash: parent,
1353 signer_pubkey: from_hex32(o.get("signer_pubkey")?.as_str()?)?,
1354 signer_id: o.get("signer_id")?.as_str()?.to_owned(),
1355 sig: from_hex64(o.get("sig")?.as_str()?)?,
1356 })
1357 }
1358}
1359
1360#[derive(Clone, Debug, PartialEq)]
1380pub struct RearrangeReceipt {
1381 pub initial_hash: [u8; 32],
1383 pub target_hash: [u8; 32],
1385 pub plan_hash: [u8; 32],
1387 pub method: String,
1389 pub success: bool,
1391 pub atoms_moved: u32,
1393 pub total_distance: u64,
1395 pub joules_micro: u64,
1397 pub grant: GrantRef,
1398 pub parent_receipt_hash: Option<[u8; 32]>,
1399 pub signer_pubkey: [u8; 32],
1400 pub signer_id: String,
1401 pub sig: [u8; 64],
1402}
1403
1404impl RearrangeReceipt {
1405 fn merkle(&self) -> [u8; 32] {
1406 merkle_root(&[self.initial_hash, self.target_hash, self.plan_hash])
1407 }
1408
1409 fn signing_payload(&self) -> Vec<u8> {
1410 self.payload_with(None)
1411 }
1412
1413 fn payload_with(&self, label: Option<&crate::quantum_energy::EnergyClass>) -> Vec<u8> {
1417 let mut o = crate::quantum_energy::labelled_domain(DOMAIN_REA, label);
1418 o.extend_from_slice(&self.initial_hash);
1419 o.extend_from_slice(&self.target_hash);
1420 o.extend_from_slice(&self.plan_hash);
1421 o.extend_from_slice(self.method.as_bytes());
1422 o.push(0);
1423 o.push(self.success as u8);
1424 o.extend_from_slice(&self.atoms_moved.to_be_bytes());
1425 o.extend_from_slice(&self.total_distance.to_be_bytes());
1426 o.extend_from_slice(&self.merkle());
1427 o.extend_from_slice(&self.joules_micro.to_be_bytes());
1428 crate::quantum_energy::write_label(label, &mut o);
1429 self.grant.append_to(&mut o);
1430 match self.parent_receipt_hash {
1431 Some(h) => {
1432 o.push(1);
1433 o.extend_from_slice(&h);
1434 }
1435 None => o.push(0),
1436 }
1437 o.extend_from_slice(&self.signer_pubkey);
1438 o.extend_from_slice(self.signer_id.as_bytes());
1439 o
1440 }
1441
1442 #[allow(clippy::too_many_arguments)]
1443 pub fn seal(
1444 signer: &SigningKey,
1445 signer_id: impl Into<String>,
1446 method: impl Into<String>,
1447 initial_hash: [u8; 32],
1448 target_hash: [u8; 32],
1449 plan_hash: [u8; 32],
1450 success: bool,
1451 atoms_moved: u32,
1452 total_distance: u64,
1453 joules_micro: u64,
1454 grant: GrantRef,
1455 parent_receipt_hash: Option<[u8; 32]>,
1456 ) -> RearrangeReceipt {
1457 let mut r = RearrangeReceipt {
1458 initial_hash,
1459 target_hash,
1460 plan_hash,
1461 method: method.into(),
1462 success,
1463 atoms_moved,
1464 total_distance,
1465 joules_micro,
1466 grant,
1467 parent_receipt_hash,
1468 signer_pubkey: signer.verifying_key().to_bytes(),
1469 signer_id: signer_id.into(),
1470 sig: [0u8; 64],
1471 };
1472 r.sig = signer.sign(&r.signing_payload()).to_bytes();
1473 r
1474 }
1475
1476 pub fn honors_budget(&self) -> bool {
1477 self.joules_micro <= self.grant.joule_ceiling_micro
1478 }
1479
1480 fn body_verifies(&self) -> bool {
1483 if !self.honors_budget() {
1484 return false;
1485 }
1486 true
1487 }
1488
1489 pub fn verify(&self) -> bool {
1490 if !self.body_verifies() {
1491 return false;
1492 }
1493 let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
1494 return false;
1495 };
1496 k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
1497 .is_ok()
1498 }
1499
1500 pub fn plan_matches(&self, bytes: &[u8]) -> bool {
1501 content_hash(bytes) == self.plan_hash
1502 }
1503
1504 pub fn receipt_hash(&self) -> [u8; 32] {
1505 let mut h = blake3::Hasher::new();
1506 h.update(DOMAIN_REA_ID);
1507 h.update(&self.signing_payload());
1508 h.update(&self.sig);
1509 *h.finalize().as_bytes()
1510 }
1511
1512 pub fn to_json(&self) -> String {
1513 let parent = match self.parent_receipt_hash {
1514 Some(h) => format!("\"{}\"", hx(&h)),
1515 None => "null".into(),
1516 };
1517 format!(
1518 "{{\"kind\":\"quantum-rearrange\",\"atoms_moved\":{},\"grant\":{},\
1519 \"initial_hash\":\"{}\",\"joules_micro\":{},\"merkle_root\":\"{}\",\"method\":{},\
1520 \"parent_receipt_hash\":{},\"plan_hash\":\"{}\",\"receipt_hash\":\"{}\",\"sig\":\"{}\",\
1521 \"signer_id\":{},\"signer_pubkey\":\"{}\",\"success\":{},\"target_hash\":\"{}\",\
1522 \"total_distance\":{}}}",
1523 self.atoms_moved,
1524 self.grant.to_json(),
1525 hx(&self.initial_hash),
1526 self.joules_micro,
1527 hx(&self.merkle()),
1528 serde_json::to_string(&self.method).unwrap(),
1529 parent,
1530 hx(&self.plan_hash),
1531 hx(&self.receipt_hash()),
1532 hx(&self.sig),
1533 serde_json::to_string(&self.signer_id).unwrap(),
1534 hx(&self.signer_pubkey),
1535 self.success,
1536 hx(&self.target_hash),
1537 self.total_distance,
1538 )
1539 }
1540
1541 pub fn from_json(s: &str) -> Option<RearrangeReceipt> {
1542 let v: serde_json::Value = serde_json::from_str(s).ok()?;
1543 let o = v.as_object()?;
1544 let parent = match o.get("parent_receipt_hash") {
1545 Some(serde_json::Value::String(s)) => Some(from_hex32(s)?),
1546 _ => None,
1547 };
1548 Some(RearrangeReceipt {
1549 initial_hash: from_hex32(o.get("initial_hash")?.as_str()?)?,
1550 target_hash: from_hex32(o.get("target_hash")?.as_str()?)?,
1551 plan_hash: from_hex32(o.get("plan_hash")?.as_str()?)?,
1552 method: o.get("method")?.as_str()?.to_owned(),
1553 success: o.get("success")?.as_bool()?,
1554 atoms_moved: o.get("atoms_moved")?.as_u64()? as u32,
1555 total_distance: o.get("total_distance")?.as_u64()?,
1556 joules_micro: o.get("joules_micro")?.as_u64()?,
1557 grant: GrantRef::from_json(o.get("grant")?)?,
1558 parent_receipt_hash: parent,
1559 signer_pubkey: from_hex32(o.get("signer_pubkey")?.as_str()?)?,
1560 signer_id: o.get("signer_id")?.as_str()?.to_owned(),
1561 sig: from_hex64(o.get("sig")?.as_str()?)?,
1562 })
1563 }
1564}
1565
1566#[cfg(test)]
1567mod tests {
1568 use super::*;
1569
1570 fn key(s: u8) -> SigningKey {
1571 SigningKey::from_bytes(&[s; 32])
1572 }
1573
1574 fn evid() -> Vec<Evidence> {
1575 vec![Evidence {
1576 kind: "randomized_benchmarking".into(),
1577 blob_hash: content_hash(b"<HDF5 RB dataset bytes>"),
1578 summary: "1q RB fidelity 0.9992".into(),
1579 }]
1580 }
1581
1582 #[test]
1583 fn calibration_seals_and_verifies() {
1584 let grant = GrantRef {
1585 grant_hash: content_hash(b"grant-A"),
1586 capability: "quantum.calibrate".into(),
1587 joule_ceiling_micro: 5_000_000,
1588 funds_ceiling: Some(100),
1589 };
1590 let r = CalibrationReceipt::seal(
1591 &key(1), "did:key:lab", "example:lab3-device", "q3",
1592 content_hash(b"pulse-config-v7"), evid(), 4_200_000, grant, None,
1593 );
1594 assert!(r.verify());
1595 assert!(r.honors_budget());
1596 assert!(r.evidence_matches(0, b"<HDF5 RB dataset bytes>"));
1598 assert!(!r.evidence_matches(0, b"<tampered dataset>"));
1599 }
1600
1601 #[test]
1602 fn over_budget_calibration_fails_verify() {
1603 let grant = GrantRef {
1605 grant_hash: [0u8; 32],
1606 capability: "quantum.calibrate".into(),
1607 joule_ceiling_micro: 1_000_000,
1608 funds_ceiling: None,
1609 };
1610 let r = CalibrationReceipt::seal(
1611 &key(2), "m", "dev", "q0", content_hash(b"cfg"), evid(),
1612 2_000_000, grant, None,
1614 );
1615 assert!(!r.honors_budget());
1616 assert!(!r.verify(), "a run that blew its energy grant must not verify");
1617 }
1618
1619 #[test]
1620 fn tamper_breaks_calibration() {
1621 let mut r = CalibrationReceipt::seal(
1622 &key(3), "m", "dev", "q0", content_hash(b"cfg"), evid(),
1623 10, GrantRef::unbounded("quantum.calibrate"), None,
1624 );
1625 r.joules_micro += 1; assert!(!r.verify());
1627 }
1628
1629 #[test]
1630 fn job_binds_to_calibration_state() {
1631 let cal = CalibrationReceipt::seal(
1634 &key(4), "lab", "dev", "q3", content_hash(b"cfg"), evid(),
1635 100, GrantRef::unbounded("quantum.calibrate"), None,
1636 );
1637 let job = JobReceipt::seal(
1638 &key(4), "lab", "example:qpu-a",
1639 content_hash(b"<QASM circuit>"), content_hash(b"{counts:{00:512,11:488}}"),
1640 Some(cal.receipt_hash()), 1000, 250, GrantRef::unbounded("quantum.job.run"), None,
1641 );
1642 assert!(job.verify());
1643 assert!(job.verify_against_calibration(&cal));
1644 assert!(job.result_matches(b"{counts:{00:512,11:488}}"));
1645
1646 let other = CalibrationReceipt::seal(
1648 &key(4), "lab", "dev", "q3", content_hash(b"cfg2"), evid(),
1649 100, GrantRef::unbounded("quantum.calibrate"), None,
1650 );
1651 assert!(!job.verify_against_calibration(&other));
1652 }
1653
1654 #[test]
1655 fn over_budget_job_fails_verify() {
1656 let grant = GrantRef {
1657 grant_hash: [0u8; 32],
1658 capability: "quantum.job.run".into(),
1659 joule_ceiling_micro: 200,
1660 funds_ceiling: None,
1661 };
1662 let job = JobReceipt::seal(
1663 &key(5), "m", "sim", content_hash(b"c"), content_hash(b"r"),
1664 None, 100, 500, grant, None,
1665 );
1666 assert!(!job.verify());
1667 }
1668
1669 #[test]
1670 fn json_round_trips() {
1671 let cal = CalibrationReceipt::seal(
1672 &key(6), "did:key:z", "dev", "cz(q3,q4)", content_hash(b"cfg"), evid(),
1673 900, GrantRef { grant_hash: content_hash(b"g"), capability: "quantum.calibrate".into(), joule_ceiling_micro: 1_000_000, funds_ceiling: None },
1674 None,
1675 );
1676 assert_eq!(CalibrationReceipt::from_json(&cal.to_json()).unwrap(), cal);
1677
1678 let job = JobReceipt::seal(
1679 &key(6), "did:key:z", "example:qpu-a", content_hash(b"c"), content_hash(b"r"),
1680 Some(cal.receipt_hash()), 2048, 42, GrantRef::unbounded("quantum.job.run"), Some(cal.receipt_hash()),
1681 );
1682 let back = JobReceipt::from_json(&job.to_json()).unwrap();
1683 assert_eq!(back, job);
1684 assert!(back.verify());
1685 }
1686
1687 #[test]
1688 fn calibration_lineage_chains() {
1689 let g = GrantRef::unbounded("quantum.calibrate");
1690 let v1 = CalibrationReceipt::seal(&key(7), "m", "dev", "q0", content_hash(b"c1"), evid(), 10, g.clone(), None);
1691 let v2 = CalibrationReceipt::seal(&key(7), "m", "dev", "q0", content_hash(b"c2"), evid(), 12, g, Some(v1.receipt_hash()));
1692 assert!(v2.verify());
1693 assert_eq!(v2.parent_receipt_hash, Some(v1.receipt_hash()));
1694 }
1695
1696 #[test]
1697 fn mitigation_seals_and_binds_inputs() {
1698 let raw = b"<noise-scaled expectation table: (1.0,0.71),(1.5,0.63),(2.0,0.57)>";
1700 let r = MitigationReceipt::seal(
1701 &key(1), "did:key:lab", "wai.quantum.circuit", "zne.richardson", "ZZ",
1702 content_hash(raw), None,
1703 744_178, 912_680, 20_971, 48_000, 900_000, GrantRef::unbounded("quantum.mitigate"), None,
1707 );
1708 assert!(r.verify());
1709 assert!(r.input_matches(raw));
1710 assert!(!r.input_matches(b"<different shots>"));
1711 assert!(r.noise_model_hash.is_none());
1712 }
1713
1714 #[test]
1715 fn mitigation_binds_pinned_noise_model() {
1716 let raw = b"<measured counts>";
1718 let model = b"<readout assignment matrix A>";
1719 let r = MitigationReceipt::seal(
1720 &key(2), "lab", "ibm:torino", "readout.tensored", "<Z0>",
1721 content_hash(raw), Some(content_hash(model)),
1722 838_861, 1_048_576, 5_242, 32_000, 600_000,
1723 GrantRef::unbounded("quantum.mitigate"), None,
1724 );
1725 assert!(r.verify());
1726 assert!(r.noise_model_matches(model));
1727 assert!(!r.noise_model_matches(b"<a different device's matrix>"));
1728 }
1729
1730 #[test]
1731 fn mitigation_over_budget_and_tamper_fail() {
1732 let grant = GrantRef {
1733 grant_hash: [0u8; 32],
1734 capability: "quantum.mitigate".into(),
1735 joule_ceiling_micro: 500_000,
1736 funds_ceiling: None,
1737 };
1738 let over = MitigationReceipt::seal(
1739 &key(3), "m", "sim", "zne.linear", "X",
1740 content_hash(b"in"), None, 0, 100, 1, 100, 900_000, grant, None,
1741 );
1742 assert!(!over.verify(), "a mitigation that blew its energy grant must not verify");
1743
1744 let mut r = MitigationReceipt::seal(
1745 &key(3), "m", "sim", "zne.linear", "X",
1746 content_hash(b"in"), None, 0, 100, 1, 100, 10,
1747 GrantRef::unbounded("quantum.mitigate"), None,
1748 );
1749 r.mitigated_estimate_fx += 1; assert!(!r.verify(), "tampering with the mitigated estimate must break the receipt");
1751 }
1752
1753 #[test]
1754 fn mitigation_json_round_trips() {
1755 let r = MitigationReceipt::seal(
1756 &key(4), "did:key:z", "wai.quantum.circuit", "shadow.pauli", "XYZ",
1757 content_hash(b"<snapshots>"), None,
1758 -262_144, -314_573, 15_728, 4096, 42,
1759 GrantRef { grant_hash: content_hash(b"g"), capability: "quantum.mitigate".into(), joule_ceiling_micro: 1_000_000, funds_ceiling: None },
1760 None,
1761 );
1762 let back = MitigationReceipt::from_json(&r.to_json()).unwrap();
1763 assert_eq!(back, r);
1764 assert!(back.verify());
1765 assert_eq!(back.raw_estimate_fx, -262_144); }
1767
1768 #[test]
1769 fn decode_seals_and_binds() {
1770 let hmat = b"<parity-check matrix H_Z of the gross code>";
1771 let synd = b"<syndrome 001011...>";
1772 let corr = b"<correction 000010...>";
1773 let r = DecodeReceipt::seal(
1774 &key(1), "did:key:lab", "bb:[[72,12]]", "relay-bp.min-sum",
1775 content_hash(hmat), content_hash(synd), content_hash(corr),
1776 true, 3, 46_656, 900_000, GrantRef::unbounded("quantum.decode"), None,
1777 );
1778 assert!(r.verify());
1779 assert!(r.converged);
1780 assert!(r.correction_matches(corr));
1781 assert!(!r.correction_matches(b"<a different correction>"));
1782 assert!(r.code_matches(hmat));
1783 }
1784
1785 #[test]
1786 fn decode_over_budget_and_tamper_fail() {
1787 let grant = GrantRef {
1788 grant_hash: [0u8; 32],
1789 capability: "quantum.decode".into(),
1790 joule_ceiling_micro: 500_000,
1791 funds_ceiling: None,
1792 };
1793 let over = DecodeReceipt::seal(
1794 &key(2), "m", "toric:L5", "relay-bp.min-sum",
1795 content_hash(b"H"), content_hash(b"s"), content_hash(b"c"),
1796 true, 1, 100, 900_000, grant, None,
1797 );
1798 assert!(!over.verify(), "a decode that blew its energy grant must not verify");
1799
1800 let mut r = DecodeReceipt::seal(
1801 &key(2), "m", "toric:L5", "relay-bp.min-sum",
1802 content_hash(b"H"), content_hash(b"s"), content_hash(b"c"),
1803 true, 1, 100, 10, GrantRef::unbounded("quantum.decode"), None,
1804 );
1805 r.work_messages += 1; assert!(!r.verify(), "tampering with the reported work must break the receipt");
1807 }
1808
1809 #[test]
1810 fn decode_json_round_trips() {
1811 let r = DecodeReceipt::seal(
1812 &key(3), "did:key:z", "toric:L7", "relay-bp.min-sum",
1813 content_hash(b"H"), content_hash(b"s"), content_hash(b"c"),
1814 false, 12, 1_234_567, 42,
1815 GrantRef { grant_hash: content_hash(b"g"), capability: "quantum.decode".into(), joule_ceiling_micro: 2_000_000, funds_ceiling: None },
1816 Some(content_hash(b"prev")),
1817 );
1818 let back = DecodeReceipt::from_json(&r.to_json()).unwrap();
1819 assert_eq!(back, r);
1820 assert!(back.verify());
1821 assert!(!back.converged); }
1823
1824 #[test]
1825 fn compile_seals_and_binds() {
1826 let src = b"<logical circuit: H0 CX0,2 ...>";
1827 let tgt = b"<routed circuit + final map: H0 SWAP1,2 CX0,1 ...>";
1828 let cpl = b"<coupling: line 0-1-2-3-4>";
1829 let r = CompileReceipt::seal(
1830 &key(1), "did:key:lab", "clifford-tableau",
1831 content_hash(src), content_hash(tgt), content_hash(cpl),
1832 true, 3, 4096, 300_000, GrantRef::unbounded("quantum.compile"), None,
1833 );
1834 assert!(r.verify());
1835 assert!(r.equivalent);
1836 assert!(r.source_matches(src));
1837 assert!(r.target_matches(tgt));
1838 assert!(!r.target_matches(b"<a tampered routed circuit>"));
1839 }
1840
1841 #[test]
1842 fn compile_over_budget_and_tamper_fail() {
1843 let grant = GrantRef {
1844 grant_hash: [0u8; 32],
1845 capability: "quantum.compile".into(),
1846 joule_ceiling_micro: 100_000,
1847 funds_ceiling: None,
1848 };
1849 let over = CompileReceipt::seal(
1850 &key(2), "m", "clifford-tableau",
1851 content_hash(b"s"), content_hash(b"t"), content_hash(b"c"),
1852 true, 0, 10, 900_000, grant, None,
1853 );
1854 assert!(!over.verify(), "a compile that blew its energy grant must not verify");
1855
1856 let mut r = CompileReceipt::seal(
1857 &key(2), "m", "clifford-tableau",
1858 content_hash(b"s"), content_hash(b"t"), content_hash(b"c"),
1859 true, 0, 10, 5, GrantRef::unbounded("quantum.compile"), None,
1860 );
1861 r.equivalent = false; assert!(!r.verify(), "tampering with the equivalence verdict must break the receipt");
1863 }
1864
1865 #[test]
1866 fn compile_json_round_trips() {
1867 let r = CompileReceipt::seal(
1868 &key(3), "did:key:z", "clifford-tableau",
1869 content_hash(b"s"), content_hash(b"t"), content_hash(b"c"),
1870 false, 7, 99_999, 21,
1871 GrantRef { grant_hash: content_hash(b"g"), capability: "quantum.compile".into(), joule_ceiling_micro: 1_000_000, funds_ceiling: None },
1872 None,
1873 );
1874 let back = CompileReceipt::from_json(&r.to_json()).unwrap();
1875 assert_eq!(back, r);
1876 assert!(back.verify());
1877 assert!(!back.equivalent);
1878 }
1879
1880 #[test]
1881 fn rearrange_seals_binds_and_json_round_trips() {
1882 let r = RearrangeReceipt::seal(
1883 &key(1), "did:key:lab", "hungarian-lsap",
1884 content_hash(b"<loaded 16x16, 58% fill>"),
1885 content_hash(b"<target 8x8 centered>"),
1886 content_hash(b"<moves: (12->20),(33->21),...>"),
1887 true, 41, 318, 450_000, GrantRef::unbounded("quantum.rearrange"), None,
1888 );
1889 assert!(r.verify());
1890 assert!(r.success);
1891 assert!(r.plan_matches(b"<moves: (12->20),(33->21),...>"));
1892 assert!(!r.plan_matches(b"<a different plan>"));
1893 let back = RearrangeReceipt::from_json(&r.to_json()).unwrap();
1894 assert_eq!(back, r);
1895 assert!(back.verify());
1896 }
1897
1898 #[test]
1899 fn rearrange_over_budget_and_tamper_fail() {
1900 let grant = GrantRef {
1901 grant_hash: [0u8; 32], capability: "quantum.rearrange".into(),
1902 joule_ceiling_micro: 100_000, funds_ceiling: None,
1903 };
1904 let over = RearrangeReceipt::seal(
1905 &key(2), "m", "hungarian-lsap",
1906 content_hash(b"i"), content_hash(b"t"), content_hash(b"p"),
1907 true, 10, 50, 900_000, grant, None,
1908 );
1909 assert!(!over.verify(), "a solve that blew its energy grant must not verify");
1910
1911 let mut r = RearrangeReceipt::seal(
1912 &key(2), "m", "hungarian-lsap",
1913 content_hash(b"i"), content_hash(b"t"), content_hash(b"p"),
1914 true, 10, 50, 5, GrantRef::unbounded("quantum.rearrange"), None,
1915 );
1916 r.success = false; assert!(!r.verify(), "tampering with the success verdict must break the receipt");
1918 }
1919}
1920
1921crate::quantum_energy::labellable!(CalibrationReceipt, DOMAIN_CAL_ID);
1922
1923crate::quantum_energy::labellable!(JobReceipt, DOMAIN_JOB_ID);
1924
1925crate::quantum_energy::labellable!(MitigationReceipt, DOMAIN_MIT_ID);
1926
1927crate::quantum_energy::labellable!(DecodeReceipt, DOMAIN_DEC_ID);
1928
1929crate::quantum_energy::labellable!(CompileReceipt, DOMAIN_CMP_ID);
1930
1931crate::quantum_energy::labellable!(RearrangeReceipt, DOMAIN_REA_ID);