open-standards/wai/wai-rs/src/receipt_pins.rs
Transaction Science cc371d815a wai: energy reports, labelled JWP leaves and what signatures cover
Two claim classes report on one energy figure without changing the
receipt that seals it. `measurement` carries a bracket's values as
integers with each bracket's liveness facts, the build and the power
reader, and is accepted only against the receipt that carries its
figure. `operating-point` records the requested, committed and
delivered operating points and a prediction. The ratio, verdict and
residual are derived by every reader, never stated.

energy-measurement §6.2 gives the figure and the least declaration a
report's integers support as exact integer formulas; wai-rs
`energy_report` implements them with no floating-point arithmetic.
§3.1 defines a live power reader: a rail must advance in two
consecutive intervals, in at least five and in at least four of every
five, over at least 300 ms. A frozen or held rail is refused rather than
sealed under a hardware class. An unmetered `0` is never a figure of
zero joules.

A JWP object's figure, class and origin are bound into its Merkle leaf
(`\x02`), and a labelled group payload signs the total's class, its
coverage and the parent link. An object must sit at the position its
inclusion proof walks, and group signatures follow the strict Ed25519
rule claims use. `jwp_delivery` and `energy_figure` implement this in
wai-rs; joule-moq 0.2.0 builds and verifies both labelled forms.

New corpus `jwp-conformance/` with `jwp_verify.py`, more claim vectors
with the earlier ones pinned, and a CI job that runs claim_verify.py,
differential.py and jwp_verify.py. Legacy receipts keep their bytes.
External JWP implementations must adopt the `\x02` leaf and the
labelled group payload before verifying objects or groups that use
them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 06:37:25 -04:00

516 lines
22 KiB
Rust

