Skip to main content

WAI Extension: OER Receipts (outcome and energy)

Mirrored from the canonical text at commit 117bad22 ().

Status: Draft. Ingests OER/2 draft 0.1, with the departures in §9. An OER receipt records what one unit of work was, what it cost, how that cost is graded, and what it rests on — canonicalised, content-addressed, hash-chained and Ed25519-signed, so a third party can check every figure without trusting the producer. Conformance is reason-set-equivalence: for every published vector, a verifier reports exactly the reasons the vector declares. Verifiers: the reference, oer-conformance/oer2_verify.py, and wai-rs (crate::oer, the oer feature); corpus: oer-conformance/. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

1. Scope and model

energy-measurement.md fixes how a joule figure is acquired — the acquisition class. It does not say how much weight the figure can bear. An OER receipt adds that: every figure carries an evidential grade, and the receipt as a whole is canonical, content-addressed and chained, so the grade cannot be quietly upgraded after the fact. An OER/2 receipt carries the grade and no acquisition class; the two are different axes (§2).

A receipt describes one unit of work of a declared kind, one of the strings decision, task, reconstruction, calibration, job, compile, decode, mitigate, rearrange, anchor, or bundle (of other receipts). Receipts form a chain; the chain, the records its decisions relied on, and a ledger identity that ties the chain to a meter are verified together.

Three independent implementations check the same corpus: the reference verifier (oer-conformance/oer2_verify.py), the portable C kernel that produces receipts (oer-conformance/settle/), and wai-rs. They share no canonicalisation code.

2. The evidential grade (REQUIRED)

provenance.how MUST be one of four grades, weakest first:

grademeaning
projectedan estimate awaiting a measurement
simulatedproduced by emulation or a model run, not by the system itself
derivedfollows from measured or published figures by stated arithmetic
measuredread from an instrument, a datasheet, or a published measurement

A figure’s grade is its evidential standing; its acquisition class (energy-measurement.md §2: CalibratedInstrument, OnChipCounter, ModelBased, Estimator, and the legacy HwShunt) is how it was obtained. They are different axes and a receipt SHOULD carry both where it carries an acquisition class at all. An OER/2 receipt has no member for the class, and cannot gain one without becoming non-canonical (§3). Its descriptive members name none. provenance.device is text; the corpus writes hardware, emulation and simulation. energy.method is text too; the corpus writes hardware_meter and model_estimated. In particular, neither hardware nor hardware_meter is HwShunt or either refined hardware class.

energy-measurement.md governs the receipt profiles WAI defines (§1 there), not an OER/2 receipt. No rule of that extension binds an OER receipt or its producers and verifiers: an OER receipt that carries a non-zero energy.joules_total and no class breaks none of them, and that extension’s publication gate (§4 there) and reporting contract (§6 there) do not apply to it either. Its evidential axis is the grade, under this document’s rules.

The two axes correspond in one direction:

acquisition classsupports the grade
CalibratedInstrumentmeasured
OnChipCountermeasured
HwShunt (legacy)measured: a figure sealed before the hardware class was refined, or a refined hardware figure mapped into the three-name vocabulary (below)
ModelBasedderived, by energy-measurement.md’s own definition
Estimatorprojected

A producer MUST NOT grade measured a figure whose acquisition class is Estimator. A receipt carries no acquisition-class member (§3), so no reason in §7 checks this; it binds the producer.

provenance.weakest_input names the lowest-graded input the figure rests on, so a measured total built on a projected constant is visible as such.

The energy claim class of jwp-receipts.md §2.4.5 carries both axes for a delivery receipt’s figure: the acquisition class with its evidence, and a grade, which it rejects when it ranks above what the class supports under the correspondence above. It may name the OER receipt that records the figure. A verifier holding that receipt then checks that it verifies under this document, is about the same object (its subject.output is the object’s jwp-receipts §2.1 content hash, as the claim writes it), and states the same grade and, to the microjoule, the same figure, read as §4 reads a quantity. That is where the producer rule above becomes checkable, against the class the claim’s signer states: a verifier that checks an accepted energy claim against the OER receipt it names holds a receipt graded no higher than that class supports. It changes nothing about the OER receipt itself, which still carries no class.

