open-standards/wai/extensions/live-metering.md
Transaction Science 01d5cb0921 wai.telemetry.aggregate: QoE measures aggregated under DAP, signed statements and aggregate receipts
extensions/telemetry-aggregation.md: four measures over session metrics
(Prio3SumVec, Prio3Sum, Prio3Histogram), the DAP task's task_info layout and
id, the floor of fifty reports each aggregator enforces, the WQS1 statement
each aggregator signs, the WQA1 receipt the collector signs, verification,
and a room's cell. delivery-telemetry: a class and what it reveals for every
CMCD key the mapping writes (§3.4), delivery-only Request mode (§4.3), §8.1
stated as facts with no consent gate, §8.4 pointing to aggregation.
live-metering §4.7/§6.5: a room may aggregate its QoE instead of sending logs.

wai-rs: HPKE base mode (RFC 9180 A.1.1), DAP-20 messages, a reference
client, leader, helper and collector, the receipt verifier, and the corpus
generator; live-conformance/aggregate/vectors.json from fixed seeds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:03:40 -04:00

799 lines
51 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Live Metering
> Status: Draft. Registers the record `wai.live.metering`: what a room session cost, in
> units and in joules, recomputable from what each hop signed. Every hop that carried the
> session (a relay, a compositor, an ingest gateway) signs one **per-hop session record**,
> `WMH1`, per room session: per track and direction the objects and bytes it carried and the
> objects it dropped by reason, its subscriber sessions' time by layer class, its own energy
> with the acquisition class of [energy-measurement](energy-measurement.md), and a ledger
> root over the grants its sessions ran under. The record is a batch: it holds no entry per
> object and none per viewer, and its size is bounded by what the room published, whatever
> the audience. The room authority, or a key its origin's key log lists for metering, signs
> the **room-session receipt**, `WMR1`, which names the per-hop records, the takes and the
> composition receipts it aggregates, and states the session's units (publisher-seconds,
> subscriber-seconds by layer class, composition-seconds, events sent and delivered,
> recorded-seconds), the joules of every hop with its class, a total per class and never
> one across classes, and each grant's consumption against its joule and funds ceilings.
> Every number in the receipt is recomputed by its verifier from the records it names.
> Clients add quality-of-experience observations, CMCD version 2 Event-mode reports carried
> on the CMCD event timeline, of which only an aggregate over enough sessions enters the
> receipt. The receipt maps onto C2PA 2.4's `c2pa.environmental-sustainability` assertion.
> No new transport. The reference implementation is the `live_metering` module of `wai-rs`
> (feature `live_metering`); the reference relay, `joule-moq-relay`, signs a per-hop record
> from its session summaries; the corpus is
> [`live-conformance/metering/`](../live-conformance/metering/), and its second
> implementation is `live-conformance/live_metering_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision: CTA-5004-B (CMCD version 2: Event mode, its batch body and
> the media type `application/cmcd`); `draft-wilaw-moq-cmcd-event-timeline-00` and
> `draft-ietf-moq-msf-01`, Internet-Drafts not past Working Group Last Call; C2PA 2.4
> §18.26; RFC 6962 §2.1; RFC 8032; RFC 8941 and RFC 9651; FIPS 180-4. A section number of
> one of those drafts in this text is that revision's.
## 1. Scope
In scope: the per-hop session record in its two versions, its track entries, its
subscriber time by layer class, its ledger, version 2's DVR figures, its size bound and its
signature (§3); the room-session receipt, the key that may sign it, its units, its joules
per hop and per class, and its ceilings (§4); verification (§5); quality-of-experience
observations, how they are carried, what of them reaches the receipt, and their aggregation
under [telemetry-aggregation](telemetry-aggregation.md) (§6); the mapping
onto C2PA (§7); and what the reference relay emits (§8).
Out of scope: a transport; how a hop measures its energy, which
[energy-measurement](energy-measurement.md) governs; how a deployment prices a unit, of
which this extension fixes only how a stated tariff is applied (§4.6); billing and payment;
and how a meter enforces a ceiling while the session runs, which [live-rooms](live-rooms.md)
§4.5 leaves to the deployment and this extension does not change (§4.6).
Relationship to the core spec: `wai.live.metering` is a **record** ([SPEC](../SPEC.md) §5):
a signed record a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
## 2. Terms
- **Room**, **room clock**, **principal**, **event**, **chain**, **grant**, **grant in
effect**, **room key**, **merged order**, **slot**, **layer**: as in live-events §2 and
live-rooms §2, §6.3, §9.2.
- **Room session**: the span of room clock a receipt states, [`start`, `end`), in
microseconds.
- **Hop**: a party that carries the session's objects and signs a record of it, named by
its Ed25519 key (RFC 8032). Its **role** is 1 relay, 2 compositor ([live-compose](live-compose.md) §7)
or 3 ingest gateway.
- **Track of the room**: a track whose Track Namespace's first field is the room id as 64
lowercase hexadecimal digits (live-rooms §6.1). A **media track** is one whose namespace
is the room, an owner and `audio`, `video` or `screen`, and whose name is a layer from `0`
to `7`; an **events track** one whose namespace ends `events` and whose name is `0`
(live-events §8.1); every other track of the room is an **other** track (receipts tracks,
catalogs, CMCD tracks among them).
- **Track digest**: live-events §3.1's anchor `track`: SHA-256 (FIPS 180-4) over the
track's namespace as live-rooms §6.2 renders it, one zero byte, and its name.
- **Session** of a hop: one transport session a peer holds with it (a MOQT session, a
WebRTC peer connection). A **subscriber session** is one that subscribes to a track of the
room and that is not another hop of the session.
- **Unmetered marker**: a zero figure with class tag 0 (energy-measurement §2), never a
figure of zero joules.
- *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers of that many bits. A digest is
SHA-256, 32 bytes. **Node**: staged-measurement Appendix C's `SHA-256("wai:out-node\x01" ‖
left ‖ right)`. The **root** of a list of leaf hashes is RFC 6962 §2.1's `MTH` with that
node; the root of no leaf is 32 zero bytes.
## 3. The per-hop session record
### 3.1 Layout
Each hop signs one record per room session, `WMH1`, when its part in the session ends:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WMH1` (ASCII) |
| `version` | 1 | 1 or 2 |
| `room` | 32 | the room id |
| `hop` | 32 | the hop's key, which signs the record |
| `role` | 1 | 1 relay, 2 compositor, 3 ingest gateway |
| `start`, `end` | 8 each | u64: the span on the room clock the record covers, [`start`, `end`) |
| `joules` | 8 | u64: the hop's own energy over the span, in microjoules; 0 is the unmetered marker |
| `class` | 1 + … | its acquisition class: energy-measurement §2.7's tag byte, then that tag's evidence |
| `tracks` | 2 + 90 × *n* | a u16 count *n*, then each track entry (§3.2) |
| `sessions` | 4 | u32: the subscriber sessions it served in the span |
| `subscriber_us` | 72 | nine u64: the subscriber-microseconds of layer classes 0 to 8 (§3.3) |
| `events` | 8 | u64: the events it delivered to subscriber sessions (§3.3) |
| `grants` | 4 | u32: the entries of its ledger (§3.4) |
| `ledger` | 32 | the ledger's root (§3.4) |
| `dvr` | 48 | version 2 only: six u64, the hop's DVR figures (§3.9) |
| `sig` | 64 | §3.5 |
Version 2 is version 1's fields in the same order and one more, `dvr`, after `ledger` and
before `sig`. A hop that keeps a DVR window (live-recording §8) signs version 2, and a hop
without one signs version 1.
### 3.2 Track entries
A track entry is 90 bytes:
| field | size | meaning |
|---|---|---|
| `kind` | 1 | 1 a media track; 2 every events track of the room, together; 3 every other track of the room, together |
| `track` | 32 | for kind 1, the track digest; for kinds 2 and 3, 32 zero bytes |
| `dir` | 1 | 1 in: what the hop received; 2 out: what it sent |
| `objects` | 8 | u64. In: every object it received on the track, those it dropped included. Out: every object it sent, live or fetched |
| `bytes` | 8 | u64: those objects' payload bytes |
| `dropped_timeout` | 8 | u64, out: objects not sent because their delivery timeout passed at this hop (MOQT §5.2), the rest of a subgroup reset for it included |
| `dropped_refused` | 8 | u64, in: objects refused at the door, their receipt not verifying (jwp-receipts §4.1) |
| `dropped_duplicate` | 8 | u64, in: objects another upstream of the track had already delivered |
| `dropped_closed` | 8 | u64, out: objects still queued when their session ended |
| `receipted` | 8 | u64, out: of the objects dropped, those whose receipts the hop forwarded to the same session on the track's receipts track (jwp-receipts §4.4) |
Media tracks are counted one by one, since a room publishes few of them. Events tracks are
counted together, since every principal of the room, an audience member who chats among
them, has one, and so are other tracks: a record holds no entry that grows with the
audience (§3.7).
### 3.3 Subscribers and layer classes
The **layer class** of a subscriber session at an instant is the highest layer of the
room's media tracks it is subscribed to then, 0 to 7; or 8 when it is subscribed to tracks
of the room but to none of its media tracks (an events-only viewer). `subscriber_us[c]` is
the sum, over the hop's subscriber sessions, of the room-clock microseconds within the span
during which the session was of class *c*. A session is a subscriber session from its first
subscription to a track of the room until it holds none; `sessions` counts those of the
span. A session's class follows the subscriptions it holds at each instant, including as
it closes: a hop that ends a closing session's subscriptions one at a time classes the
moments between by what is still held, class 8 once its last media subscription has ended
and while another of the room's tracks is still held.
`events` counts the objects of Object ID 1 or above the hop delivered on the room's events
tracks to subscriber sessions: on an MSF event timeline each is one event (live-events
§8.1), while object 0 of a group repeats the join set and is not counted.
A session another hop holds with this one (a relay that subscribes upstream for its own
subscribers, a compositor's subscriptions to the inputs it composes) is a **hop link**: its
objects are counted in the track entries, and it is not a subscriber session. So a
subscriber is counted at exactly one hop, the one that serves it, and a room's subscriber
time sums over its hops. A compositor is a principal of the room under its own grant, and
its subscriptions at a relay are that relay's subscriber sessions.
### 3.4 The ledger
The hop's **ledger** is the list of its entries, one for each grant its sessions in the span
ran under, in strictly ascending order of `grant`. An entry is 143 bytes:
| field | size | meaning |
|---|---|---|
| `grant` | 32 | the grant's digest (live-rooms §4.2); 32 zero bytes for sessions under no grant, such as a viewer admitted by Privacy Pass (live-rooms §10.4) |
| `joules_ceiling` | 8 | u64: the grant's `joules_ceiling`; 2^64 − 1 for none, and for the zero grant |
| `funds_ceiling` | 8 | u64: the grant's `funds_ceiling`; 2^64 − 1 for none |
| `currency` | 3 | the grant's `currency`; three zero bytes for none |
| `sessions` | 4 | u32: the hop's sessions under the grant, as publisher or subscriber |
| `bytes_in` | 8 | u64: the payload bytes the hop received from them on tracks of the room |
| `bytes_out` | 8 | u64: the payload bytes it sent to them on tracks of the room |
| `subscriber_us` | 72 | nine u64: their subscriber-microseconds by layer class (§3.3) |
A hop knows the grant a session runs under from the token it was admitted with, which maps
the grant's claims (live-rooms §10): the hop holds the grant beside the token, and the
ceilings are the grant's. Each entry's leaf is SHA-256 over the 20 bytes
`wai:metering-ledger` (ASCII) and 0x01, followed by the entry; `ledger` is the root of the
entries' leaves in order, and `grants` their number. The ledger travels beside the record,
unsigned; the record signs its root, so the ledger can only be the one its hop signed, and
one entry can be shown to the principal it concerns with its RFC 6962 audit path, without
the others.
### 3.5 Digest and signature
The **digest** of a record is SHA-256 over its bytes before `sig`. Its **signature** is
Ed25519 by `hop` over the 17 bytes `wai:metering-hop` (ASCII) and 0x01, followed by the bytes
the digest covers, verified strictly, as [SPEC](../SPEC.md) §7 *Signatures* requires: a key
that fails step 1 or 2 is `bad_key`, and a signature that fails a later step is
`bad_signature`.
### 3.6 Reading and rules
A reader reads exactly the bytes it is given as one record, as live-events §3.3 reads a
record with `WMH1` for `WEV1` (`truncated`, `malformed`, `unknown_version`), except that it
reads version 1 or 2, each in the order of §3.1 for its version; any other version is
`unknown_version`. The class's evidence is read as energy-measurement §2.7 writes it; for
tag 5 a reference length above 255 is `malformed`. It then refuses a record that breaks any
of these rules as `energy_invalid`:
- the tag is 0 exactly when `joules` is 0;
- the tag is 0, 2 (`ModelBased`), 3 (`Estimator`), 4 (`OnChipCounter`) or 5
(`CalibratedInstrument`): the legacy tag 1 (`HwShunt`) is never emitted (energy-measurement
§2.6);
- for tags 4 and 5, `relative_ppm` and `window_us` are above 0, the figure meets the
resolution floor (`joules` × `relative_ppm` ≥ 288 676, energy-measurement §2.3), tag 4's
filtering byte is 0, 1 or 2, and tag 5's reference is UTF-8 (RFC 3629) and not only white
space;
and as `record_invalid` one that breaks any of these:
- `role` is 1, 2 or 3; `start` is below `end`;
- the track entries are in strictly ascending order of (`kind`, `track`, `dir`), each
`kind` 1 to 3 and each `dir` 1 or 2; a media entry's `track` is not zero and another
entry's is;
- an entry in has `dropped_timeout`, `dropped_closed` and `receipted` 0, and
`dropped_refused` + `dropped_duplicate` at most `objects`; an entry out has
`dropped_refused` and `dropped_duplicate` 0, and `receipted` at most `dropped_timeout` +
`dropped_closed`;
- with `sessions` 0, every `subscriber_us` and `events` are 0;
- `ledger` is 32 zero bytes exactly when `grants` is 0;
- in a version 2 record (§3.9), `dvr_objects_out` is at most the sum of `objects` over the
out entries, and `dvr_bytes_out` at most the sum of their `bytes`; `dvr_objects_stored` is
at most the sum of `objects` over the in entries, and `dvr_bytes_stored` at most the sum of
their `bytes`.
A ledger is read as entries of 143 bytes, its length a multiple of 143, in strictly
ascending order of `grant`, each `currency` three zero bytes exactly when its
`funds_ceiling` is 2^64 − 1 and three capitals `A`–`Z` otherwise, and the zero grant with
both ceilings 2^64 − 1: otherwise `ledger_invalid`. Its number of entries is checked
against the record's `grants` when the record is verified (§5.1 step 4).
### 3.7 The size bound
Let *M* be the number of distinct media tracks the room published at some instant of the
room session (§5.3). A record of version 1 is then at most
B(M) = 552 + 180 × (M + 2)
bytes, and one of version 2 at most
B₂(M) = 600 + 180 × (M + 2)
bytes. A record longer than the bound of its version is refused `record_too_large` before it
is read: its version is read from its fifth byte before anything else is read, and a record
whose fifth byte is not 2, or that has no fifth byte, is held to B(M). 552 is the record's
fixed fields, 281 bytes, with the longest class evidence, 271 bytes (tag 5 with a reference
of 255 bytes); 600 is those and version 2's `dvr`, 48 bytes; and a record has at most two
entries for each media track (in and out) and two each for the events and other tracks
together, 2 × (*M* + 2) entries of 90 bytes.
The bound does not depend on the audience. A session with three viewers and one with a
million, carried by the same publishers, give records of one size: subscribers are summed
into nine layer classes and their grants into the ledger, which the record signs by its
root. A hop's ledger grows with the grants it served; the record, which is what travels
upstream and into the receipt, does not. Receipts batch per group upstream in the same way
(jwp-receipts §2.3): a relay forwards a group receipt, not one per viewer per object.
### 3.8 What a hop counts
A hop's record covers one room session, every session it held with a track of the room in
the span. A hop that signs more than one record in a session (a long session flushed in
parts) gives them spans that do not overlap and cuts them at group boundaries: an object is
counted in the record whose span holds the arrival of its group's first object at the hop.
### 3.9 DVR figures
A hop that keeps a DVR window of the room ([live-recording](live-recording.md) §8) signs
version 2, whose `dvr` field is six u64 over the record's span, in this order:
| field | meaning |
|---|---|
| `dvr_objects_out` | of the objects the record's out entries count (§3.2), those the hop sent from its DVR store in answer to a FETCH or a fill |
| `dvr_bytes_out` | those objects' payload bytes |
| `dvr_objects_stored` | the objects of the room the hop wrote to its DVR store in the span |
| `dvr_bytes_stored` | those objects' payload bytes |
| `dvr_byte_s` | the payload bytes of the room its store held, integrated over the span, in byte-seconds, rounded down: ⌊Σ *b* × *h* / 10^6⌋ over the objects it held, *b* an object's payload bytes and *h* the microseconds within the span during which the store held it |
| `dvr_dropped` | the objects of the room the hop dropped from its store because they did not verify against their receipts (jwp-receipts §4.1), which it never served |
A hop that keeps no DVR window signs version 1, which states no DVR figures. The objects a
hop sends from its store are among those its out entries count, and the objects it stores
among those its in entries count, so §3.6 holds the out figures to the out entries and the
stored figures to the in entries. A record flushed in parts (§3.8) states in each part the
figures of its own span.
## 4. The room-session receipt
### 4.1 Layout
When the session is over, the room authority, or a metering key (§4.3), signs the receipt,
`WMR1`:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WMR1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `room` | 32 | the room id |
| `signer` | 32 | the key that signs the receipt |
| `start`, `end` | 8 each | u64: the room session, [`start`, `end`) |
| `hops` | 1 + … | a u8 count, then for each per-hop record it aggregates, in strictly ascending order of `record`: `record` (32, the record's digest), `hop` (32), `role` (u8), `joules` (u64) and `class` (1 + …), restated from the record |
| `takes` | 1 + 32 × *k* | a u8 count, then the digest of each take manifest it aggregates ([capture-binding](capture-binding.md) §7.1), strictly ascending |
| `compose` | 4 + 32 | u32: the composition receipts it aggregates ([live-compose](live-compose.md) §8); then their **list digest**, SHA-256 over their digests in ascending order, concatenated (32 zero bytes when none) |
| `qoe` | 4 + 32 | u32: the session logs of QoE observations it aggregates (§6); then the SHA-256 of the logs document (32 zero bytes when none) |
| `units` | 120 | fifteen u64: `publisher_us`, `subscriber_us` for layer classes 0 to 8, `sessions`, `composition_us`, `events_sent`, `events_delivered`, `recorded_us` (§4.4) |
| `unmetered` | 1 | u8: the hop entries whose figure is the unmetered marker |
| `totals` | 1 + 13 × *m* | a u8 count *m*, then for each acquisition class among the hop entries: `tag` (u8), `figures` (u32, the hop entries of that class) and `joules` (u64, their sum), in strictly ascending order of `tag` (§4.5) |
| `tariff` | 3 + 52 | `currency` (3; zero for no tariff), then thirteen u32 prices in millionths of a minor unit: per second of `publisher_us`, of `subscriber_us` for each layer class 0 to 8, of `composition_us` and of `recorded_us`, and per event sent (§4.6) |
| `ceilings` | 4 + 32 + 4 + 33 × *p* | u32: the grants of the session (§4.6); the **consumption root** (32); then a u32 count *p* and each grant past a ceiling: `grant` (32) and `flags` (u8: bit 0 past its joule ceiling, bit 1 past its funds ceiling), in strictly ascending order of `grant` |
| `qoe_cell` | 49 | the QoE aggregate (§4.7): `suppressed` (u8), `started`, `start_failed`, `exit_before_start` (u32 each), `stalls`, `stall_ms`, `engaged_ms` (u64 each), `vst_count` (u32), `vst_ms` (u64) |
| `sig` | 64 | §4.2 |
### 4.2 Digest, signature and rules
The **digest** of a receipt is SHA-256 over its bytes before `sig`; its **signature** is
Ed25519 by `signer` over the 21 bytes `wai:metering-session` (ASCII) and 0x01, followed by
the bytes the digest covers, verified strictly (SPEC §7 *Signatures*).
A reader reads exactly the bytes it is given, as §3.6 reads a record with `WMR1` for `WMH1`
(`truncated`, `malformed`, `unknown_version`), and refuses as `classes_summed` a receipt
whose `totals` names tag 0: a total over figures of no one class, which this layout has no
other way to state. It refuses as `session_invalid` a receipt that breaks any of these:
- `start` is below `end`;
- each list is in the strictly ascending order §4.1 gives it; each hop entry's `role` is 1,
2 or 3, and its class reads and meets §3.6's `energy_invalid` rules; each total's `tag` is
2 to 5 and its `figures` above 0;
- `compose`'s digest is zero exactly when its count is, and so is `qoe`'s;
- with no tariff (`currency` zero) every price is 0; otherwise `currency` is three capitals
`A`–`Z`;
- each past grant's `flags` is 1, 2 or 3;
- `suppressed` is 0 or 1, and with 1 every other field of the cell is 0.
### 4.3 Who signs: the metering authority
The receipt is signed by the room authority (live-rooms §3.1), or by a **metering key**: a
key the room's origin's key log lists, not revoked, with the authority `live/meter` in its
`may_sign` (jwp-receipts §7.5). Where the room names an origin, the signer's key-log entry,
the room authority's included, **MUST** also hold `energy/` followed by the class label of
each class the receipt totals, as jwp-receipts §7.5 requires of every signer that restates
a figure with its class. A room that names no origin has no key log, and only its authority
signs its receipts.
The metering power is a key-log authority and not one of a grant's powers (live-rooms
§3.3), for three reasons:
1. **A receipt states every principal's consumption.** A participant's power to meter would
let one participant sign the bill of another, the authority's own ceilings included.
Metering is the operator's act, as storing a take is (`capture/store`), and the operator's
keys live in the origin's key log.
2. **Revocation and trust come with it.** A metering key is revoked through the same log as
the room authority and the hops (§5.2), and a relying party anchors it as it anchors any
origin key (jwp-receipts §7.4), with the energy authorities its figures need.
3. **A grant's powers are a closed set of four bits**, which live-rooms §3.4 holds a
descriptor and a grant to; this extension does not change them.
A signer that is neither is refused `signer_not_authorised`; a metering key whose entry is
`revoked` is refused `signer_revoked`.
### 4.4 Units
Every unit is an integer of room-clock microseconds or a count, over the room session
[`start`, `end`), and each is recomputed from the records the receipt names:
- **`publisher_us`**, publisher-microseconds: over every principal, the microseconds of the
session during which it **publishes** (live-rooms §7.3: an own track published, or a place
in a slot, live or standby), by the lifecycle replay of §5.3.
- **`subscriber_us`** for layer classes 0 to 8, and **`sessions`**: the sums over the hop
records of their `subscriber_us` and their `sessions` (§3.3).
- **`composition_us`**, composition-microseconds: over each composed track, the receipts of
one compositor, one media kind and one output track, sorted by `t`: the sum of
*t*<sub>*j*+1</sub> − *t*<sub>*j*</sub> over consecutive receipts at most 1 000 000 µs
apart. A composed track whose outputs stop for more than a second did not compose in
between, and a lone receipt adds nothing, so the figure is never more than the time the
composition ran.
- **`events_sent`**: the records the room verifier accepted (live-rooms §9.2), each once by
its digest, whose `t` is in the session: every principal's, the room keys' lifecycle
events included.
- **`events_delivered`**: the sum over the hop records of their `events` (§3.3).
- **`recorded_us`**, recorded-microseconds: over each take, the **committed capture**: with
*o* the offset of its first held commitment and *s* the `size` of its last (capture-binding
§9.2, §9.3), the sum over the manifest's chunks of index below *s* of the part of [`start`
+ *o*, `end` + *o*), each chunk's capture range on the room clock, within the session. A
take with no held commitment records nothing. What a device captured and did not commit
while capturing is not counted.
The units do not sum the DVR figures of version 2 records (§3.9) in this revision.
### 4.5 Joules per hop and per class
Each hop entry restates its record's `hop`, `role`, `joules` and `class`, so the receipt
states the energy of every hop with the class it was obtained under. `unmetered` counts the
entries whose figure is the unmetered marker; they are in no total, and a reader **MUST
NOT** present them as zero joules (energy-measurement §2).
The **totals** are one per class: for each tag among the metered entries, the number of
entries of that tag and the sum of their `joules`. Nothing in the receipt sums figures of
two classes. A `ModelBased` figure added to an `OnChipCounter` figure is neither a counter
reading nor a model's output, and a total of the two, labelled with either class, would
present a model as a measurement or a measurement as a model. So a receipt whose total
names tag 0 is refused `classes_summed` (§4.2), and so is one whose total for a class is the
sum of the figures of that class and of at least one other while not the sum of its own:
the total of class *X* equals Σ<sub>*c* ∈ *S*</sub> *T<sub>c</sub>* for some set *S* of the
receipt's classes that holds *X* and another, where *T<sub>c</sub>* is the sum of the
figures of class *c*, and does not equal *T<sub>X</sub>*. Any other total that does not
recompute is `unit_mismatch`.
The receipt's energy covers the session **whole** when `unmetered` is 0 and it has at least
one hop entry, and **partially** otherwise (energy-binding §4's `Whole` and `Partial`). A
partial total **MUST NOT** be presented as the session's energy.
### 4.6 Ceilings
**The grants of the session** are every grant a hop ledger names, the zero grant included,
and every grant that was in effect for a present principal at some instant of the session
(§5.3), in strictly ascending order of digest. For each, its **consumption** is:
- `publisher_us`: the publisher-microseconds of principals while it was their grant in
effect;
- `subscriber_us`, by layer class: the sums of its ledger entries' over the hop records;
- `composition_us`: the composition-microseconds of §4.4 whose earlier receipt's compositor
had it in effect at that receipt's `t`;
- `recorded_us`: the committed capture of the takes whose first held commitment was verified
under it (live-rooms §9.2);
- `events_sent`: the records of §4.4's count verified under it;
- **attributed joules**, by class: each metered hop record's figure *J* is shared among its
ledger entries by the bytes they moved: an entry with *b* = `bytes_in` + `bytes_out` of a
record whose entries move *B* bytes in all receives ⌊*J* × *b* / *B*⌋ µJ of that record's
class, nothing when *B* is 0, and the remainder is attributed to no grant. An attributed
share is an allocation of a hop's figure, not a measurement of the grant's use, and keeps
the figure's class; it is **unmetered** for the grant when an unmetered record's entry for
it moved bytes;
- **funds**, where the tariff's `currency` is the grant's: ⌈*N* / 10^12⌉ minor units of it,
with *N* = `publisher_us` × *p*<sub>pub</sub> + Σ<sub>*c*</sub> `subscriber_us`[*c*] ×
*p*<sub>sub,*c*</sub> + `composition_us` × *p*<sub>comp</sub> + `recorded_us` ×
*p*<sub>rec</sub> + `events_sent` × *p*<sub>ev</sub> × 10^6, at most 2^64 − 2; 2^64 − 1
states that no funds were computed (no tariff, or another currency).
Its verdicts, against its ceilings, each the first row of the table below that applies,
taken in the order none, past, unmetered, partial, within for the joule ceiling, and none,
no tariff, past, within for the funds ceiling:
| verdict | joule ceiling | funds ceiling |
|---|---|---|
| 0 none | `joules_ceiling` is 2^64 − 1 | `funds_ceiling` is 2^64 − 1 |
| 1 within | every hop that moved its bytes was metered, at least one did, and the attributed joules of every class together are at most the ceiling | the funds are at most the ceiling |
| 2 past | the attributed joules of every class together are above the ceiling | the funds are above the ceiling |
| 3 partial / no tariff | within on what was metered, and an unmetered hop moved its bytes | no funds were computed |
| 4 unmetered | no metered hop moved its bytes | — |
A joule ceiling is one number, and holding a use to it adds what each hop spent, whatever
its class. The verdict is that comparison; the sum it compares is stated nowhere as a
figure, and the attributed joules stay per class.
A grant's **consumption entry** is 198 bytes: `grant` (32), `joules_ceiling` (8),
`funds_ceiling` (8), `currency` (3), `publisher_us` (8), `subscriber_us` (9 × 8),
`composition_us` (8), `recorded_us` (8), `events_sent` (8), the attributed joules of tags 2,
3, 4 and 5 (4 × 8), `unmetered` (u8: 1 when its share is unmetered at some hop), `funds` (8),
and the joule and funds verdicts (u8 each). Its leaf is SHA-256 over the 25 bytes
`wai:metering-consumption` (ASCII) and 0x01, followed by the entry; the **consumption root**
is the root of the grants' leaves in order, and the receipt's `ceilings` states their number,
the root and every grant with a verdict of 2. Every principal can be shown its own entry with
its audit path against the receipt, and nobody else's.
**Past a ceiling.** live-rooms §4.5 fixes what a ceiling is: the energy and the funds a
grant's use may spend, which a meter holds the use to. How a meter holds it while the session
runs is the deployment's, and this extension changes neither the ceiling nor that. A meter
that ends a use past its ceiling does so through the room's own records: the clock removes
the principal (`leave`, reason 2) or the authority exchanges its grant (live-rooms §4.6). A
receipt records what the session did, not what it was allowed: a use past its ceiling is
counted whole, and the receipt names its grant among those past.
### 4.7 The QoE cell
The cell aggregates the session logs of §6 into one cell. With fewer than 50 logs it is
**suppressed** (delivery-telemetry §8.4: suppressed, not rounded): `suppressed` is 1 and
every other field 0. Otherwise `suppressed` is 0, and over the logs' session metrics
(delivery-telemetry §7.2): `started`, `start_failed` and `exit_before_start` count the logs
for which each holds; `stalls`, `stall_ms` and `engaged_ms` are sums; `vst_count` counts the
logs with a video start time and `vst_ms` sums those times. A cell of fewer than 50 logs that
is not suppressed is refused `qoe_not_suppressed`. The cell is the signer's aggregate of
statements nobody signed (delivery-telemetry §1.1) and **MUST NOT** be presented as evidence
of what a viewer saw; it carries no joule figure (§1.2 there).
A room **MAY** instead aggregate its quality of experience under
[telemetry-aggregation](telemetry-aggregation.md) (§6.5): its receipt then names no logs
(`qoe` zero) and its cell is suppressed, as above, and the room's cell is computed from the
aggregate receipts of its tasks (telemetry-aggregation §9.2), whose floor of 50 reports each
aggregator enforces.
## 5. Verification
### 5.1 Inputs and steps
A receipt verifier is given the room (a descriptor, grants, the head of the authority's key
log or none, and the chains, as live-rooms §9.1), the receipt, the per-hop records with their
ledgers, the composition descriptors and receipts, the take manifests, and the logs document
or none. It runs these steps and stops at the first refusal, which it reports with the
reason and where:
1. **The room** (live-rooms §9): its refusal, at `room`, with the room's own place. A gap in
a room key's chain is `lifecycle_incomplete` (live-compose §4.1), at `room`: the
lifecycle, and with it every unit, is unknown after it. A gap in another principal's
chain is not a refusal: the units count what the room verifier holds.
2. **The receipt** (§4.2), at `receipt`: `truncated`, `malformed`, `unknown_version`,
`classes_summed`, `session_invalid`; `wrong_room` when its `room` is not the room id;
`bad_key`, `bad_signature`.
3. **The signer** (§4.3), at `receipt`: `signer_not_authorised`, `signer_revoked`.
4. **Each hop entry**, in order, at `hop` and the entry's index:
1. a record whose digest is the entry's `record` is given, with a ledger: otherwise
`record_missing`. (The digest of given bytes is SHA-256 over all but their last 64.)
2. Its length is at most the bound of its version, read from its fifth byte (§3.7):
otherwise `record_too_large`.
3. It reads as a record of version 1 or 2 (§3.6): `truncated`, `malformed`,
`unknown_version`, `energy_invalid`, `record_invalid`; its `room` is the room id
(`wrong_room`); its signature verifies (`bad_key`, `bad_signature`).
4. The entry's `hop`, `role`, `joules` and `class` are the record's: otherwise
`unit_mismatch`.
5. The hop may sign for its role (§5.2): `hop_not_authorised`, `hop_revoked`.
6. Its span is within the session: `start` ≤ its `start` and its `end` ≤ `end`. Otherwise
`span_outside_session`.
7. Each media entry's `track` is a track the room published in the session (§5.3):
otherwise `track_unknown`.
8. Its ledger reads (§3.6, `ledger_invalid`); has `grants` entries and the root `ledger`;
and each entry names the zero grant or a grant of the room, with that grant's ceilings
and currency. Otherwise `ledger_mismatch`.
9. Its `subscriber_us` are the sums of its ledger's, class by class: otherwise
`unit_mismatch`.
5. **Each take**, in the receipt's order, at `take` and its index: a manifest with that
digest is given (`record_missing`); it reads and verifies (capture-binding §7.2, with
that section's reasons); and it is in the room as capture-binding §9.2 finds it, with that
section's reasons.
6. **The composition receipts**, at `compose`: the receipts given are as many as `compose`
states and have its list digest, otherwise `record_missing`; then each, in ascending
order of digest, at its index: it reads (live-compose §8.1, with that section's
reasons), the composition descriptor it names is given (`record_missing`), it verifies
against the room and that descriptor (live-compose §8.3, with that section's reasons),
and its `t` is in the session (`span_outside_session`).
7. **The logs**, at `qoe`: when `qoe` states logs, a logs document whose SHA-256 is its
digest is given (`record_missing`); it reads (§6.1, `qoe_invalid`); and it holds as many
logs as `qoe` states (`unit_mismatch`).
8. **The units** (§4.4), at `units`: `unit_mismatch`.
9. **The energy** (§4.5), at `energy`: `unmetered` is the number of unmetered hop entries;
then each stated total, in order, names a class among the hop entries' and states that
class's sum, or else is `classes_summed` when the sum it states is one §4.5 describes
and `unit_mismatch` otherwise, and states that class's number of figures; and the totals
name every class of the metered entries. Otherwise `unit_mismatch`.
10. **The QoE cell** (§4.7), at `qoe`: `qoe_not_suppressed`, then `unit_mismatch`.
11. **The ceilings** (§4.6), at `ceilings`: the number of grants, the consumption root and
the grants past a ceiling recompute; otherwise `ceiling_mismatch`.
A receipt that passes is **accepted**: every unit, every hop's figure, every total and every
grant's consumption is what the records it names give.
### 5.2 Who may sign a hop record
Where the room names an origin, the hop's key has an entry in the head given for the room
(live-rooms §5) whose `may_sign` holds the authority of its role, `live/relay`,
`live/compositor` or `live/ingest`, and, for a metered figure, `energy/` followed by the
label of its class (jwp-receipts §7.5): otherwise `hop_not_authorised`. An entry whose
`status` is `revoked` is `hop_revoked`. A hardware class's energy authority is confirmed only
under a pinned anchor (jwp-receipts §7.5, *Pinned-only authorities*); the verifier is a
function of the head its caller anchored, and the caller states which anchor that was.
Where the room names no origin, a hop's key is a room key (live-rooms §2): otherwise
`hop_not_authorised`.
### 5.3 The room's replay
Over the records the room verifier accepted, in the merged order (live-rooms §9.2), the room
keys' lifecycle events are applied as live-rooms §7.2 applies them, each taking effect at its
`t`: the room at an instant *x* is the state after every lifecycle event whose `t` is at most
*x* (live-compose §4.1). Between two consecutive instants at which lifecycle events fall, the
state is constant. From it:
- **who publishes**, and so `publisher_us`: a principal publishes while it has an own track
published or stands in a slot, live or standby;
- **the grant in effect** of each present principal, and so the grants of the session;
- **the media tracks published**, and so *M* (§3.7) and `track_unknown`: at each instant of
the session, the tracks of each own track's layers 0 to `layers` − 1 (owner the principal),
and of each slot's live publisher's layers (owner `_` and the slot's name), each as its
track digest. A standby's media is not delivered and adds no track.
### 5.4 Outcome
- **refuse**: the reason and where (§5.1).
- **accept**: the receipt's digest, the session's units, each hop record's digest, size,
bound and version, with a version 2 record's six DVR figures (§3.9), the totals and the
coverage, each grant's consumption entry, the consumption root, and the QoE cell.
## 6. Quality-of-experience observations
### 6.1 Observations and their logs
A client of the room reports its experience with the observations of
[delivery-telemetry](delivery-telemetry.md) §2, unchanged: `start`, `present`, `stall-start`,
`stall-end`, `end` and the rest, one session's in `sn` order (§7.1 there). Their Event-mode
reports are CMCD version 2 (CTA-5004-B), as delivery-telemetry §3 maps them. A **session
log** is one session's observations; the **logs document** is a JSON array of logs, each a
non-empty JSON array of observations, as UTF-8 bytes; it reads when every observation
reads (delivery-telemetry §2.4) and every log is a log (§7.1 there), and is refused
`qoe_invalid` otherwise. The receipt names the document by its SHA-256 and states the number
of logs; the cell (§4.7) is computed from their session metrics (delivery-telemetry §7.2).
### 6.2 On the CMCD event timeline
Over MoQ, a client publishes its reports on a CMCD track of
`draft-wilaw-moq-cmcd-event-timeline-00`: an MSF event timeline (MSF §8) whose catalog entry
is in the catalog's `publishTracks`, with `packaging` `eventtimeline`, `eventType`
`urn:cta:cmcd:2026` and a `cmcdConfig` (that draft's §4.2, §4.3). The room's catalog names
the track; its namespace's first field is the room id, so it is a track of the room, counted
among the other tracks (§3.2). Each record of the timeline is one Event-mode report:
- an observation that delivery-telemetry §3 reports in Request mode, a `response` (its
`e=rr`, which that draft's §3 bars) and a `present` that is not the session's first are
not carried;
- the record's wallclock index `T` is the report's `ts`, and `ts` is not in its `data` (that
draft's §4.2.1);
- `data` holds the report's members that the draft's §3 permits and the catalog's
`cmcdConfig` enables, `v` always, each value a string: an Integer's decimal digits, a
Token's characters, a String's characters without its quotes, an Inner List as RFC 9651
serializes it. `sf` and the custom keys of delivery-telemetry §4 are not among the keys
that draft permits and are not carried;
- where the catalog's track name asks for a player identifier (that draft's §5), the client
uses the observation's `sid`, fresh per session (delivery-telemetry §8.1), and never a
principal's key or any identifier that outlives the session.
The draft's own example writes Integers as JSON numbers; its normative text says that every
value is a string, and this revision follows the text. The reference writes the record with
`live_metering::cmcd_timeline_record`.
### 6.3 Over HTTP: `application/cmcd`
Over HTTP, a client sends its Event-mode reports to the room's service as CTA-5004-B's batch:
an HTTP POST whose body has the media type `application/cmcd`, one report a line, the lines
separated by one line feed and the last not followed by one, each line a report in the
`key=value` syntax with its keys in alphabetical order and no URL encoding. A report of
delivery-telemetry §3 is such a line already: an RFC 9651 Dictionary with its keys in
bytewise order, with no optional white space, every Event-mode report carrying `e`, `ts` and
`v`. CTA-5004-B cites RFC 8941, which RFC 9651 obsoletes; the reports use no type RFC 9651
added (Date, Display String), so a line is the same bytes under either. The reference writes
a batch with `live_metering::cmcd_batch_body`.
### 6.4 Privacy
- **What reaches the receipt**: the number of logs, the digest of the document, and the cell
of §4.7, suppressed below 50 sessions. Nothing per session.
- **What never leaves the session and the room's service**: the observations themselves,
their `sid`, their timestamps, the objects, URLs and timings they name, and each session's
metrics. A client sends them only to the room's own service (delivery-telemetry §8.1,
§8.3); a relay carries a CMCD track only to the subscribers the room's service names and
never fans it out to the room's participants. The digest in the receipt commits to the
document without revealing it: a party holding the document can check the cell, and one
that does not learns the cell alone.
- An observation names no principal; a client's `sid` is fresh per session and derived from
no identifier, so the logs of one viewer in two sessions do not link, and a log cannot be
matched to a grant. The cell is not per grant and not per layer class, so no cell is
smaller than the session.
### 6.5 Aggregated
A room's clients **MAY** report their sessions' quality of experience as the measures of
[telemetry-aggregation](telemetry-aggregation.md) §3 instead of as logs: each client
secret-shares each session's measurement between the room's two aggregators under DAP
(`draft-ietf-ppm-dap-20`), and the room's service receives aggregates alone. The room's tasks
name the room id as their scope (telemetry-aggregation §4.1). The room-session receipt then
states no logs and a suppressed cell (§4.7), and the room's cell is computed, field by field,
from the accepted aggregate receipts of the room's `outcomes`, `stalls` and `durations` tasks
over one batch (telemetry-aggregation §9.2): the figures §4.7 computes from logs, without the
room's service holding any session's. What §6.4 says of logs then holds of the aggregators,
each of which holds one share of each measurement and learns nothing from it alone.
## 7. C2PA
A room-session receipt maps onto C2PA 2.4's `c2pa.environmental-sustainability` assertion
(§18.26) in a manifest of an asset of the session, such as its recording
([live-recording](live-recording.md)) or its composite, under
[energy-binding](energy-binding.md)'s rules:
- **Energy.** The standard assertion holds one figure, `energy_kwh`. It is written only when
the receipt's energy is whole (§4.5) and every hop entry is of one class: then
`energy_kwh` is that class's total, derived (1 µJ is 2.8 × 10^-13 kWh, energy-binding §4),
and `measurementMethod` names this extension, the class and the hop count. With several
classes or an unmetered hop, no standard assertion is written: one figure would sum
classes or present a part as the whole.
- **GHG and water.** `carbon_kgco2e` and `water_litres` are written only when the emitter's
caller supplies them, each with the method that gave it, which the companion carries as
`carbonMethod` and `waterMethod`; the standard assertion is then written whether or not it
holds `energy_kwh`. The receipt holds neither figure, and an emitter **MUST NOT** derive
them from the energy and a grid or water factor (energy-binding §4).
- **The companion.** Beside it, never instead, the emitter writes the companion assertion
`science.transaction.wai.live.metering`: the receipt's digest, the room id, the session
span, the coverage, `unmetered`, the totals by class label, the units, and the number of
grants with the consumption root. It is written whether or not the standard assertion is,
since it is the only place the classes are stated. A reader refuses a standard assertion
with no companion, and an `energy_kwh` beside a companion that states several classes or
a partial total.
The reference implements this in `c2pa_emit` (feature `c2pa_emit` with `live_metering`):
`SessionManifest` writes the assertions into a real signed C2PA manifest of a given asset,
with the session's recording or the receipt as its parent ingredient and the action
`c2pa.converted`, every assertion created; `recover_session` reads them back, applying the
reader's rules above, and `RecoveredSession::describes` checks them against the receipt.
## 8. The reference relay
`joule-moq-relay` keeps a signed session summary of every MOQT session it serves, and signs a
per-hop record of a room from those summaries (`metering::hop_record`): for each track of the
room its sessions carried, the objects and bytes in and out, the drops by reason with the
receipts forwarded, its subscriber sessions' time by layer class from the subscriptions each
held, the events it delivered, and a ledger from the room grants its tokens map to. It signs
version 2 when it keeps a DVR store, with that store's figures (§3.9), and version 1
otherwise. Its own energy is the unmetered marker, since no meter backs it. It connects to
no other hop, so every session it serves that subscribes is a subscriber session. Its record
recomputes from its summaries, which its own test checks, and the corpus holds a record it
signed in that test (§10).
## 9. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `session_invalid` | §4.2, §5.1 step 2 |
| `signer_not_authorised` | §4.3, §5.1 step 3 |
| `signer_revoked` | §4.3, §5.1 step 3 |
| `record_missing` | §5.1 steps 4 to 7 |
| `record_too_large` | §3.7, §5.1 step 4 |
| `energy_invalid` | §3.6, §4.2 |
| `record_invalid` | §3.6 |
| `hop_not_authorised` | §5.2 |
| `hop_revoked` | §5.2 |
| `span_outside_session` | §5.1 steps 4 and 6 |
| `track_unknown` | §5.1 step 4 |
| `ledger_invalid` | §3.6 |
| `ledger_mismatch` | §5.1 step 4 |
| `unit_mismatch` | §4.4, §4.5, §4.7, §5.1 |
| `classes_summed` | §4.2, §4.5 |
| `ceiling_mismatch` | §4.6, §5.1 step 11 |
| `qoe_invalid` | §6.1 |
| `qoe_not_suppressed` | §4.7 |
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version` for a
record or a receipt that does not read; `bad_key` and `bad_signature` for their signatures;
`wrong_room` for one of another room; every code a room verifier raises (live-rooms §11 and
the live-events codes it lists), at `room`; `lifecycle_incomplete` (live-compose §10), at
`room`; capture-binding §12's codes for a take, at `take`; and live-compose §10's for a
composition receipt, at `compose`.
## 10. Conformance
[`live-conformance/metering/`](../live-conformance/metering/) holds the corpus, in one file,
`vectors.json`, written from fixed seeds by `wai_live_metering_vectors gen`, and the record
the reference relay signed in its test, `relay-record.wmh` with its ledger
`relay-ledger.wml`, which the generator reads. It has rooms, and **vectors**, each a room, a
receipt and what it names, with the outcome §5.4 gives them:
- **Accepted**: a lesson room with a relay, a compositor and two recorded takes, its receipt
whole; mixed acquisition classes (a relay's counter reading, a compositor's model, an ingest
gateway's estimate) with one total per class; a viewer's grant past its joule ceiling and
another past its funds ceiling, named in the receipt; a large audience, ten thousand
subscriber sessions whose relay record is the size of the lesson's, with a QoE cell of more
than fifty logs; a relay that keeps a DVR window of the composite and signs a version 2
record of what it stored, held, dropped and served from it; and the reference relay's own
record, signed in its test, in a receipt over the room its test publishes into.
- **Refused**: each reason of §9 at least once, each vector a control of one defect, and the
codes a receipt, a record, a take, a composition receipt and a room raise from the
extensions this one reads; among them version 2 records whose DVR figures exceed their
entries' (`record_invalid`) or whose length is past B₂(*M*) (`record_too_large`), and a
record of version 3 (`unknown_version`).
A conforming verifier gives every vector the outcome, the reason, the place and the facts
the corpus states. `wai_live_metering_vectors verify` and the test `tests/live_metering.rs`
check that the file regenerates byte for byte, that `wai-rs` gives every vector its outcome,
that every reason of §9 refuses a vector, and that §9 lists exactly the reference's codes.
`live-conformance/live_metering_verify.py` is the second implementation, written from this
text: it shares no code with `wai-rs`, verifies signatures with WAI's strict rule written
from RFC 8032 (`claim-conformance/_ed25519.py`), verifies each room, take and composition
receipt with the corpora's own second implementations (`live_rooms_verify.py`,
`capture_take_verify.py`, `live_compose_verify.py`), recomputes every unit, every total and
every grant's consumption itself, gives every vector the same outcome, and requires every
mutated copy of the corpus to fail.
## 11. Security and privacy considerations
- Every count a receipt states rests on a signature: a hop's on its record, a device's on
its take, a compositor's on its receipts, a principal's on its events. A meter that
inflates a unit is refused, since the verifier recomputes it; a hop that inflates its own
counts is caught only where another record contradicts it (an upstream relay's out against
a downstream one's in), and the receipt makes both comparable.
- A hop's figure is its own statement, and its class says how it was obtained
(energy-measurement §5): the receipt makes the figures attributable and their sums
recomputable, not true. An attributed share is an allocation by bytes and says so.
- The record's bound keeps what a hop sends upstream independent of the audience, so metering
a large audience costs a relay tree what metering a small one does.
- A ledger entry names a grant and its use; the ledger and the consumption entries are given
to the parties that verify the receipt, and each principal can be shown its own entry
alone, by its audit path. A grant names its principal (live-rooms §13), so a deployment
that publishes a receipt publishes its root and counts, not its ledgers.
- QoE observations are unsigned (delivery-telemetry §1.1); the receipt's cell is suppressed
below 50 sessions and never carries a session's data (§6.4). Aggregated (§6.5), the floor is
each aggregator's, and the service holds no session's figures.