# WAI Extension: Sink Energy

> Status: Draft. Registers the record `wai.sink.energy`: what decoding cost at the sink,
> per stream and per segment, with the acquisition class of
> [energy-measurement](energy-measurement.md) and its uncertainty, and how one viewing
> session's energy splits into its origin, its relay, the sink's decode and its display.
> Three signed records: a sink's **decode table** (§4), a measured figure per registered
> capability, decode path and sink energy profile, produced by a pinned procedure and
> committed with its raw readings; a **segment receipt** (§5), the decode energy of one
> segment of one stream, measured, taken from the sink's own table, estimated, or stated
> as unmetered; and a **session receipt** (§6), the per-hop split over the four hop roles a
> stream log uses (origin, relay, sink decode, display), each hop a figure with its own
> class, the unmetered marker where the hop took place unmeasured, or absent where the sink
> states nothing about it. The counters, scopes, idle baselines
> and windows are energy-measurement §9's sink profiles. No new transport, no change to
> the wire format or to SPEC §4's algorithm. The reference implementation is the
> `sink_energy` module of `wai-rs` (feature `sink_energy`), whose readers are in
> `sink_meter` and whose table procedure is the binary `wai_sink_table`; the corpus is
> [`live-conformance/sink-energy/`](../live-conformance/sink-energy/), with the measured
> tables of the reference sinks under its `tables/`, and its second implementation is
> `live-conformance/sink_energy_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision: RFC 8032 (Ed25519); RFC 8785 (JSON Canonicalization
> Scheme); RFC 8259; FIPS 180-4 (SHA-256); JCGM 100:2008 (GUM) §4.3.7 and F.2.2.1;
> Commission Delegated Regulation (EU) 2019/2013, for the on-mode power a display's
> energy label states; arXiv 2402.09001, 2603.18305 and 2511.00707, for §4.6's
> outside measurements of decode energy. A section number in this text is this
> extension's unless it names another document.

## 1. Scope

In scope: the three records, their signatures and identities (§3); the decode table, the
procedure that produces it, how each row's status and figure follow from its rounds, and
outside measurements of decode energy (§4); the segment receipt and its four methods
(§5); the session receipt and its per-hop split (§6); verification (§7); the reasons
(§8); and what a conforming implementation publishes (§9).

Out of scope: a transport; how a counter works, which energy-measurement §9 states per
platform; the energy of networks, which no sink can read and this extension never
attributes to a byte; and what a party does with a figure.

Relationship to other documents:

- **[energy-measurement](energy-measurement.md).** Every figure carries a class of its
  §2, encoded as its §2.7 writes the JSON members. A measured figure follows its §3, §3.1,
  §4 and §6.2. The profiles are its §9. A record here carries its report's values inline,
  under the figure's own signature (its §6).
- **[browser-sink](browser-sink.md) §6.** A page's `energy` entry for
  `"<capability> <route>"` **MAY** be a row of its sink's decode table whose status is
  `figure`: `joules_micro` the row's, `decodes` 1, `window_us`, `class`, `relative_ppm`
  and `filtering` the row's class and evidence, `coverage` and `fabrics` the row's.
- **[jwp-receipts](jwp-receipts.md) §2.1.** A segment receipt names what it decoded by
  that section's content hash.
- **[live-metering](live-metering.md) §3.** A relay's hop entry is read from a hop's
  per-hop session record, `WMH1`.
- **stream-termination §2, §3.5 and §4.6.** The hop roles, the hop entries' figure, marker
  and absence, and each role's figure for one segment of a stream log. This extension's
  hops are that table (§6.2), not a second one, and a segment receipt here is the
  sink-decode hop's figure for its segment.
- **[staged-measurement](staged-measurement.md) §8.** A decode figure never includes a
  display (its paragraph 8.4). The display is a hop of its own (§6.3), always an estimate.

Relationship to the core spec: `wai.sink.energy` is a **record** ([SPEC](../SPEC.md) §5).
It names no payload format and is not dispatchable.

## 2. Terms

- **Sink**: the party that decodes and presents. **Reference sink**: a machine the
  measured tables of the corpus were taken on.
- **Path**: `software` (the decode runs on general-purpose processors) or `hardware` (a
  fixed-function decoder of the platform does it).
- **Profile**: a sink energy profile of energy-measurement §9: `macos-task`,
  `macos-gpu`, `linux-rapl`, `linux-nvml`, and the estimators `estimator-cpu-time/1` and
  `estimator-display/1`.
- **Unit**: what a figure is per: `image` (one picture of `width` × `height`), `frame`
  (one video frame of `width` × `height`), `content-ms` (one millisecond of audio content)
  or `byte` (one byte of decoded text). A row or a segment states how many units its
  figure covers (`units`).
- **Class**, **evidence**, **unmetered marker**, **bracket**, **liveness**, **ratio**,
  **verdict**: as in energy-measurement §2–§4 and §6.2. The class is written as
  jwp-receipts §2.4.5 writes it: `joules_micro`, `energy_provenance`,
  `energy_uncertainty` (`{ "relative_ppm", "window_us" }`), `energy_counter_filtering`
  and `energy_calibration_ref`, each `null` where the class carries none; unmetered is
  `joules_micro` 0 with all four others `null`.
- **Bracket summary**: `{ "n", "mean_uw", "sd_uw", "live_intervals", "longest_live_run" }`,
  energy-measurement §6.2's integers for one bracket.
- Hexadecimal is lowercase. Every number in a record is an integer from 0 to 2⁵³ − 1.
  `⌊x⌋` is the floor; every division below is exact integer arithmetic.

## 3. The records, their signatures and identities

Each record is one JSON object (RFC 8259). Its **body** is the object without its `sig`
member. A record is signed with Ed25519 (RFC 8032) by the key its `signer` member names,
64 hexadecimal digits, over

```
domain ‖ JCS(body)
```

where JCS is RFC 8785's canonical form, and `sig` is the 64-byte signature as 128
hexadecimal digits. A signature is verified as WAI verifies every signature (SPEC §7,
*Signatures*). The record's **identity** is

```
SHA-256(id-domain ‖ JCS(body) ‖ signature bytes)
```

| record | first member | domain | id-domain |
|---|---|---|---|
| decode table (§4) | `"wai_sink_table": 1` | `wai:sink-table\x01` | `wai:sink-table-id\x01` |
| segment receipt (§5) | `"wai_sink_segment": 1` | `wai:sink-segment\x01` | `wai:sink-segment-id\x01` |
| session receipt (§6) | `"wai_sink_session": 1` | `wai:sink-session\x01` | `wai:sink-session-id\x01` |

The first member names the record and its version; every member the tables below list is
required, and a record with a member missing, a member they do not list, or a value of
another type is refused (`record_malformed`). Terms are compared as exact strings.

## 4. The decode table

A decode table is a sink's measured decode energy: for each registered capability and
decode path, on each profile the sink can read, one figure per completion of a stated work
item, from repeated bracketed measurements, with every round's values in the record and
every raw reading in a file it binds.

### 4.1 The record

| member | type | meaning |
|---|---|---|
| `wai_sink_table` | integer | `1` |
| `signer` | 32 B hex | the key that signs the table |
| `procedure` | term | `wai-sink-table/1` (§4.4) |
| `procedure_sha256` | 32 B hex | SHA-256 of the procedure text committed with the table |
| `readings_sha256` | 32 B hex | SHA-256 of the readings file committed with the table (§4.3) |
| `machine` | object | `{ "os": string, "arch": string, "processor": string, "cores": integer, "memory_gib": integer }` |
| `build` | object | the decoders' build, as jwp-receipts §2.4.8 writes one: `{ "profile": "release" or "debug", "toolchain": string or null, "target": string or null }` |
| `measured` | string | the date the rounds were taken, `YYYY-MM-DD` |
| `rounds` | integer | rounds per row: odd, at least 7 |
| `sample_us` | integer | the sampling interval every bracket was read at, in microseconds |
| `counters` | array | the sink's survey: one `{ "profile": term, "status": term, "detail": string }` per counter it looked for, in ascending order of (`profile`, `detail`); `status` is one of energy-measurement §9.1's five, or `implausible` (§9.3 there) |
| `rows` | array | the rows (§4.2), in strictly ascending order of (`capability`, `path`, `decoder`, `profile`), strings by their bytes and a `null` profile first |

### 4.2 A row

| member | type | meaning |
|---|---|---|
| `capability` | string | a capability the registry lists (`registry/capabilities.json`) |
| `path` | term | `software` or `hardware` |
| `decoder` | string | the decoder: `builtin:<crate>@<version>` as jwp-receipts §2.4.12 writes one, or `platform:<interface>` for a decoder of the platform, `<interface>` 1 to 64 of `a`–`z`, `0`–`9` and `-` |
| `profile` | term or null | the profile read: `macos-task`, `macos-gpu`, `linux-rapl` or `linux-nvml` (energy-measurement §9.2, §9.3), whose minimum windows are 1 s, 1 s, 1 s and 5 s; `null` only in a row `absent` for `no_decoder` |
| `fabrics` | array | the fabrics the profile's reading covers (energy-measurement §9.1), exactly: `["cpu"]` for `macos-task`, `linux-rapl` and `estimator-cpu-time/1`, `["gpu"]` for `macos-gpu` and `linux-nvml`; empty in an absent row |
| `coverage` | term or null | `Partial` (§4.5); `null` in an absent row |
| `unit` | term | `image`, `frame`, `content-ms` or `byte` |
| `units` | integer | units per completion, at least 1 |
| `width`, `height` | integer or null | the decoded picture's size for `image` and `frame`; `null` for the other units |
| `workers` | integer | the burst's workers `N`, a power of two; 0 in an absent row |
| `status` | term | `figure`, `relative`, `unmetered` or `absent` (§4.5) |
| `reason` | term or null | why a row is `unmetered` or `absent` (§4.5, §8); `null` otherwise |
| `rounds` | array | each round's `{ "idle": summary, "busy": summary, "window_us": integer, "completions": integer }`, in the order taken; empty in an absent row |
| `median` | integer or null | the index in `rounds` of the median round (§4.5); `null` unless `figure` or `relative` |
| `range` | array or null | `[least, greatest]` of the rounds' figures; `null` unless `figure` or `relative` |
| `joules_micro`, `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | — | the figure per completion and its class (§2); unmetered unless `figure` or `relative` |