3. Receipt members

Members are fixed per path, and only these members may appear:

pathmembers, in canonical order
(root)oer, receipt_id, kind, capability, intent, subject, work, energy, provenance, settlement, floor, efficiency, bundle, chain, hash_alg, sig_alg
subjectinputs, output
workstatus, outcome, latency_s, proficiency
energyjoules_total, boundary, breakdown, method, temperature_k
energy.breakdowncompute_j, movement_j, idle_alloc_j, shared_alloc_j, upstream_j
provenancehow, device, weakest_input, hardware_profile, corpus
provenance.weakest_inputhow, input
settlementquestion_key, served_from_record, record_id, record_hash, reuse_count, derivation_j, valid_until
floorbits_committed, bits_delivered, floor_j, decades_above_floor
efficiencycompleted_units, wasted_units, joules_per_completed, waste_fraction, value_per_joule
bundlestages, provenance_root
bundle.stages[]kind, receipt_hash, joules
chainprev_hash, receipt_hash, signer, signature, anchor
chain.anchorpath, delivered_at, root

An object at any other path orders its members by key. A member not listed for its path makes the receipt non-canonical.

oer is the profile version, a string ("2.0"); its major version is the part before the first .. A verifier MUST reject a major version other than 2, and an oer that is not a string, before any check but whether the receipt is an object (§7) — a version written as a JSON number would have a major that depends on how each parser rounds it. energy.boundary MUST be stated: a joule figure without its measurement boundary cannot be compared with any other.

Capability. capability names what the receipt is about, in the receipt’s own namespace: a decision receipt names its decision class under oer.decision.* (oer.decision.ev_charging_authorization), and bundles and anchors use oer.bundle.* and oer.anchor.*. These are not WAI payload capabilities, which name formats a sink reconstructs (SPEC.md §5), and a receipt MUST NOT name them in the wai. namespace.

Status, escalation and abstention. work.status is 1 for a unit of work that completed and -1 for one that was wasted; a bundle’s efficiency counts them as completed_units and wasted_units. Two outcomes are easy to confuse:

A producer MUST receipt a decision it did not earn — one closed by a stand-in, a default or a guess — as an abstention, never as a verdict. An unsettled answer is a first-class result. An abstention is completed work (status 1), so efficiency.joules_per_completed counts it; a figure per settled decision MUST NOT.

4. Canonical form

The canonical body is the receipt without chain and receipt_id, with every object’s members in the order of §3, serialised as JSON with , and : as separators and no other whitespace, and with non-ASCII characters written unescaped (UTF-8). Within strings, " \ and the controls \b \f \n \r \t take their short escapes and every other character below U+0020 is written \u00XX in lower-case hex.

Numbers. A receipt body carries two kinds of number and no others. Integers are written in plain decimal, with no fraction or exponent, and lie within ±(2⁵³ − 1): the I-JSON range (RFC 7493 §2.2), which any reader of IEEE 754 binary64 or wider reads exactly. Every non-integer quantity — every joule figure, temperature and ratio — is a decimal string ("14.960786694120"). A number with a fraction or an exponent (1.0, 1e2), negative zero (-0), and an integer outside that range MUST NOT appear in a body, and a verifier MUST report malformed for one. The canonical form therefore never prints a float. That matters because parsers disagree on exactly these shapes — one reads -0 and 2⁶⁴ as exact integers, another as floats — and two verifiers that hashed them would compute two receipt ids for one receipt. The rule is about how a number is written: a verifier whose parser reads 1.0 and 1 as the same value needs a reader that sees the text. Records (§6) are not receipt bodies and are unaffected.

