use crate::merkle::merkle_root;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
const DOMAIN_RECEIPT: &[u8] = b"wai:phasor-receipt\x01";
const DOMAIN_RECEIPT_ID: &[u8] = b"wai:phasor-receipt-id\x01";
const DOMAIN_COMPUTE_ID: &[u8] = b"wai:phasor-compute-id\x01";
const DOMAIN_RESULT: &[u8] = b"wai:phasor-result\x01";
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 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let mut out = [0u8; 32];
for (i, c) in s.as_bytes().chunks(2).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 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let mut out = [0u8; 64];
for (i, c) in s.as_bytes().chunks(2).enumerate() {
out[i] = u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok()?;
}
Some(out)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PhasorWork {
pub n_features: u32,
pub input_dim: u32,
pub n_samples: u32,
}
impl PhasorWork {
pub fn phasor_ops(&self) -> u64 {
(self.n_features as u64)
.saturating_mul(self.input_dim as u64 + 1)
.saturating_mul(self.n_samples as u64)
}
}
pub fn computation_id(method: &str, params: &[f64]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(DOMAIN_COMPUTE_ID);
h.update(&(method.len() as u64).to_be_bytes());
h.update(method.as_bytes());
h.update(&(params.len() as u64).to_be_bytes());
for p in params {
h.update(&p.to_bits().to_be_bytes());
}
*h.finalize().as_bytes()
}
pub fn result_fingerprint(values: &[f64]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(DOMAIN_RESULT);
h.update(&(values.len() as u64).to_be_bytes());
for v in values {
h.update(&v.to_bits().to_be_bytes());
}
*h.finalize().as_bytes()
}
#[derive(Clone, Debug, PartialEq)]
pub struct PhasorReceipt {
pub computation_hash: [u8; 32],
pub method: String,
pub result_hash: [u8; 32],
pub work: PhasorWork,
pub merkle_root: [u8; 32],
pub phasor_ops: u64,
pub joules_micro: u64,
pub parent_receipt_hash: Option<[u8; 32]>,
pub signer_pubkey: [u8; 32],
pub signer_id: String,
pub sig: [u8; 64],
}
impl PhasorReceipt {
fn leaves(&self) -> Vec<[u8; 32]> {
vec![self.computation_hash, self.result_hash]
}
fn signing_payload(&self) -> Vec<u8> {
let mut o = DOMAIN_RECEIPT.to_vec();
o.extend_from_slice(&self.computation_hash);
o.extend_from_slice(&(self.method.len() as u64).to_be_bytes());
o.extend_from_slice(self.method.as_bytes());
o.extend_from_slice(&self.result_hash);
o.extend_from_slice(&self.work.n_features.to_be_bytes());
o.extend_from_slice(&self.work.input_dim.to_be_bytes());
o.extend_from_slice(&self.work.n_samples.to_be_bytes());
o.extend_from_slice(&self.merkle_root);
o.extend_from_slice(&self.phasor_ops.to_be_bytes());
o.extend_from_slice(&self.joules_micro.to_be_bytes());
match self.parent_receipt_hash {
Some(h) => {
o.push(1);
o.extend_from_slice(&h);
}
None => o.push(0),
}
o.extend_from_slice(&self.signer_pubkey);
o.extend_from_slice(self.signer_id.as_bytes());
o
}
#[allow(clippy::too_many_arguments)]
pub fn seal(
signer: &SigningKey,
signer_id: impl Into<String>,
method: impl Into<String>,
computation_hash: [u8; 32],
result_hash: [u8; 32],
work: PhasorWork,
joules_micro: u64,
parent_receipt_hash: Option<[u8; 32]>,
) -> PhasorReceipt {
let mut r = PhasorReceipt {
computation_hash,
method: method.into(),
result_hash,
work,
merkle_root: [0u8; 32],
phasor_ops: work.phasor_ops(),
joules_micro,
parent_receipt_hash,
signer_pubkey: signer.verifying_key().to_bytes(),
signer_id: signer_id.into(),
sig: [0u8; 64],
};
r.merkle_root = merkle_root(&r.leaves());
r.sig = signer.sign(&r.signing_payload()).to_bytes();
r
}
pub fn verify(&self) -> bool {
if merkle_root(&self.leaves()) != self.merkle_root {
return false;
}
if self.phasor_ops != self.work.phasor_ops() {
return false;
}
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 verify_result(&self, values: &[f64]) -> bool {
result_fingerprint(values) == self.result_hash && self.verify()
}
pub fn micro_joules_per_megaop(&self) -> f64 {
if self.joules_micro == 0 || self.phasor_ops == 0 {
return 0.0;
}
self.joules_micro as f64 / (self.phasor_ops as f64 / 1.0e6)
}
pub fn pico_joules_per_op(&self) -> f64 {
if self.joules_micro == 0 || self.phasor_ops == 0 {
return 0.0;
}
(self.joules_micro as f64 * 1.0e6) / self.phasor_ops as f64
}
pub fn receipt_hash(&self) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(DOMAIN_RECEIPT_ID);
h.update(&self.signing_payload());
h.update(&self.sig);
*h.finalize().as_bytes()
}
pub fn to_json(&self) -> String {
let parent = match self.parent_receipt_hash {
Some(h) => format!("\"{}\"", hx(&h)),
None => "null".into(),
};
format!(
"{{\"kind\":\"phasor\",\"computation_hash\":\"{}\",\"input_dim\":{},\
\"joules_micro\":{},\"method\":{},\"n_features\":{},\"n_samples\":{},\
\"parent_receipt_hash\":{},\"phasor_ops\":{},\"receipt_hash\":\"{}\",\
\"result_hash\":\"{}\",\"root_hash\":\"{}\",\"sig\":\"{}\",\"signer_id\":{},\
\"signer_pubkey\":\"{}\"}}",
hx(&self.computation_hash),
self.work.input_dim,
self.joules_micro,
serde_json::to_string(&self.method).unwrap(),
self.work.n_features,
self.work.n_samples,
parent,
self.phasor_ops,
hx(&self.receipt_hash()),
hx(&self.result_hash),
hx(&self.merkle_root),
hx(&self.sig),
serde_json::to_string(&self.signer_id).unwrap(),
hx(&self.signer_pubkey),
)
}
pub fn from_json(s: &str) -> Option<PhasorReceipt> {
let v: serde_json::Value = serde_json::from_str(s).ok()?;
let o = v.as_object()?;
let u = |k: &str| o.get(k).and_then(|x| x.as_u64());
let parent = match o.get("parent_receipt_hash") {
Some(serde_json::Value::String(s)) => Some(from_hex32(s)?),
_ => None,
};
Some(PhasorReceipt {
computation_hash: from_hex32(o.get("computation_hash")?.as_str()?)?,
method: o.get("method")?.as_str()?.to_owned(),
result_hash: from_hex32(o.get("result_hash")?.as_str()?)?,
work: PhasorWork {
n_features: u("n_features")? as u32,
input_dim: u("input_dim")? as u32,
n_samples: u("n_samples")? as u32,
},
merkle_root: from_hex32(o.get("root_hash")?.as_str()?)?,
phasor_ops: u("phasor_ops")?,
joules_micro: u("joules_micro")?,
parent_receipt_hash: parent,
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::*;
fn key(s: u8) -> SigningKey {
SigningKey::from_bytes(&[s; 32])
}
#[test]
fn work_is_linear_and_recomputable() {
let w = PhasorWork { n_features: 200, input_dim: 2, n_samples: 300 };
assert_eq!(w.phasor_ops(), 180_000);
let w2 = PhasorWork { n_features: 400, input_dim: 2, n_samples: 300 };
assert_eq!(w2.phasor_ops(), 360_000);
}
#[test]
fn seals_and_verifies() {
let ch = computation_id("wai.quantum.kernel", &[4.0, 7.0]);
let rh = result_fingerprint(&[0.1, -0.2, 0.3]);
let w = PhasorWork { n_features: 200, input_dim: 2, n_samples: 300 };
let r = PhasorReceipt::seal(&key(1), "did:key:test", "wai.quantum.kernel", ch, rh, w, 117_000, None);
assert!(r.verify());
assert_eq!(r.phasor_ops, 180_000);
assert_eq!(r.joules_micro, 117_000);
assert!((r.micro_joules_per_megaop() - 650_000.0).abs() < 1.0);
assert!((r.pico_joules_per_op() - 650_000.0).abs() < 1e-3);
}
#[test]
fn verify_result_catches_a_lie() {
let ch = computation_id("wai.quantum.born", &[3.0]);
let vals = [0.5, 0.5, 0.25, 0.75];
let rh = result_fingerprint(&vals);
let w = PhasorWork { n_features: 64, input_dim: 3, n_samples: 1 };
let r = PhasorReceipt::seal(&key(2), "m", "wai.quantum.born", ch, rh, w, 0, None);
assert!(r.verify_result(&vals));
assert!(!r.verify_result(&[0.5, 0.5, 0.25, 0.76]));
}
#[test]
fn tamper_breaks_verification() {
let ch = computation_id("wai.quantum.kernel", &[1.0]);
let rh = result_fingerprint(&[1.0]);
let w = PhasorWork { n_features: 10, input_dim: 2, n_samples: 5 };
let mut r = PhasorReceipt::seal(&key(3), "m", "wai.quantum.kernel", ch, rh, w, 42, None);
r.phasor_ops = 1;
assert!(!r.verify());
let mut r2 = PhasorReceipt::seal(&key(3), "m", "wai.quantum.kernel", ch, rh, w, 42, None);
r2.work.n_features = 1;
assert!(!r2.verify());
}
#[test]
fn unmetered_prices_are_zero() {
let ch = computation_id("wai.quantum.kernel", &[1.0]);
let rh = result_fingerprint(&[1.0]);
let w = PhasorWork { n_features: 10, input_dim: 2, n_samples: 5 };
let r = PhasorReceipt::seal(&key(4), "m", "wai.quantum.kernel", ch, rh, w, 0, None);
assert!(r.verify());
assert_eq!(r.micro_joules_per_megaop(), 0.0);
assert_eq!(r.pico_joules_per_op(), 0.0);
}
#[test]
fn json_round_trips_and_chains() {
let ch = computation_id("wai.quantum.kernel", &[4.0, 7.0]);
let rh = result_fingerprint(&[0.1, 0.2]);
let w = PhasorWork { n_features: 200, input_dim: 2, n_samples: 300 };
let root = PhasorReceipt::seal(&key(5), "did:key:z6Mk", "wai.quantum.kernel", ch, rh, w, 900, None);
let back = PhasorReceipt::from_json(&root.to_json()).expect("parse");
assert_eq!(root, back);
assert!(back.verify());
let child = PhasorReceipt::seal(
&key(5), "did:key:z6Mk", "wai.quantum.kernel", ch, rh, w, 950, Some(root.receipt_hash()),
);
assert!(child.verify());
assert_eq!(child.parent_receipt_hash, Some(root.receipt_hash()));
}
}