### 4.3 From readings to a round

The readings file is the JCS form (RFC 8785) of a JSON object, and `readings_sha256` is
the SHA-256 of those bytes. The object is `{ "wai_sink_readings": 1, "rows": [ … ] }`, one entry
per row of the table in the table's order, each `{ "capability", "path", "decoder",
"profile", "rounds": [ { "idle": [[d, t], …], "busy": [[d, t], …], "completions": integer } … ] }`.
Each `[d, t]` is one interval of the bracket: `d` the counter's advance over it, in
nanojoules, across its wrap; `t` its length, in microseconds, on a monotonic clock. The
busy bracket's first read is taken as the window opens and its last as it closes, so the
window is the sum of its intervals' `t`, and `completions` is the count of completions
between those two reads.

A round's values follow from its intervals, with `n` the intervals and `pᵢ = ⌊(2000·dᵢ +
tᵢ) / (2·tᵢ)⌋` each interval's power in microwatts:

- `mean_uw = ⌊(2·Σp + n) / (2n)⌋`, the mean rounded to the nearest, a half up;
- `sd_uw` the least integer `s` with `s² · n · (n − 1) ≥ n·Σp² − (Σp)²`, the sample
  standard deviation rounded up (0 when `n` < 2);
- `live_intervals` the intervals with `d > 0`, and `longest_live_run` the longest run of
  consecutive ones: an energy counter advances in an interval when its second read
  exceeds its first (energy-measurement §3.1);
- `window_us` the sum of the busy intervals' `t`, and `completions` as read.

A verifier holding the readings file recomputes every round and requires the table's
values (`table_readings_mismatch`), and requires its SHA-256 to be `readings_sha256`.

### 4.4 The procedure `wai-sink-table/1`

1. **Survey.** For each profile the platform has, read the counter at the sampling
   interval for at least 2 s with nothing of the sink's running, and record its status
   under energy-measurement §9.1. A profile whose counter is not `live` gives no rows
   but `absent` ones. A profile whose own sampling interval is not the table's
   (`linux-nvml`'s is 1 s) is surveyed, and gives no rows in this table.
2. **Work items.** For each capability with a decoder in the sink, a fixed input, written
   by the procedure from fixed seeds or taken from a committed vector, and decoded once
   with its output checked against its reference before any round: a decode whose output
   is wrong is not timed and not metered. A **completion** is one decode of the whole
   item. The procedure text states each item, its digest, its units and its size.
3. **Rounds.** For each round, and in each round for each row in an order rotated by one
   each round: settle for 2 s, so that a processor package's draw falls back from the burst
   before it (read 200 ms after a burst, the reference Linux sink's package still drew several
   times its resting draw); read the **idle bracket** of the row's
   counter over the decode workers parked at their start barrier; release `N` workers
   together as the window opens, and read the **busy bracket** while each decodes the item
   over and over, counting each completion; close the window; stop and join the workers.
   The window opens from rest (energy-measurement §2.3). `N` is the sink's worker count
   rounded down to a power of two (jwp-receipts §2.4.12, `threads`).
4. **Record.** Write every interval to the readings file, every round's values and each
   row's status, figure and class to the table (§4.5), and sign the table.

The procedure text committed with a table states the steps above as the tool ran them:
its command line, the sampling interval and bracket lengths, the rounds, the work items
with their digests, the workers, and the decoders' build.

### 4.5 A row's status and figure

**Absent.** A row is `absent` when there is nothing to measure: `no_decoder` (the sink has
no decoder for the capability on the path; `profile` `null`), `no_work_item` (the sink
decodes it, and the procedure defines no work item for it), `no_hardware_path` (the
platform's decoder for it is not a hardware one on this sink, or its output differed from
the software path's), or `no_counter` (the row's profile is not `live` in the survey). An absent row has no rounds, `workers` 0, and the
unmetered marker; it states what was not measured, never a figure of zero.

**A round's figure.** Every other row has exactly `rounds` rounds. Round `r` is the
bracket `{ idle, busy, window_us, completions, in_flight: workers, opening: from-rest }`
of energy-measurement §6.2. It **gives a figure** `F_r`, with least declaration `p_r`, when
it passes, in order:

1. `too_few_samples`: each bracket has at least 2 intervals;
2. `rail_not_live`: each bracket's rail is live: `sd_uw` above 0, `live_intervals` at
   least 5, `longest_live_run` at least 2, `5 · live_intervals ≥ 4 · n`, and the
   bracket spans at least 300 ms (the idle bracket `n · sample_us`, the busy bracket
   `window_us`);
3. `window_short`: `window_us` is at least the profile's minimum window;
4. `empty_burst`: `completions` is at least 1;
5. `no_marginal`: the busy mean is above the idle mean;
6. `below_attribution_floor`: `M ≥ sd_idle` (energy-measurement §4);
7. `figure_zero`: `F_r`, §6.2's figure, is at least 1;
8. `not_declarable`: `F_r` is at most 2⁵³ − 1 and §6.2's least `p_r` at most 2³² − 1.

**The row.** If any round gives no figure, the row is `unmetered`, and its `reason` is
the reason of the first round in `rounds` that gives none, the first rule it breaks.
Otherwise the rounds are ordered by (`F_r`, `r`) and the **median** is the middle one.
The row's verdict is the median round's (energy-measurement §6.2): clean gives `figure`,
usable for relative comparison gives `relative`, and noisy or unusable gives
`unmetered` with reason `noisy`. A `figure` or `relative` row seals:

- `joules_micro` = `F_median`, and `range` = `[min F_r, max F_r]`;
- `energy_provenance` `OnChipCounter` with the profile's filtering state, or
  `CalibratedInstrument` with its calibration reference where the profile reads an
  instrument;
- `energy_uncertainty.window_us` = the median round's `window_us`, and
  `relative_ppm ≥ max(p_median, q)`, where `q` is the least integer with
  `12 · q² · F_median² ≥ 10¹² · (max F_r − min F_r)²`: the rounds' spread read as a
  rectangular distribution over its range (GUM §4.3.7), relative to the figure.

A row whose `max(p_median, q)` exceeds 2³² − 1 cannot declare it (energy-measurement
§2.7): it is `unmetered`, with reason `not_declarable`, and like every unmetered row it
keeps its rounds and has `median` and `range` `null`.

A `relative` row's figure is for comparing two rows of the same table measured round by
round together; a reader **MUST NOT** present it as an absolute figure
(energy-measurement §4; staged-measurement §8, its paragraph 8.3). An `unmetered` row keeps its rounds,
so the measurement that failed stays auditable.

**Coverage.** A figure is `Whole` only where its profile's scope covers every fabric
the decode runs on. No profile of this revision does: none reads a media engine
(energy-measurement §9.2), and none reads memory. So every row that is not absent, and
every segment that is not unmetered, is `Partial` in this revision, and a `Whole` one is
refused (`table_row_figure`, `segment_method_class`); `Whole` is kept for a profile a
later revision registers.

### 4.6 Outside measurements of decode energy (informative)

The three points below are outside measurements of decode energy. Their figures are not
figures of a WAI decoder or of a reference sink: no record of this extension states them,
and no rule of it reads them. Each is cited by its arXiv identifier and the table it is
read from, with what was measured and against what.

1. **A platform's hardware decoder, against software decoders of the same format.**
   arXiv 2402.09001, Table IV. Over AVC, VP9, HEVC and AV1, each in a random-access and a
   low-delay configuration, a hardware decoder spent from 0.89% to 8.86% of the energy of
   a software decoder of the same format, and from 4.44% to 8.86% of the energy of the
   optimised software decoders with vector instructions enabled. The figures are dynamic
   energy: the energy measured at idle is subtracted. The software decoders ran
   single-threaded on a desktop processor, read by the processor's own energy counter;
   the hardware decoders ran on a single-board computer's video unit, read at its power
   input by an external meter. The point sets two decode paths side by side as two
   machines built them.
2. **Fewer frames, chosen by the encoder.** arXiv 2603.18305, Table V, its row for all
   sequences. Decoding energy was 17.60% lower, encoding energy 17.46% lower and the
   bitrate 3.38% lower, each a Bjøntegaard delta at equal matched PSNR (mPSNR, computed
   against the source), against encoding each sequence at its source's 120 frames per
   second. A trained classifier picks, per sequence and per rate-factor setting (the
   table covers four), the frame rate the encoder codes at, the source's or a lower one;
   where it is lower, the decoder decodes fewer frames than the source has. One software
   encoder and one software decoder of HEVC were measured, by the processor's own energy
   counter, idle subtracted.
3. **Lower resolution or coarser quantisation, chosen by the encoder.** arXiv 2511.00707,
   Table 4, its row ρ = 0.05. Decoding energy was 53.54% lower and encoding energy 51.22%
   lower, for an average loss of 1.68 VMAF points and 3.67 dB PSNR, against the
   configuration chosen with no loss allowed (ρ = 0). Per sequence, the encoder takes, of
   the resolutions and quantisers whose predicted VMAF is within 5% of the best predicted,
   the one of least predicted energy. One software encoder and one software decoder of VVC
   were measured on UHD sources at 60 frames per second, by a software tool reading the
   processor's energy counter and the graphics processor's; the paper does not state
   whether the idle energy is subtracted, nor whether Table 4's savings are the measured
   or the predicted energy of the configurations chosen.

How they bear on this extension:

- Points 2 and 3 are choices of what to encode, and each costs frames or picture quality.
  Point 1 is a choice of decoder for the same stream. None of the three changes what a
  decoder computes from a given stream.
- SPEC §4's *Energy order* already ranks first a classical decode that the sink's platform
  answers supported, smooth and power-efficient. Point 1 measures what a hardware decode
  spent beside a software one; the platform's answer stays a hint that orders, never an
  energy figure or evidence of hardware (SPEC §4).
- A `hardware` row of this revision reads no media engine (§4.5, *Coverage*): its figure
  is the processors' part of a fixed-function decode. It and point 1 measure different
  things, and are not set against each other as like for like.

## 5. The segment receipt

A segment receipt is a sink's statement of what one segment of one stream cost it to
decode: the sink-decode hop's figure for that segment (role 3 of §6.2). A sink signs one
per segment it decoded, in order, each naming the one before it. Where the sink also keeps
a stream log, the log's segment record states the same figure and class for role 3
(stream-termination §4.6), and the receipt here is where its bracket, table row or
estimator inputs are.

### 5.1 The record

| member | type | meaning |
|---|---|---|
| `wai_sink_segment` | integer | `1` |
| `signer` | 32 B hex | the sink's key |
| `session` | 32 B hex | fresh random bytes per viewing session, derived from nothing that identifies a person, a device or an account |
| `stream` | 32 B hex | the stream: for a track of a room, live-metering §2's track digest; otherwise SHA-256 of the stream's address as the sink fetched it, without query or fragment |
| `segment` | integer | the segment's index in this session's reading of the stream, from 0 |
| `prev` | 32 B hex or null | the identity (§3) of this session's receipt for segment `segment − 1` of the stream; `null` exactly when `segment` is 0 |
| `object` | 32 B hex | the content hash (jwp-receipts §2.1) of the segment as decoded |
| `capability`, `path`, `decoder`, `unit`, `units`, `width`, `height` | — | as a table row has them (§4.2), for this segment: `units` is the units the segment decoded |
| `media_us` | integer | the segment's content duration, in microseconds, at least 1 |
| `method` | term | `measured`, `table`, `estimator` or `unmetered` (§5.2) |
| `profile` | term or null | the profile the figure comes from; `null` exactly when `unmetered` |
| `fabrics`, `coverage` | — | as a table row has them; empty and `null` exactly when `unmetered` |
| `joules_micro`, `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | — | the segment's decode energy and its class (§2) |
| `bracket` | object or null | `measured`: `{ "idle": summary, "busy": summary, "window_us": integer, "sample_us": integer }`; otherwise `null` |
| `table` | object or null | `table`: `{ "id": 32 B hex, "row": integer, "per_completion_uj": integer, "units_per_completion": integer }`; otherwise `null` |
| `estimator` | object or null | `estimator`: `{ "method": "estimator-cpu-time/1", "cpu_us": integer, "active_mw": integer, "basis": string, "relative_ppm": integer }`; otherwise `null` |
| `reason` | term or null | `unmetered`: why (§8, *unmetered reasons*); otherwise `null` |