The document. A vector document is read before any receipt in it is verified, and one that cannot be read under this profile is refused whole:

The corpus holds one document for each in oer-conformance/unreadable/, among them a repeated figure inside a receipt and nesting far past any parser’s limit. A vector in edge_vectors.json holds the extremes a document may carry — the largest binary64 as a float and as an integer, a float just below the midpoint, a surrogate pair, nesting exactly 64 deep — so a verifier that refuses too much fails too.

Decimal strings. A decimal string is JSON’s own number grammar (RFC 8259 §6) carried as a string, in ASCII: an optional - and no other sign, no whitespace, no leading zero, digits on both sides of any ., and an exponent of one to three digits. The significand — every digit before the exponent, leading zeros included — has at most 34 digits, so every decimal string is exact in IEEE 754 decimal128. Where a check reads a quantity it accepts a decimal string or an integer in the range above, and nothing else. A receipt member that fails is malformed; a ledger figure that fails makes the identity fail.

The canonical form is pinned independently of the vectors. The reference generator uses the reference verifier’s canonicaliser, so a defect in the field order would be baked into every receipt id and then confirmed by the same code — reversing the declared order and regenerating still yields a passing corpus. Every verifier therefore also checks a canonical fixture: an input whose members are given in the wrong order at two levels, against literal expected bytes and their sha-256 id (sha256:364718c7…84ed30). The cross-check against the C kernel, which shares no code with either verifier, proves the same property from the other side.

A second fixture pins the stage list that bundle.provenance_root addresses (§6), which is serialised by the same rules as the body. Every stage list in the corpus is ASCII, and on ASCII text an escaped and an unescaped serialisation agree, so no vector can tell them apart. The fixture’s stages carry a two-byte and a four-byte character, with their members out of order, against literal bytes and their sha-256 id (sha256:c1caa0ce…f2638b).

5. Identity, chain and signature

H is named by hash_alg: sha-256 writes sha256:<hex>, blake3 writes blake3:<hex>. A verifier that lacks the named algorithm MUST report missing_capability — never a mismatch, which would blame the producer for the verifier’s gap. sig_alg MUST be ed25519.

6. Composition checks

Every check is evaluated in exact decimal arithmetic: no sum, product or comparison is rounded, so no verdict is decided by rounding noise and two conforming verifiers cannot disagree at a tolerance’s edge. “Within t” means |a − b| ≤ t — a difference exactly at the tolerance passes, and a miss counts the same on either side — and every relative tolerance scales by the magnitude of the figure it names, so a negative figure is judged exactly as a positive one.

7. Reasons

A verifier reports every reason a receipt fails, from this vocabulary, and nothing else, and a receipt too broken to check is a reason, never a verifier error. Some checks stop at the first failure and report it alone, discarding anything found before it; they run in this order, so a receipt with more than one names the same reason everywhere:

  1. the receipt is not a JSON object — malformed;
  2. oer is absent, not a string, or of another major — unknown_major (§3);
  3. a member not listed for its path, chain included — canonical_failure;
  4. the body carries a number §4 forbids — malformed;
  5. hash_alg names an algorithm the verifier lacks — missing_capability;
  6. any other member a check needs is absent or mistyped, hash_alg and bundle.stages (not a list) included — malformed.

Everything else accumulates. Some members are judged on their value, so a wrong one is a named reason rather than malformed:

