Sixteen sites still decoded the key with VerifyingKey::from_bytes and checked with the lenient verify, which accepts a key of small order (under the identity, (R, S) = (B, 1) verifies over any message), a second spelling of a point (y = p + j is reduced), and an R of small order. Each now calls crate::sig::verify_strict: the video, SSAI, key-release, channel and package receipts (v1 and v2), the provenance, ternary and the five media receipts, and the world's receipt, op signature and container seal. A package names a bad key apart from a bad signature under receipt-signature; an op's bad key is still BadActorKey. claim_receipt's own copy of the rule (signer_key, verify_ed25519_strict) is gone: claims, JWP groups and the key log call the same routine, and its key test moved into sig.rs. sig builds under a new `ed25519` feature that is only the dependency, which world and oer enable, so the helper exists exactly when ed25519-dalek does and asks for nothing else. sig::forgeries builds, for any signing payload, the three signatures a lenient verifier accepts and the rule refuses; every moved site has a test that its receipt refuses all three. oer.rs is untouched: OER/2's settled rule refuses the same keys but not R of small order. Co-Authored-By: Claude Opus 5.5 <[email protected]>
351 lines
14 KiB
Rust
351 lines
14 KiB
Rust
//! Ternary-MPS receipt — the energy-accounted seal for a `wai.ternary.tt`
|
|
//! reconstruction (extensions/ternary-tt § Receipt).
|
|
//!
|
|
//! The whole reason `wai.ternary.tt` exists is provenance, not compression rate:
|
|
//! a quantum-inspired transform whose reconstruction is byte-exact, portable, and
|
|
//! auditable. This receipt is where that pays off — the same design as
|
|
//! [`crate::quantum_receipt::QuantumReceipt`], separating the two costs so each
|
|
//! is trusted the way it honestly can be:
|
|
//!
|
|
//! - **work** (`trit_macs = Σ trit-tensor sizes`) — portable-exact, recomputable
|
|
//! by anyone from the container, a sink cannot inflate it. **Verified.**
|
|
//! - **joules_micro** — measured marginal energy the sink spent reconstructing.
|
|
//! **Attested** by signature (the `wai_quantum_meter` IOReport pattern applies
|
|
//! unchanged), with its acquisition class as an optional label
|
|
//! (`extensions/energy-measurement.md` §2.8).
|
|
//!
|
|
//! Both bind to two hashes: `payload_hash` (BLAKE3 of the canonical `WQTT`
|
|
//! bytes — tamper-evidence) and `reconstruction_hash` (the decoded signal —
|
|
//! re-checkable by re-decoding). A JWP profile: the world/provenance Merkle +
|
|
//! Ed25519, reused, chaining via `parent_receipt_hash`.
|
|
|
|
use crate::ternary::{from_wqtt, TernaryStack};
|
|
use crate::energy_class::{
|
|
check_label, json_with_label, label_coheres, label_from_json, labelled_domain, write_label,
|
|
AcquisitionError, EnergyProvenance,
|
|
};
|
|
use crate::world::receipt::merkle_root;
|
|
use ed25519_dalek::{Signer, SigningKey};
|
|
|
|
const DOMAIN_RECEIPT: &[u8] = b"wai:ternary-receipt\x01";
|
|
const DOMAIN_RECEIPT_ID: &[u8] = b"wai:ternary-receipt-id\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)
|
|
}
|
|
|
|
/// A signed, energy-accounted seal over one `wai.ternary.tt` reconstruction.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct TernaryReceipt {
|
|
/// BLAKE3 of the canonical `WQTT` payload bytes — identity + tamper-evidence.
|
|
pub payload_hash: [u8; 32],
|
|
/// BLAKE3 of the decoded signal — the reconstruction, re-checkable.
|
|
pub reconstruction_hash: [u8; 32],
|
|
pub n_signal: u64,
|
|
pub n_layers: u32,
|
|
/// Portable-exact reconstruction work (Σ trit-tensor sizes). Verified by
|
|
/// recomputation from the container.
|
|
pub trit_macs: u64,
|
|
/// Measured microjoules the sink spent reconstructing (`0` = unmetered).
|
|
/// Attested by the signature, not independently verifiable.
|
|
pub joules_micro: u64,
|
|
/// How `joules_micro` was acquired — an optional label
|
|
/// (`extensions/energy-measurement.md` §2.8). `None` is an unlabelled
|
|
/// figure, as every receipt sealed before the label existed carries; it is
|
|
/// never read as any class. Inside the signature when present.
|
|
pub energy_provenance: Option<EnergyProvenance>,
|
|
pub merkle_root: [u8; 32],
|
|
pub parent_receipt_hash: Option<[u8; 32]>,
|
|
pub signer_pubkey: [u8; 32],
|
|
pub signer_id: String,
|
|
pub sig: [u8; 64],
|
|
}
|
|
|
|
impl TernaryReceipt {
|
|
fn leaves(&self) -> Vec<[u8; 32]> {
|
|
vec![self.payload_hash, self.reconstruction_hash]
|
|
}
|
|
|
|
fn signing_payload(&self) -> Vec<u8> {
|
|
let label = self.energy_provenance.as_ref();
|
|
let mut o = labelled_domain(DOMAIN_RECEIPT, label);
|
|
o.extend_from_slice(&self.payload_hash);
|
|
o.extend_from_slice(&self.reconstruction_hash);
|
|
o.extend_from_slice(&self.n_signal.to_be_bytes());
|
|
o.extend_from_slice(&self.n_layers.to_be_bytes());
|
|
o.extend_from_slice(&self.trit_macs.to_be_bytes());
|
|
o.extend_from_slice(&self.joules_micro.to_be_bytes());
|
|
write_label(label, &mut o);
|
|
o.extend_from_slice(&self.merkle_root);
|
|
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
|
|
}
|
|
|
|
/// Seal a receipt over `stack`: hash its canonical `WQTT` bytes and its
|
|
/// decode, bind the exact work and the measured `joules_micro` (`0` if
|
|
/// unmetered), and sign. Sealing decodes the stack, so the
|
|
/// `reconstruction_hash` is always the true reconstruction, never a claim.
|
|
pub fn seal(
|
|
signer: &SigningKey,
|
|
signer_id: impl Into<String>,
|
|
stack: &TernaryStack,
|
|
joules_micro: u64,
|
|
parent_receipt_hash: Option<[u8; 32]>,
|
|
) -> TernaryReceipt {
|
|
let payload = stack.to_wqtt();
|
|
let payload_hash = *blake3::hash(&payload).as_bytes();
|
|
let reconstruction_hash = stack.reconstruction_hash();
|
|
let mut r = TernaryReceipt {
|
|
payload_hash,
|
|
reconstruction_hash,
|
|
n_signal: stack.n_signal as u64,
|
|
n_layers: stack.layers.len() as u32,
|
|
trit_macs: stack.trit_macs(),
|
|
joules_micro,
|
|
energy_provenance: None,
|
|
merkle_root: [0u8; 32],
|
|
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
|
|
}
|
|
|
|
/// Merkle root recomputes and the Ed25519 signature is valid. Does not
|
|
/// re-decode — see [`Self::verify_reconstruction`].
|
|
pub fn verify(&self) -> bool {
|
|
if !label_coheres(self.joules_micro, self.energy_provenance.as_ref()) {
|
|
return false;
|
|
}
|
|
if merkle_root(&self.leaves()) != self.merkle_root {
|
|
return false;
|
|
}
|
|
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
|
}
|
|
|
|
/// The strong, portable check: recover the stack from the `WQTT` payload,
|
|
/// confirm it hashes to this receipt's `payload_hash`, decodes to its
|
|
/// `reconstruction_hash`, and matches its `trit_macs` — then that the
|
|
/// receipt verifies. Rejects a receipt whose claimed reconstruction is not
|
|
/// what the payload actually decodes to, on any machine. (The measurement —
|
|
/// `joules_micro` and the class that labels it — stays attested-only.)
|
|
pub fn verify_reconstruction(&self, wqtt_payload: &[u8]) -> bool {
|
|
if *blake3::hash(wqtt_payload).as_bytes() != self.payload_hash {
|
|
return false;
|
|
}
|
|
let Ok(stack) = from_wqtt(wqtt_payload) else {
|
|
return false;
|
|
};
|
|
if stack.n_signal as u64 != self.n_signal
|
|
|| stack.layers.len() as u32 != self.n_layers
|
|
|| stack.trit_macs() != self.trit_macs
|
|
{
|
|
return false;
|
|
}
|
|
if stack.reconstruction_hash() != self.reconstruction_hash {
|
|
return false;
|
|
}
|
|
self.verify()
|
|
}
|
|
|
|
/// Label the figure with its acquisition class and re-sign
|
|
/// (`extensions/energy-measurement.md` §2.8). `signer` must be the key
|
|
/// that sealed the receipt. Refuses whatever [`Self::verify`] would
|
|
/// refuse: legacy `HwShunt`, a class beside a zero figure, a refined class
|
|
/// without its evidence.
|
|
pub fn with_energy_class(
|
|
mut self,
|
|
signer: &SigningKey,
|
|
class: EnergyProvenance,
|
|
) -> Result<TernaryReceipt, AcquisitionError> {
|
|
if signer.verifying_key().to_bytes() != self.signer_pubkey {
|
|
return Err(AcquisitionError::SignerMismatch);
|
|
}
|
|
check_label(self.joules_micro, Some(&class))?;
|
|
self.energy_provenance = Some(class);
|
|
self.sig = signer.sign(&self.signing_payload()).to_bytes();
|
|
Ok(self)
|
|
}
|
|
|
|
/// Re-sign whatever the fields now say, bypassing every check — so tests
|
|
/// can present `verify()` with a correctly signed receipt that breaks a
|
|
/// label rule, and see the rule itself refuse it.
|
|
#[cfg(test)]
|
|
pub(crate) fn resign_for_test(&mut self, signer: &SigningKey) {
|
|
self.sig = signer.sign(&self.signing_payload()).to_bytes();
|
|
}
|
|
|
|
/// Stable id for chaining a derived receipt via `parent_receipt_hash`.
|
|
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()
|
|
}
|
|
|
|
/// Canonical JSON (hex hashes, sorted keys).
|
|
pub fn to_json(&self) -> String {
|
|
let parent = match self.parent_receipt_hash {
|
|
Some(h) => format!("\"{}\"", hx(&h)),
|
|
None => "null".into(),
|
|
};
|
|
let json = format!(
|
|
"{{\"kind\":\"ternary-tt\",\"joules_micro\":{},\"merkle_root\":\"{}\",\
|
|
\"n_layers\":{},\"n_signal\":{},\"parent_receipt_hash\":{},\
|
|
\"payload_hash\":\"{}\",\"receipt_hash\":\"{}\",\
|
|
\"reconstruction_hash\":\"{}\",\"sig\":\"{}\",\"signer_id\":{},\
|
|
\"signer_pubkey\":\"{}\",\"trit_macs\":{}}}",
|
|
self.joules_micro,
|
|
hx(&self.merkle_root),
|
|
self.n_layers,
|
|
self.n_signal,
|
|
parent,
|
|
hx(&self.payload_hash),
|
|
hx(&self.receipt_hash()),
|
|
hx(&self.reconstruction_hash),
|
|
hx(&self.sig),
|
|
serde_json::to_string(&self.signer_id).unwrap(),
|
|
hx(&self.signer_pubkey),
|
|
self.trit_macs,
|
|
);
|
|
json_with_label(json, self.energy_provenance.as_ref())
|
|
}
|
|
|
|
/// Parse a receipt from its canonical JSON. A malformed label (evidence
|
|
/// without a class, a refined class without its evidence, an unknown
|
|
/// label) is refused rather than dropped, so a parse can never turn a
|
|
/// labelled receipt into an unlabelled one.
|
|
pub fn from_json(s: &str) -> Option<TernaryReceipt> {
|
|
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(TernaryReceipt {
|
|
payload_hash: from_hex32(o.get("payload_hash")?.as_str()?)?,
|
|
reconstruction_hash: from_hex32(o.get("reconstruction_hash")?.as_str()?)?,
|
|
n_signal: u("n_signal")?,
|
|
n_layers: u("n_layers")? as u32,
|
|
trit_macs: u("trit_macs")?,
|
|
joules_micro: u("joules_micro")?,
|
|
energy_provenance: label_from_json(o).ok()?,
|
|
merkle_root: from_hex32(o.get("merkle_root")?.as_str()?)?,
|
|
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::*;
|
|
use crate::ternary::encode_residual;
|
|
|
|
fn key(s: u8) -> SigningKey {
|
|
SigningKey::from_bytes(&[s; 32])
|
|
}
|
|
|
|
fn stream(n: usize) -> Vec<i64> {
|
|
(0..n)
|
|
.map(|i| {
|
|
let x = i as f64 / n as f64;
|
|
(700.0 * (2.0 * std::f64::consts::PI * 5.0 * x).sin()).round() as i64
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn seals_verifies_and_reconstructs() {
|
|
let stack = encode_residual(&stream(256), 8, 2, 4, 3);
|
|
let r = TernaryReceipt::seal(&key(1), "did:key:test", &stack, 4200, None);
|
|
assert!(r.verify());
|
|
assert!(r.verify_reconstruction(&stack.to_wqtt()));
|
|
assert_eq!(r.trit_macs, stack.trit_macs());
|
|
assert_eq!(r.n_signal, 256);
|
|
assert_eq!(r.joules_micro, 4200);
|
|
}
|
|
|
|
#[test]
|
|
fn tamper_breaks_verification() {
|
|
let stack = encode_residual(&stream(256), 8, 2, 4, 3);
|
|
let mut r = TernaryReceipt::seal(&key(2), "m", &stack, 0, None);
|
|
r.joules_micro += 1; // a signed field
|
|
assert!(!r.verify());
|
|
}
|
|
|
|
#[test]
|
|
fn a_lie_about_the_payload_is_caught() {
|
|
let a = encode_residual(&stream(256), 8, 2, 4, 3);
|
|
let b = encode_residual(&stream(256), 8, 2, 2, 2); // different codec params
|
|
let r = TernaryReceipt::seal(&key(3), "m", &a, 0, None);
|
|
// b's WQTT does not match a's payload_hash.
|
|
assert!(!r.verify_reconstruction(&b.to_wqtt()));
|
|
}
|
|
|
|
#[test]
|
|
fn json_round_trips() {
|
|
let stack = encode_residual(&stream(256), 8, 2, 4, 2);
|
|
let r = TernaryReceipt::seal(&key(4), "did:key:z6Mk", &stack, 900, None);
|
|
let back = TernaryReceipt::from_json(&r.to_json()).expect("parse");
|
|
assert_eq!(r, back);
|
|
assert!(back.verify());
|
|
}
|
|
|
|
#[test]
|
|
fn chains_to_a_parent() {
|
|
let s1 = encode_residual(&stream(256), 8, 2, 4, 2);
|
|
let root = TernaryReceipt::seal(&key(5), "m", &s1, 10, None);
|
|
let s2 = encode_residual(&stream(256), 8, 2, 4, 3);
|
|
let child = TernaryReceipt::seal(&key(5), "m", &s2, 12, Some(root.receipt_hash()));
|
|
assert!(child.verify());
|
|
assert_eq!(child.parent_receipt_hash, Some(root.receipt_hash()));
|
|
}
|
|
|
|
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
|
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
|
/// lenient check accepts each.
|
|
#[test]
|
|
fn the_signature_is_verified_strictly() {
|
|
let stack = encode_residual(&stream(256), 8, 2, 4, 3);
|
|
let r = TernaryReceipt::seal(&key(1), "did:key:test", &stack, 4200, None);
|
|
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
|
}
|
|
}
|