### 5.2 The four methods

**`measured`.** The sink read its counter over the segment's own decode, under a profile
of energy-measurement §9.2 or §9.3, and seals `OnChipCounter` (or
`CalibratedInstrument`). The busy window is **bounded**: it opens as the segment's first
unit is submitted to the decoder and closes as its last is output, and no unit of another
segment is decoded inside it. The work the window paid for is then exactly the segment:
one completion, with no part-done unit at either edge, which removes the completion-count
term by construction (energy-measurement §2.3). The idle bracket is the last the sink read
under the profile with its decode parked. With `M`, `T`, `n_i`, `n_b`, `s_i`, `s_b` as in
energy-measurement §6.2, these are its §6.2 forms with `Wn = Wd = 1` and `N = 0`:

```
F = ⌊(2·M·T + 10⁶) / (2·10⁶)⌋                       the segment's figure, µJ
p = the least integer with p² · K ≥ P, where
K = 12 · n_i · n_b · F²
P = 12 · T² · (s_i² · n_b + s_b² · n_i)  +  10¹² · n_i · n_b
```

The receipt is accepted when both brackets are live (as rule 2 of §4.5 reads them, the
idle span `n · sample_us`), `window_us` is at least the profile's minimum window, `M ≥
s_i`, `joules_micro` is `F` and at least 1, `energy_uncertainty.window_us` is `T`, and
`relative_ppm ≥ p`. Below a ratio of 10 the figure is sealed and verifiable but not
publishable (energy-measurement §4): a reader derives the verdict from `bracket`.

**`table`.** The sink took its figure from a row of a decode table:

```
joules_micro = ⌊(2 · per_completion_uj · units + units_per_completion) / (2 · units_per_completion)⌋
```

at least 1. Its class is `ModelBased` when the table's `signer` is the receipt's `signer`
— a model calibrated on the host it describes (energy-measurement §2) — and `Estimator`
when it is another key's table, a figure measured on another machine. Neither carries
evidence. A verifier holding the table requires: its identity is `table.id`; row
`table.row` exists, has status `figure`, and the receipt's `capability`, `path`,
`decoder`, `profile`, `unit`, `width`, `height`, `fabrics` and `coverage`; `per_completion_uj` is the
row's `joules_micro` and `units_per_completion` its `units`.

**`estimator`.** Energy-measurement §9.4's `estimator-cpu-time/1`: `profile` names it,
`joules_micro` is its `E` over `cpu_us` and `active_mw` and at least 1, `basis` is
non-empty, `relative_ppm` is at least 1, and the class is `Estimator`. A reader presents
the figure with the stated uncertainty `estimator.relative_ppm`.

**`unmetered`.** No figure: the unmetered marker, `profile`, `fabrics`, `coverage`,
`bracket`, `table` and `estimator` empty, and `reason` one of §8's unmetered reasons.

`HwShunt` is never a segment's class (energy-measurement §2.6, `hwshunt_forbidden`).

### 5.3 The chain

A verifier holding a session's receipts for one stream requires (`segment_chain_broken`):
the same `session`, `stream` and `signer` throughout; `segment` 0 to `k` with none missing or
repeated; and each `prev` the identity of the receipt before it. A stream whose sink
stopped decoding has a last segment; it has no receipt after it.

## 6. The session receipt and the per-hop split

### 6.1 The record

| member | type | meaning |
|---|---|---|
| `wai_sink_session` | integer | `1` |
| `signer`, `session` | 32 B hex | as in the segment receipts it closes |
| `streams` | array | per stream, `{ "stream": 32 B hex, "segments": integer, "last": 32 B hex }`: its segment receipts' count and the identity of its last; at least one, in strictly ascending order of `stream` |
| `media_us` | integer | the sum of the segments' `media_us` |
| `presented_us` | integer | the time the session's content was on screen, in microseconds |
| `display` | object or null | the display's declaration (§6.3), or `null` |
| `hops` | array | the session's hop entries (§6.2), in strictly ascending order of `role`, each role at most once |

### 6.2 The hops, and a hop entry

The hops are the four roles of a delivery's energy, and they are one table with a stream
log's hop entries (stream-termination §2, *Hop*, and §3.5): **1** origin, **2** relay,
**3** sink decode, **4** display. A hop is in one of three states, by the same rule:

- **a figure**: an entry with a figure and its own class;
- **the unmetered marker**: an entry with the marker (energy-measurement §2), for a hop the
  sink knows took place and has no figure for; it says the hop happened and was not
  measured, and it is never a figure of zero joules;
- **absent**: no entry, for a hop the sink states nothing about. Absence is not the marker:
  it says nothing, and in particular not that the hop cost nothing.

| member | type | meaning |
|---|---|---|
| `role` | integer | 1 origin, 2 relay, 3 sink decode, 4 display |
| `joules_micro`, `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | — | the hop's figure and its own class (§2), or the marker |
| `unmetered` | term or null | for the marker, why the hop has no figure: `not_reported`, `hop_unmetered`, `hop_shared`, `unmetered` or `display_undeclared`; `null` for a figure |
| `coverage` | term or null | `null` for the marker; for a figure, `Whole` for the display (the estimate is of the whole panel's draw), `Partial` for the origin and the relay (a hop's record does not state what its figure covers), and the sink decode's by §6.3 |
| `source` | object or null | for the origin and the relay with a figure: the hop record the entry restates (§6.4); otherwise `null` |