reasonraised when
unknown_majoroer is absent, not a string, or of a major other than 2 (reported alone)
unknown_kindkind is not one of §1’s kinds
canonical_failurea member not listed for its path (reported alone)
malformedthe receipt is not an object, a member a check needs is absent or mistyped (a quantity that is not a §4 decimal string or integer included), or the body carries a number §4 forbids (reported alone)
missing_capabilitythe verifier lacks hash_alg (reported alone), or sig_alg is not ed25519
receipt_id_mismatchreceipt_id ≠ H(body)
receipt_hash_mismatchchain.receipt_hash ≠ H(prev_hash ‖ body)
chain_breakprev_hash ≠ the previous receipt’s receipt_hash, or either is not a string
sum_mismatchbreakdown does not sum to joules_total
boundary_missingenergy.boundary is absent, empty or not a string
floor_mismatchfloor_j is not the Landauer floor within 1%
grade_unknownprovenance.how is not a §2 grade
abstention_settledan abstain receipt carries an answer or a record (§3)
bundle_root_mismatchprovenance_root does not address the stages
bundle_sum_mismatchstages do not sum to joules_total
efficiency_missinga bundle without efficiency.completed_units
signer_unknownchain.signer is not a string naming a published key that is a string
signature_invalidthe signature does not verify, or the published key fails §5’s strict decoding
record_mismatcha cited record does not rehash to record_hash, or cannot be rehashed (§6)
ledger_mismatchthe ledger identity does not hold

Reasons for receipts that share a receipt_id accumulate; a repeated id MUST NOT replace, and so hide, the reasons of an earlier receipt.

8. Conformance

reason-set-equivalence. For every vector in oer-conformance/, a conforming verifier reports exactly the declared set of reasons — the valid vectors none, each tampered vector exactly its own — and both fixtures (§4) hold.

filevectorsproduced by
vectors.json9the reference generator: a simulated corridor day, two valid vectors and seven tampered twins
firmware_vectors.json1the portable C kernel (settle/): 400 receipts, independent SHA-256 and Ed25519
edge_vectors.json99generate_edge_vectors.py: one case for each reason the upstream nine never exercise, each number shape and decimal-string defect §4 forbids and each limit from the accepting side, each bound and tolerance edge, each step of the order of the reasons reported alone, each member judged on its value, each claim an abstention cannot make, each key a signature cannot rest on, and the extremes a document may hold
unreadable/11 documentsone for each way §4 refuses a document whole; every verifier must refuse all of them

The upstream vectors cover eight reason names; the edge vectors pin the rest, including malformed, canonical_failure and abstention_settled, the repeated-id rule, and every edge a verifier could draw in the wrong place: each integer bound from both sides; each decimal-string limit from both sides; each of the four tolerances — sum, floor, bundle, ledger — exactly at its edge and just past it, on both sides, for negative and small figures, and with the figure that sets its base; for each of the four, a verdict that flips if it is computed to 34 digits (and for the sum, to 28); each step of the order of the reasons reported alone; and keys that are small-order or non-canonical encodings, each under a signature that a lenient verifier accepts. Their expectations are written by hand from this document — none is copied from a verifier’s output, which would only prove the code agrees with itself.

Two rules bind the producer and no reason in §7 can enforce them: the capability namespace, and receipting an unearned decision as an abstention (§3). A verifier cannot tell from a receipt’s bytes whether a verdict was earned — only that an abstention claims none.

wai_oer_vectors verify oer-conformance              # wai-rs, --features oer: every file and unreadable/
python3 oer-conformance/oer2_verify.py <file>        # reference verifier, per file
python3 oer-conformance/oer2_verify.py --unreadable oer-conformance/unreadable
python3 oer-conformance/fuzz_differential.py         # the two verifiers against each other
make -C oer-conformance/settle && oer-conformance/settle/settle_host out.json 400   # the C kernel

The vectors pin reason-set-equivalence one case at a time. Between them, fuzz_differential.py mutates corpus documents and requires the reference and wai-rs to report the same reasons, or the same refusal, for every one, and neither to crash.

CI runs wai_oer_vectors — every vector file and every document in unreadable/ — with the other cargo-class corpora, rebuilds the C kernel, and checks its output byte-for-byte against the pinned firmware_vectors.json and through wai_oer_vectors.

9. Departures from draft 0.1

This extension settles the points the draft left open. Each changes what a conforming producer writes or what a verifier accepts; none changes how a receipt is chained or signed.