//! Signed-byte pins for every receipt family that seals `joules_micro`.
//!
//! Each family below carried its energy figure with no acquisition class
//! before it was given an optional one. A receipt sealed without a class must
//! keep exactly the signed bytes it had before: otherwise every receipt
//! already issued stops verifying. So each
//! test builds one fixed, unlabelled receipt with a non-zero figure and pins
//! two things computed from the code *before* the class was added:
//!
//! - `receipt_hash()`, which covers the whole signing payload and the
//! signature, so any change to a signed byte changes it;
//! - where the family renders JSON, a BLAKE3 hash of `to_json()`, so the
//! rendered form of a legacy receipt is byte-identical too.
//!
//! The hex values were produced by the pre-change code and must never be
//! edited to make a test pass. A failure here means legacy receipts broke.
use ed25519_dalek::SigningKey;
fn key(s: u8) -> SigningKey {
SigningKey::from_bytes(&[s; 32])
}
fn hx(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn json_hash(j: &str) -> String {
hx(blake3::hash(j.as_bytes()).as_bytes())
}
// ---------------------------------------------------------------------------
// One fixed, unlabelled receipt per family, with a non-zero figure.
// ---------------------------------------------------------------------------
#[cfg(feature = "world")]
fn world() -> crate::world::receipt::SessionReceipt {
use crate::world::Checkpoint;
let cps = vec![
Checkpoint { tick: 100, hash: [0x11; 32] },
Checkpoint { tick: 200, hash: [0x22; 32] },
];
crate::world::receipt::SessionReceipt::build(&key(9), "did:key:pin", [0xab; 32], 3, &[], &cps, 12_345, 999, Some([0x44; 32]))
}
#[cfg(feature = "audio_scene")]
fn audio_scene() -> crate::audio_scene::SceneReceipt {
use crate::audio_scene::{BlockHash, SceneReceipt};
let blocks: Vec<BlockHash> = (0..3u64).map(|i| BlockHash { index: i, hash: [i as u8 + 7; 32] }).collect();
SceneReceipt::build(&key(9), "did:web:pin#audio", [0x5a; 32], 1, blocks, 777, 4_321, None)
}
#[cfg(feature = "film")]
fn film() -> crate::film::FilmReceipt {
use crate::film::{FilmReceipt, FrameHash};
let frames: Vec<FrameHash> = (0..5u64).map(|i| FrameHash { index: i, hash: [i as u8 + 1; 32] }).collect();
FilmReceipt::build(&key(9), "did:web:pin#film", [0xab; 32], 2, frames, 5, 8_080, Some([7; 32]))
}
#[cfg(feature = "haptic")]
fn haptic() -> crate::haptic::HapticReceipt {
use crate::haptic::{ChannelHash, HapticReceipt};
let ch: Vec<ChannelHash> = (0..2u64).map(|i| ChannelHash { index: i, hash: [i as u8 + 3; 32] }).collect();
HapticReceipt::build(&key(9), "did:web:pin#haptic", [0x3c; 32], 0, ch, 960, 555, None)
}
#[cfg(feature = "score")]
fn score() -> crate::score::ScoreReceipt {
use crate::score::{ScoreReceipt, SegmentHash};
let seg: Vec<SegmentHash> = (0..4u64).map(|i| SegmentHash { index: i, hash: [i as u8 + 9; 32] }).collect();
ScoreReceipt::build(&key(9), "did:web:pin#score", [0x2e; 32], 5, seg, 48_000, 2_468, Some([1; 32]))
}
#[cfg(feature = "splat4d")]
fn splat4d() -> crate::splat4d::Splat4dReceipt {
use crate::splat4d::{FrameHash, Splat4dReceipt};
let fr: Vec<FrameHash> = (0..3u64).map(|i| FrameHash { index: i, hash: [i as u8 + 0x40; 32] }).collect();
Splat4dReceipt::build(&key(9), "did:web:pin#splat4d", [0x4d; 32], 0, fr, 3_000, 9_999, None)
}
#[cfg(feature = "ternary_receipt")]
fn ternary() -> crate::ternary_receipt::TernaryReceipt {
use crate::ternary::encode_residual;
let stream: Vec<i64> = (0..256).map(|i| ((i * 37) % 1400) as i64 - 700).collect();
let stack = encode_residual(&stream, 8, 2, 4, 3);
crate::ternary_receipt::TernaryReceipt::seal(&key(1), "did:key:pin", &stack, 4_200, Some([0x66; 32]))
}
#[cfg(feature = "provenance")]
fn provenance_steps() -> Vec<crate::provenance::ProvStep> {
use crate::provenance::ProvStep;
let raw = [0x10; 32];
let png = [0x20; 32];
let jpeg = [0x30; 32];
vec![
ProvStep { op: "capture".into(), input_hash: [0u8; 32], output_hash: raw, work: 12_000_000, joules_micro: 0, energy_provenance: None },
ProvStep { op: "encode.png".into(), input_hash: raw, output_hash: png, work: 12_000_000, joules_micro: 4_200, energy_provenance: None },
ProvStep { op: "encode.jpeg".into(), input_hash: png, output_hash: jpeg, work: 12_000_000, joules_micro: 90, energy_provenance: None },
]
}
#[cfg(feature = "provenance")]
fn provenance(steps: Vec<crate::provenance::ProvStep>) -> crate::provenance::AssetReceipt {
crate::provenance::AssetReceipt::build(&key(9), "did:web:pin#prov", [0x30; 32], "image", "wai.image.jpeg", 0, steps, None)
}
// ---------------------------------------------------------------------------
// The legacy pins.
// ---------------------------------------------------------------------------
#[cfg(feature = "world")]
#[test]
fn world_session_legacy_bytes() {
let r = world();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "e9fad41501e4d009b436e4f9830fd4f72fd29af8dbb333a6cba919ad6d2a0e72");
assert_eq!(json_hash(&r.to_json()), "f36e2358488181caac445253cb9c4df138cf174ce52f725ed550ed59d5178d55");
}
#[cfg(feature = "audio_scene")]
#[test]
fn audio_scene_legacy_bytes() {
let r = audio_scene();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "0b13c8d93ea2207461af14e5589402d389837839383cfdbad09081a21dd21ab5");
}
#[cfg(feature = "film")]
#[test]
fn film_legacy_bytes() {
let r = film();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "e92507a9c11b0b4d2a7f3b940d256fb8a7d22f49705991af358911401f4ebf62");
assert_eq!(json_hash(&r.to_json()), "58516902783835bb7f36b175bbb1f37589971d3afa402864f26025039c74a030");
}
#[cfg(feature = "haptic")]
#[test]
fn haptic_legacy_bytes() {
let r = haptic();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "f06b8e9f5592be27fd953ce168b6d42ef50aa6bc65ee5fb5900543b9b24ccfec");
assert_eq!(json_hash(&r.to_json()), "20a6aa26ae7cb62fedbe5064e329fedcf7aa5a60a3829a530a5fd0287824c623");
}
#[cfg(feature = "score")]
#[test]
fn score_legacy_bytes() {
let r = score();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "61069b0b9e2d341b855a5230c681ceae9358492e4e5f9c883407dbea206f11e9");
assert_eq!(json_hash(&r.to_json()), "3fd5d6ff397d646efee0d625801706b4c90753973a80d0f494ea5f3c18b3e26d");
}
#[cfg(feature = "splat4d")]
#[test]
fn splat4d_legacy_bytes() {
let r = splat4d();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "fa9fb6e12674b6b9e5e662fa51cb99f98df5d2e57046e1436239a926ebbf4655");
assert_eq!(json_hash(&r.to_json()), "1e2a9d497299e2f2d41eca827d845f568376a30bcbbcf0fc5001a8f9e8a4360a");
}
#[cfg(feature = "ternary_receipt")]
#[test]
fn ternary_legacy_bytes() {
let r = ternary();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "3e4d15dc85b6a223cbd7d409161bfaeeb660e625736ffc0aa86f54db8aa6bd41");
assert_eq!(json_hash(&r.to_json()), "78112ecd5b127992955c1247e17ea3b8f93a8e73c76df4f79de9395c8769b734");
}
#[cfg(feature = "provenance")]
#[test]
fn provenance_legacy_bytes() {
let r = provenance(provenance_steps());
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "d22c9b26323f3e529d13290c66c8af3cb1e7c4c39debc41650190acfab1340c5");
assert_eq!(json_hash(&r.to_json()), "70c753ccf508dc652504147dee940131cfbed02e782c194d7e34174c00048c8e");
}
// ---------------------------------------------------------------------------
// The optional label, exercised identically on every family.
// ---------------------------------------------------------------------------
mod labels {
use super::*;
use crate::energy_class::{
label_from_json, AcquisitionError, CounterFiltering, EnergyProvenance, Uncertainty,
};
/// Uniform access to the families' label machinery, for one generic check.
pub(super) trait Family: Clone + std::fmt::Debug {
fn ok(&self) -> bool;
fn id(&self) -> [u8; 32];
fn label(&mut self) -> &mut Option<EnergyProvenance>;
fn figure(&mut self) -> &mut u64;
fn relabel(self, k: &SigningKey, c: EnergyProvenance) -> Result<Self, AcquisitionError>;
fn resign(&mut self, k: &SigningKey);
fn json(&self) -> Option<String>;
}
macro_rules! family {
($t:ty, json) => {
family!(@impl $t, |r: &$t| Some(r.to_json()));
};
($t:ty, nojson) => {
family!(@impl $t, |_r: &$t| None);
};
(@impl $t:ty, $json:expr) => {
impl Family for $t {
fn ok(&self) -> bool { self.verify() }
fn id(&self) -> [u8; 32] { self.receipt_hash() }
fn label(&mut self) -> &mut Option<EnergyProvenance> { &mut self.energy_provenance }
fn figure(&mut self) -> &mut u64 { &mut self.joules_micro }
fn relabel(self, k: &SigningKey, c: EnergyProvenance) -> Result<Self, AcquisitionError> {
self.with_energy_class(k, c)
}
fn resign(&mut self, k: &SigningKey) { self.resign_for_test(k) }
fn json(&self) -> Option<String> { ($json)(self) }
}
};
}
#[cfg(feature = "world")]
family!(crate::world::receipt::SessionReceipt, json);
#[cfg(feature = "audio_scene")]
family!(crate::audio_scene::SceneReceipt, nojson);
#[cfg(feature = "film")]
family!(crate::film::FilmReceipt, json);
#[cfg(feature = "haptic")]
family!(crate::haptic::HapticReceipt, json);
#[cfg(feature = "score")]
family!(crate::score::ScoreReceipt, json);
#[cfg(feature = "splat4d")]
family!(crate::splat4d::Splat4dReceipt, json);
#[cfg(feature = "ternary_receipt")]
family!(crate::ternary_receipt::TernaryReceipt, json);
pub(super) fn chip(ppm: u32) -> EnergyProvenance {
EnergyProvenance::OnChipCounter {
uncertainty: Uncertainty { relative_ppm: ppm, window_us: 2_977_104 },
filtering: CounterFiltering::Unknown,
}
}
/// Everything the label must do, on one family's pinned legacy receipt
/// sealed by `signer`.
pub(super) fn exercise<R: Family>(legacy: R, signer: &SigningKey) {
assert!(legacy.ok());
let legacy_id = legacy.id();
// Labelling re-signs, and the labelled receipt is a different receipt.
let l = legacy.clone().relabel(signer, chip(15_403)).expect("a valid label");
assert!(l.ok(), "a labelled receipt verifies");
assert_ne!(l.id(), legacy_id);
// The label is inside the signature: it cannot be stripped, added,
// restated or promoted in transit.
let mut stripped = l.clone();
*stripped.label() = None;
assert!(!stripped.ok(), "stripping the label breaks the signature");
let mut added = legacy.clone();
*added.label() = Some(chip(15_403));
assert!(!added.ok(), "adding a label without the signer breaks the signature");
let mut restated = l.clone();
*restated.label() = Some(chip(1));
assert!(!restated.ok(), "restating the declared uncertainty breaks the signature");
let mut promoted = l.clone();
*promoted.label() = Some(
EnergyProvenance::calibrated_instrument(Uncertainty { relative_ppm: 15_403, window_us: 2_977_104 }, "cert")
.unwrap(),
);
assert!(!promoted.ok(), "promoting the class breaks the signature");
// Labelling refuses what verify() would refuse.
#[allow(deprecated)]
let hw = EnergyProvenance::HwShunt;
assert_eq!(legacy.clone().relabel(signer, hw.clone()).unwrap_err(), AcquisitionError::LegacyLabel);
assert_eq!(legacy.clone().relabel(&key(8), chip(15_403)).unwrap_err(), AcquisitionError::SignerMismatch);
let mut zero = legacy.clone();
*zero.figure() = 0;
assert_eq!(zero.relabel(signer, chip(15_403)).unwrap_err(), AcquisitionError::NoFigure);
let undeclared = EnergyProvenance::OnChipCounter {
uncertainty: Uncertainty { relative_ppm: 0, window_us: 1 },
filtering: CounterFiltering::Unknown,
};
assert_eq!(legacy.clone().relabel(signer, undeclared.clone()).unwrap_err(), AcquisitionError::NoUncertainty);
// And verify() itself refuses them even when correctly signed.
for bad in [Some(hw), Some(undeclared)] {
let mut r = legacy.clone();
*r.label() = bad;
r.resign(signer);
assert!(!r.ok(), "a correctly signed receipt with a refused label must not verify");
}
let mut zero = l.clone();
*zero.figure() = 0;
zero.resign(signer);
assert!(!zero.ok(), "a class beside a zero figure must not verify");
// An unlabelled zero is unmetered and fine, as it always was.
let mut unmetered = legacy.clone();
*unmetered.figure() = 0;
unmetered.resign(signer);
assert!(unmetered.ok());
// JSON: unlabelled renders no class; labelled renders the class, its
// evidence and the labelled receipt's own hash.
if let (Some(lj), Some(j)) = (legacy.json(), l.json()) {
assert!(!lj.contains("energy_provenance") && !lj.contains("energy_uncertainty"));
let v: serde_json::Value = serde_json::from_str(&j).unwrap();
assert_eq!(label_from_json(v.as_object().unwrap()), Ok(Some(chip(15_403))));
assert_eq!(v["receipt_hash"], serde_json::Value::from(hx(&l.id())));
}
}
#[cfg(feature = "world")]
#[test]
fn world_label() {
exercise(world(), &key(9));
}
#[cfg(feature = "audio_scene")]
#[test]
fn audio_scene_label() {
exercise(audio_scene(), &key(9));
}
#[cfg(feature = "film")]
#[test]
fn film_label() {
exercise(film(), &key(9));
}
#[cfg(feature = "haptic")]
#[test]
fn haptic_label() {
exercise(haptic(), &key(9));
}
#[cfg(feature = "score")]
#[test]
fn score_label() {
exercise(score(), &key(9));
}
#[cfg(feature = "splat4d")]
#[test]
fn splat4d_label() {
exercise(splat4d(), &key(9));
}
#[cfg(feature = "ternary_receipt")]
#[test]
fn ternary_label() {
exercise(ternary(), &key(1));
// The parser keeps the label, and refuses a malformed one rather than
// dropping it.
use crate::ternary_receipt::TernaryReceipt;
let l = ternary().with_energy_class(&key(1), chip(15_403)).unwrap();
let back = TernaryReceipt::from_json(&l.to_json()).expect("parse");
assert_eq!(back, l);
assert!(back.verify());
let broken = l.to_json().replace("\"energy_provenance\":\"OnChipCounter\",", "");
assert!(TernaryReceipt::from_json(&broken).is_none(), "evidence without a class is refused");
let legacy = TernaryReceipt::from_json(&ternary().to_json()).unwrap();
assert_eq!(legacy.energy_provenance, None);
assert!(legacy.verify());
}
#[cfg(feature = "provenance")]
#[test]
fn provenance_step_labels() {
use crate::provenance::AssetReceipt;
let legacy = provenance(provenance_steps());
// Label only the measured encode step; the capture step stays unmetered.
let mut steps = provenance_steps();
steps[1] = steps[1].clone().with_energy_class(chip(15_403)).unwrap();
let l = provenance(steps.clone());
assert!(l.verify());
assert_ne!(l.receipt_hash(), legacy.receipt_hash());
assert_ne!(l.steps[1].content_hash(), legacy.steps[1].content_hash(), "the label is in the leaf");
assert_eq!(l.steps[2].content_hash(), legacy.steps[2].content_hash(), "an unlabelled step keeps its leaf");
// Stripping, adding or restating a step label breaks the receipt.
let mut stripped = l.clone();
stripped.steps[1].energy_provenance = None;
assert!(!stripped.verify());
let mut added = legacy.clone();
added.steps[2].energy_provenance = Some(chip(1));
assert!(!added.verify());
let mut restated = l.clone();
restated.steps[1].energy_provenance = Some(chip(1));
assert!(!restated.verify());
// A step label is refused beside a zero figure or as legacy HwShunt,
// both at labelling and in verify() when correctly signed.
assert_eq!(steps[0].clone().with_energy_class(chip(15_403)).unwrap_err(), AcquisitionError::NoFigure);
#[allow(deprecated)]
let hw = EnergyProvenance::HwShunt;
assert_eq!(steps[2].clone().with_energy_class(hw.clone()).unwrap_err(), AcquisitionError::LegacyLabel);
let mut bad = l.clone();
bad.steps[0].energy_provenance = Some(chip(15_403));
bad.merkle_root = crate::world::receipt::merkle_root(
&bad.steps.iter().map(|s| s.content_hash()).collect::<Vec<_>>(),
);
bad.resign_for_test(&key(9));
assert!(!bad.verify(), "a labelled zero-figure step must not verify");
let mut bad = l.clone();
bad.steps[2].energy_provenance = Some(hw);
bad.merkle_root = crate::world::receipt::merkle_root(
&bad.steps.iter().map(|s| s.content_hash()).collect::<Vec<_>>(),
);
bad.resign_for_test(&key(9));
assert!(!bad.verify(), "a legacy HwShunt step label must not verify");
// JSON round trip keeps the step label; a legacy receipt parses unlabelled.
let back = AssetReceipt::from_json(&l.to_json()).expect("parse");
assert_eq!(back, l);
assert!(back.verify());
let legacy_back = AssetReceipt::from_json(&legacy.to_json()).unwrap();
assert!(legacy_back.steps.iter().all(|s| s.energy_provenance.is_none()));
assert!(legacy_back.verify());
}
}
// ---------------------------------------------------------------------------
// Pins for the labelled forms (energy-measurement §2.8).
//
// The legacy pins above hold unlabelled receipts to their pre-label bytes.
// These hold one labelled receipt of every family to the bytes the label
// gave it when it was introduced, so a later change to a labelled encoding —
// a byte leaking into `write_signed` or `write_label`, or a changed domain —
// fails here instead of re-signing quietly. The hex was produced by the code
// as the labels were first introduced and must never be edited to make a
// test pass.
// ---------------------------------------------------------------------------
mod labelled_pins {
use super::*;
use super::labels::chip;
#[cfg(feature = "world")]
#[test]
fn world_labelled_bytes() {
let r = world().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "32b7ef840b7140c369ecdd5d04c0b9ed2f361980aacfa072c0342d3ad4eb1057");
assert_eq!(json_hash(&r.to_json()), "6c8f53541d7f8a896a8a0e5e2873c076c5afab36c914a95d9e4bf3211b042855");
}
#[cfg(feature = "audio_scene")]
#[test]
fn audio_scene_labelled_bytes() {
let r = audio_scene().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "2fe5b5771dfd818a050c020fe365522f5ac5bfbbd133191e0663c25ab3b1533e");
}
#[cfg(feature = "film")]
#[test]
fn film_labelled_bytes() {
let r = film().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "9bd844ee24ea3903a273a79e509edaae3eef830509edb05a586482581d003e06");
assert_eq!(json_hash(&r.to_json()), "d8644a2f82e407b14d1ff57a2f12b733f63e931c62c00b9526880d2af55e329e");
}
#[cfg(feature = "haptic")]
#[test]
fn haptic_labelled_bytes() {
let r = haptic().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "e17f760cd90364feb9ac47e121a783c85035253c29bf640406c66cfc8cf3e487");
assert_eq!(json_hash(&r.to_json()), "1fb81b2976b6e49a568d63a0a60a872946c29b6751de88f30b12a3fa99dfaa85");
}
#[cfg(feature = "score")]
#[test]
fn score_labelled_bytes() {
let r = score().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "0e795647d7ef74bf400fee962e6bcf57292116e0a084da41a1c153de777a0bee");
assert_eq!(json_hash(&r.to_json()), "856823c9874b10bb5d78c01b576318bc0cd44e1f2112fbcd56f72204db38b544");
}
#[cfg(feature = "splat4d")]
#[test]
fn splat4d_labelled_bytes() {
let r = splat4d().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "410e26fdf5c60b8bba50f39d318a6e6e8c122f8a3807bb65f90dcf2a92c13a76");
assert_eq!(json_hash(&r.to_json()), "5efa236fa7b818685fda50ef7e52c9037c48c76b32eeb0fbfc092888a255156d");
}
#[cfg(feature = "ternary_receipt")]
#[test]
fn ternary_labelled_bytes() {
let r = ternary().with_energy_class(&key(1), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "3eaf95f6d3e7cbc2505f6bacfadac60f2cf7451041cf36c1c7307c1345120c3e");
assert_eq!(json_hash(&r.to_json()), "321f0c9d739ce6fd6e4bf6e32d663ef3e14f25339ad75799fce9f17159052413");
}
#[cfg(feature = "provenance")]
#[test]
fn provenance_labelled_bytes() {
let mut steps = provenance_steps();
steps[1] = steps[1].clone().with_energy_class(chip(15_403)).unwrap();
let r = provenance(steps);
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "db3979573fc3ff07a1c2d82256268c862d6a2d1737ea9636c876155ef9546ae1");
assert_eq!(json_hash(&r.to_json()), "d1a52b06b30dece511e701a86b86eb176b5fa414ae2084520a35a1e004fdcee0");
assert_eq!(hx(&r.steps[1].content_hash()), "8b94897f5b73b5b8e62b8328ee416368e8d087c65ee661fc188e4bcb5b97da42");
}
}