Which hops have an entry:

- the **sink decode** always has one: a session's sink decoded its segments;
- the **display** has one exactly when `presented_us` is above 0, and none when nothing was
  presented;
- the **origin** always has one: the session's segments came from one;
- the **relay** has one where the sink knows its segments came through one, and none
  otherwise.

A figure entry's figure is at least 1 and its class coheres with it (energy-measurement
§2); a marker entry has `coverage` and `source` `null` and a reason its role may have:
`not_reported` (the sink holds no figure for the hop), `hop_unmetered` (the hop's own
record carries the marker) or `hop_shared` (§6.4) for the origin and the relay;
`unmetered` for the sink decode; `display_undeclared` or `unmetered` for the display
(`split_part_invalid`). Each hop carries its own class: the receipt states no total over
its hops, and a reader **MUST NOT** add hops of different classes into one figure, nor add
the display to any decode figure (energy-measurement §9.5).

### 6.3 The figure of each hop

**Sink decode (3).** Read from the session's segment receipts (§5), each of which is the
sink-decode hop's figure for its segment, the figure a stream log's segment receipt states
for role 3 (stream-termination §4.6). With the metered segments those whose `method` is
not `unmetered`:

- every segment unmetered: the entry is the marker, `unmetered`;
- otherwise `joules_micro` is the sum of the metered segments' figures, and the class is
  the weakest among theirs in energy-measurement §2's order. Where every metered segment
  is `CalibratedInstrument` with one calibration reference, the entry is that class with
  that reference. Where every metered segment is a hardware class otherwise, the entry is
  `OnChipCounter`, its filtering `On` if any segment's is `On`, `Off` if every metered
  segment is `OnChipCounter` with `Off`, and `Unknown` otherwise. A hardware entry
  declares `window_us` the longest of the segments' windows and `relative_ppm` at least
  `⌈Σ(Fᵢ · pᵢ) / ΣFᵢ⌉` over the metered segments' figures and declarations: linear in the
  parts, so it assumes their errors fully correlated, as jwp-receipts §2.4.1 composes a
  group's;
- `coverage` is `Whole` only when no segment is unmetered and every metered segment is
  `Whole`.

**Display (4).** Energy-measurement §9.5's `estimator-display/1` over the session's
`presented_us`, with the declaration

```
{ "method": "estimator-display/1", "panel_class": "backlit" or "emissive", "on_mode_mw": integer,
  "reference_nits": integer, "nits": integer, "basis": string, "relative_ppm": integer }
```

`on_mode_mw`, `reference_nits` and `relative_ppm` at least 1, `nits` at least 0 and
`basis` non-empty. The entry is a figure with class `Estimator` when the declaration is
present and its `E` is at least 1; the marker `display_undeclared` when `display` is
`null`, and the marker `unmetered` when `E` is 0. A reader presents the figure with the
stated uncertainty `display.relative_ppm`, as an estimate, apart.

**Origin (1)** and **relay (2).** A hop's own signed figure, where this session was the
hop's sole user (§6.4), or the marker: `not_reported` when the sink holds no such figure,
`hop_unmetered` when the hop's record carries the marker, `hop_shared` when the session
shared the hop with other sessions.

### 6.4 A hop's figure, where the session was its sole user

```
source = { "kind": "live-metering-hop" or "other", "record": 32 B hex, "hop_joules": integer,
           "hop_class": class label, "share_num": integer, "share_den": integer }
```

`record` is the hop's record's digest: for `live-metering-hop` the digest of
live-metering §3.5, and for `other` the SHA-256 of the record's bytes as published.
`hop_joules` and `hop_class` are its figure and class label, `hop_joules` at least 1.
`share_num` is the busy time this session accrued on the hop and `share_den` the busy
time every user of the hop accrued over the hop's span, `1 ≤ share_num ≤ share_den`.

A hop's figure is about every session it carried, and it is not divided among them by
the time each kept it busy: busy time does not apportion energy (energy-measurement
§9.1). The entry restates the hop's figure only where this session was the hop's sole
user, at least 99% of its busy time:

```
100 · share_num ≥ 99 · share_den
joules_micro = ⌊(2 · hop_joules · share_num + share_den) / (2 · share_den)⌋
```

and then `joules_micro` is at least 1, its attribution is `rail` (energy-measurement
§9.1), and its class and evidence are the hop's: `energy_provenance` is `hop_class`. A
hop this session shared below that gives no figure: its entry is the marker, with reason
`hop_shared`. A `HwShunt` hop gives
`hwshunt_forbidden`.

For `live-metering-hop` the record is a per-hop session record `WMH1` (live-metering
§3.1), and `share_den` is the sum of its nine `subscriber_us`: `share_num` is the sink's
statement of its own subscriber-microseconds on that hop, which the record does not hold
(live-metering §3 keeps no per-viewer entry). A verifier holding the record's bytes
requires: they read as a per-hop record of the room their own `room` field names, its
signature verified (live-metering §3.6); their digest is `record`; its
`joules` is `hop_joules`, its class label is `hop_class` and its evidence the entry's;
and `share_den` is its subscriber-microseconds' sum (`split_hop_mismatch`). For `other`,
the arithmetic and the class rule are checked from the receipt alone.

### 6.5 Against the segment receipts

A verifier holding the session's segment receipts requires (`session_streams_mismatch`)
that each verifies (§7), names this `session` and `signer`, and that per stream they form
a chain (§5.3) whose count is `segments` and whose last receipt's identity is `last`, with
no stream left out; and that `media_us` is the sum of their `media_us`. It then requires
the sink-decode entry to be what §6.3 gives (`split_decode_mismatch`).

## 7. Verification

A verifier reads a record and checks it in the order below; the first rule broken is the
refusal.

1. **Shape** (`record_malformed`). The record is a JSON object with no duplicate member;
   it has the member §3 names it by, with the value 1 (JCS fixes no order of members, so
   "first" in §3 is the order a producer writes, not a rule); it has exactly the members
   its tables list, each of the type they give. Beyond the types:
   - a hex member has the stated length in lowercase hexadecimal digits; a term is one
     of the terms its table lists; an integer is from 0 to 2⁵³ − 1;
   - a bracket summary has exactly its five integer members, and a round exactly its
     four members;
   - `width` and `height` are integers of at least 1 for `image` and `frame`, and `null`
     for the other units; `units` is at least 1;
   - `decoder` follows its grammar (§4.2);
   - every member of a `fabrics` array is one of `cpu`, `gpu`, `media` and `npu`
     (energy-measurement §9.1), none twice;
   - `measured` is a date `YYYY-MM-DD`;
   - a `source` (§6.4) and a `display` (§6.3) have exactly their members, of their types;
   - each of `hops` has exactly the members of §6.2, its `role` from 1 to 4, the entries in
     strictly ascending order of `role`;
   - a `capability` is one `registry/capabilities.json` lists;
   - a table's `counters` are in ascending order of (`profile`, `detail`), and its
     `machine` and `build` have exactly their members;
   - a segment's `media_us` is at least 1, and its `prev` is `null` exactly when its
     `segment` is 0;
   - a session's `streams` are at least one, in strictly ascending order of `stream`;
   - `hop_class` is a string.
2. **Signature** (`signature_invalid`): `sig` over the body under `signer` (§3).
3. **Classes.** For each class the record carries, in this order — a table's rows in
   order; the segment's; a session's hop entries in the order of their role, each entry's
   class and then its `source`'s `hop_class` — three rules in
   turn: a `HwShunt` label is refused (`hwshunt_forbidden`); then a class beside a
   `joules_micro` of 0, or a non-zero `joules_micro` beside no class, is refused
   (`unmetered_as_zero`); then a label energy-measurement §2 does not name, a class whose
   evidence members are not exactly those its §2.7 requires (as jwp-receipts §2.4.5 writes
   them, `null` where a class carries none), an `energy_uncertainty` with a member of 0 or
   a `relative_ppm` above 2³² − 1, a hardware declaration below §2.3's resolution floor, or
   a `hop_class` that is not a label of a class, is refused (`energy_class_invalid`).
4. **A decode table.** `rounds` is odd and at least 7, and each row not `absent` has
   exactly that many rounds (`table_rounds_invalid`); the rows are in strictly ascending
   order (`table_row_order`); then each row in order, by §4.5:
   - its status and reason (`table_row_status`): an absent row has a reason of the four
     absent ones, no rounds, `workers` 0, `profile` `null` exactly for `no_decoder`,
     empty `fabrics`, `coverage`, `median` and `range` `null`; an absent or `unmetered`
     row carries the unmetered marker; any other row has
     `workers` a power of two and the status and reason its rounds give; and a row's
     profile has an entry with status `live` in `counters` exactly when the row is not
     absent for `no_counter` (`null` profiles aside);
   - its figure (`table_row_figure`): `fabrics` its profile's, and for `figure` and
     `relative` rows `coverage` `Partial`, `median` (an index into `rounds`, from 0) and
     `range` the ones its rounds give, `joules_micro` the median's figure, the class
     `OnChipCounter` with filtering `Unknown` (energy-measurement §9.2, §9.3) or
     `CalibratedInstrument`, and `window_us` the median round's; for `unmetered` rows
     `coverage` `Partial` and `median` and `range` `null`;
   - its declaration (`table_row_declaration`): `relative_ppm ≥ max(p_median, q)`;

   and then, once every row has passed, holding the readings file: its SHA-256 is
   `readings_sha256`, it has one entry
     per row naming the row's `capability`, `path`, `decoder` and `profile`, each row's rounds as many
     as the table's, and every round's values are the ones §4.3 gives
     (`table_readings_mismatch`).
5. **A segment receipt.**
   - Its method (`segment_method_class`): `measured` has `bracket` and no `table`,
     `estimator` or `reason`, a profile of §9.2 or §9.3 of energy-measurement, and a
     hardware class; `table` has `table` and no other, a profile of those, and `ModelBased`
     or `Estimator`; `estimator` has `estimator` and no other, the profile
     `estimator-cpu-time/1`, and `Estimator`; `unmetered` has none of the three, `profile`,
     `coverage` `null`, empty `fabrics`, the unmetered marker and a `reason` of §8's
     unmetered reasons. Every metered segment has `fabrics` its profile's and `coverage`
     `Partial`, and `reason` `null`.
   - `measured` (§5.2): both brackets live (`segment_rail_not_live`); `window_us` at least
     the profile's minimum window (`segment_window_short`); `M ≥ s_i`
     (`segment_below_floor`); `joules_micro` `F`, at least 1, and the declared
     `window_us` `T` (`segment_figure_mismatch`); `relative_ppm ≥ p`
     (`segment_declaration_below`).
   - `table` (§5.2): a verifier **holds** the table whose identity is `table.id` when one
     is given to it; a held table that does not pass steps 1–4 is refused here
     (`segment_table_mismatch`), and `table.row` is an index into its `rows`, from 0.
     The figure is the formula's and at least 1, the class `ModelBased`
     exactly when the verifier holds the table and its `signer` is the receipt's, and
     otherwise `Estimator` where it holds the table; and, holding the table, everything
     §5.2 requires of the row (`segment_table_mismatch`). A verifier that does not hold
     the table checks the formula only.
   - `estimator` (§5.2): the figure is `E` over its inputs and at least 1, `basis`
     non-empty and `relative_ppm` at least 1 (`segment_estimator_mismatch`).
   - Holding the stream's receipts: §5.3 (`segment_chain_broken`), after each receipt has
     passed every step above.
6. **A session receipt**, in order:
   - the hop entries as §6.2 reads them (`split_part_invalid`): the sink decode and the
     origin have an entry, the display has one exactly when `presented_us` is above 0;
     then, in the order of their role, a figure entry has `unmetered` `null`, a figure of
     at least 1, the `coverage` §6.2 gives it (for the sink decode, either of the two here,
     its value checked against §6.3 below) and a `source` exactly when its role is the
     origin's or the relay's; a marker entry has `coverage` and `source` `null` and a
     reason its role may have;
   - the display declaration, where there is one, within its ranges (§6.3), and the
     display entry, where there is one, against §6.3 (`split_display_mismatch`);
   - the origin's then the relay's entry, where they have a figure, against §6.4: the
     sole-user rule, the figure, the class and evidence, and, holding the record, the record
     (`split_hop_mismatch`);
   - holding the segment receipts (a verifier given none holds none), §6.5: each receipt
     verifies (steps 1–5) and the streams agree (`session_streams_mismatch`), then the
     sink-decode entry is what §6.3 gives (`split_decode_mismatch`).

An accepted record establishes that its signer stated it. The arithmetic between its
values is checked, and so are its readings, its table and its hop records where the
verifier holds them. The values themselves — the counter readings, the completions, the
declared inputs of an estimate — are the signer's statements (energy-measurement §5): a
record that invents every value consistently passes every check.

## 8. Reasons

| reason | refused when |
|---|---|
| `record_malformed` | a record does not read in its shape (§3, §7 step 1) |
| `signature_invalid` | its signature does not verify (§3) |
| `hwshunt_forbidden` | a class, or a hop's class, is `HwShunt` (energy-measurement §2.6) |
| `energy_class_invalid` | a class label is unknown, or its evidence is missing, extra or below the resolution floor |
| `unmetered_as_zero` | a class sits beside a zero figure, or a non-zero figure beside no class |
| `table_rounds_invalid` | `rounds` is even or below 7, or a row has another count of rounds |
| `table_row_order` | the rows are not in strictly ascending order |
| `table_row_status` | a row's status or reason is not the one its rounds give (§4.5) |
| `table_row_figure` | a row's median, range, figure, class or window is not the one its rounds give |
| `table_row_declaration` | a row declares less than `max(p_median, q)` |
| `table_readings_mismatch` | the readings file's digest, or a round recomputed from it, differs |
| `segment_method_class` | a segment's members or class do not fit its method |
| `segment_rail_not_live` | a measured segment's bracket is not live |
| `segment_window_short` | a measured segment's window is below its profile's minimum |
| `segment_below_floor` | a measured segment's marginal is below the idle deviation |
| `segment_figure_mismatch` | a measured segment's figure is not its bracket's `F`, or its window not `T` |
| `segment_declaration_below` | a measured segment declares less than its bracket's `p` |
| `segment_table_mismatch` | a table segment's figure, class or row disagrees with its table |
| `segment_estimator_mismatch` | an estimated segment's figure or inputs are not the method's |
| `segment_chain_broken` | a stream's receipts do not form its chain |
| `split_part_invalid` | a hop entry is missing where §6.2 requires one, or is neither a figure nor a marker with its reason |
| `split_display_mismatch` | the display entry is not the estimator's over the declaration |
| `split_hop_mismatch` | an origin or relay entry's busy time, class or record disagrees |
| `split_decode_mismatch` | the sink-decode entry is not what the segment receipts give |
| `session_streams_mismatch` | the streams, counts, last receipts or `media_us` disagree with the segment receipts |

The **row reasons** a table row states are §4.5's: `no_decoder`, `no_work_item`,
`no_hardware_path`, `no_counter`, the round rules `too_few_samples`, `rail_not_live`, `window_short`,
`empty_burst`, `no_marginal`, `below_attribution_floor`, `figure_zero` and
`not_declarable`, and `noisy`. The **unmetered reasons** a segment states are
`no_counter`, `rail_not_live`, `window_short`, `no_marginal`,
`below_attribution_floor`, `figure_zero`, `not_declarable` and `not_reported`.

## 9. Conformance

- A producer of a decode table publishes the table, its readings file and its procedure
  text together, and a table whose readings are not published **MUST NOT** be presented
  as measured.
- A sink that states a segment's decode energy signs a segment receipt for every segment
  it decoded on the stream, unmetered ones included, so a gap shows in the chain.
- The corpus holds decode tables, segment chains and session receipts, accepted and
  refused, each refusal under its reason; every reason of §8 refuses at least one
  vector. It also holds the measured tables of the reference sinks, each with its
  readings and its procedure, which both implementations verify.
- Continuous integration runs the integer checks on every change, and a hardware job on
  the runners whose label names a reference sink's platform. Where the job's counter is
  not readable on its runner, the job reports the measurement as **skipped**, with the
  counter's status, and never as passed.

## 10. Security and privacy considerations

- A decode table names its machine (operating system, architecture, processor, cores
  and memory) and publishes its power levels and its build in cleartext: it is about a
  reference machine, and its signer chooses to publish that. A sink that measures its own
  table and keeps it is not required to publish it.
- A segment receipt names no person, device or account: its `session` is random per
  viewing session. Its `stream` digest and `object` hash name what was decoded, and its
  timings and energy show how the sink decoded it; a sink discloses them to whoever it
  gives the receipt.
- Energy readings are a side channel (energy-measurement §2.4). The profiles read
  counters at 200 ms or slower, the records carry bracket statistics rather than single
  instants, and a reader **MUST NOT** take a sink's receipts as permission to read its
  counters.
