open-standards/wai/extensions/live-metering.md
Transaction Science f631a7a4e7
Some checks failed
joule-moq-agent AI participants / joule-moq-agent · does this change touch it (push) Successful in 1m20s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 1m12s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 1m26s
wai-app and Create / wai-app · does this change touch it (push) Successful in 1m7s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 3m30s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m55s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 3m44s
wai-ledger service / does this change touch it (push) Failing after 59s
wai-ledger service / service · linux (push) Has been skipped
wai-ledger service / service · macos (push) Has been skipped
wai-quantum studio and desktop / does this change touch it (push) Failing after 5s
wai-sink-android native sink library / does this change touch it (push) Failing after 6s
wai-pack / wai-pack · does this change touch it (push) Failing after 50s
wai-quantum studio and desktop / studio · browser (push) Has been skipped
wai-quantum studio and desktop / desktop app · macos (push) Has been skipped
wai-quantum studio and desktop / desktop app · linux (push) Has been skipped
wai-quantum studio and desktop / desktop app · windows (push) Has been skipped
wai-sink-swift native sink package / does this change touch it (push) Failing after 5s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been skipped
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been skipped
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been skipped
WAI operation catalogue / does this change touch it (push) Successful in 2m40s
wai-pack / wai-pack · linux (push) Has been skipped
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m30s
wai-pack / wai-pack · linux-arm64 (push) Has been skipped
wai-pack / wai-pack · macos (push) Has been skipped
wai-pack / wai-pack · independent verifier (Python) (push) Has been skipped
wai-pack / wai-pack · the floor through a media prober (push) Has been skipped
wai-fleet service / does this change touch it (push) Successful in 2m27s
wai-pack / wai-pack · dependency licences (push) Has been skipped
WAI Studio / does this change touch it (push) Failing after 8s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been skipped
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been skipped
WAI Studio / Studio in WebKit · linux (push) Has been skipped
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 4m6s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 8s
WAI examples / WAI examples · does this change touch them (push) Successful in 11s
wai web player plugins / the browser run's room compiles · linux (push) Successful in 58s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 32s
wai tv web application / the tv application, built to its baseline and checked · linux (push) Successful in 5m12s
wai web player plugins / the plugins' core under Node · linux (push) Successful in 6m56s
wai-audit collector and player integration / collector · linux (push) Successful in 3m3s
wai-fleet service / service · linux (push) Successful in 2m22s
wai-quantum for JavaScript / javascript · linux (push) Successful in 6m54s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 16m49s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m25s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 8m44s
joule-code conformance gate / joule-moq-ingest, every target (push) Failing after 6m56s
joule-code conformance gate / joule-moq-relay, every target (push) Failing after 7m31s
WAI examples / WAI examples · portable · linux (push) Successful in 24m14s
joule-moq-agent AI participants / joule-moq-agent, every target (push) Successful in 5m45s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 6m55s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 5m28s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 16m10s
joule-moq-room room service / joule-moq-room, every target (push) Failing after 15m19s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 51s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 52s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m39s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m47s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Failing after 16m25s
wai-fleet service / service · macos (push) Successful in 1m25s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 20m13s
wai tv web application / the tv application in Chromium, Firefox and WebKit · macos (push) Successful in 21m9s
joule-moq-http in browsers / the egress and the protected room in Chromium, Firefox and WebKit · macos (push) Successful in 50m56s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 20m9s
wai web player plugins / the three players in Chromium, Firefox and WebKit · macos (push) Failing after 34m1s
Live metering: an egress is a hop of the room, of role 4, and its bytes are billed as the room's bytes out (D85)
waiTransport's wt §50.2 and §50.4. An egress signs a per-hop record of role 4 (live-metering §3.12):
what it received at the relay and what it answered 200 over HTTP, no subscriber time, no DVR
window, its ledger by the grant each request carried; key-log authority live/egress. A room-session
receipt of version 2 counts its bytes out beside the relays', so receipt-ledger tariff version 4
bills them at the room's price for bytes out: no new source kind and no new tariff version. Its
session at a relay is a subscriber session under its grant, not a hop link. energy-methods places
its figure in delivery_relay. The gateway signs its record (Gateway::hop_record), held to its own
trace in its test. Metering corpus: room lesson_egress, vector `egress` and seven planted faults;
live_metering_verify.py checks them and each of its five egress checks, dropped, is caught.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 08:28:28 -05:00

1041 lines
70 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, and in version 2 the bytes the hops sent), 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),
3 ingest gateway, or 4 egress, which serves the room's tracks over HTTP
([live-http-egress](live-http-egress.md); §3.12).
- **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, 4 egress (§3.12) |
| `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; an egress signs version 1 (§3.12).
### 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 to 4; `start` is below `end`;
- a record of role 4 (§3.12) is of version 1, its `sessions` is 0, and each of its out entries
has `dropped_timeout`, `dropped_closed` and `receipted` 0;
- 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. The boundary is
the record's **cut**: for each track, a Group ID, the first group of the record after it. A
record counts the objects of the groups below its cut that the record before it did not. A hop
that closes a span at an instant by itself cuts each track after the largest group whose first
object arrived at it before that instant, so an object is counted in the record whose span holds
the arrival of its group's first object at the hop.
Hops that carry a room together (§3.10) end each part of the session at **one** cut, so that a
group is counted in the same part at both ends of every link. Each hop closes its part at an
instant of its own and states the cut it would make by itself; a group whose first object arrives
at a hop after its instant waits for the cut. The cut they all apply is the greatest of theirs,
track by track. No hop has counted a group at or above it in the closing part, since each hop's
own cut is above every group that began there before its instant, and a hop refuses a cut below
its own for any track. Hops that each cut at their own instant count a group whose first object
arrived between their instants in different parts, and §3.10's chain fails across the cut, in
one part or the next, whichever hop cut first.
A record is signed once a span has closed, so a group can outrun its record. A hop closes a
record a grace after its span ends, and an object of a group whose record has closed is counted
in the record open when the hop counts it. An object a hop sends in answer to a FETCH or a fill
is counted in the record open when it is sent. What becomes of an object already counted, its
refusal at the door or its deduplication, stands in the record that counted the object, so a
record's drops are among its objects (§3.6).
### 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, but for `dvr_objects_stored` and `dvr_bytes_stored`: a group is counted
in the part it began in and written once it is whole, often after that part's span, so each part
states what the hop has written and no earlier part stated, up to what its own in entries count,
and a later part with objects in goes on from there.
### 3.10 A room across relays
Where a room is carried by several relays connected by hop links ([live-rooms](live-rooms.md)
§10.7), every relay that carried it signs its own record, by its own key, of the sessions it
held, and the records chain hop to hop:
- **A hop link is counted at both of its ends, in the track entries alone.** The relay that
sends a track's objects over the link counts them in its out entry of the track (§3.2), and
the relay that receives them counts them in its in entry, its door's refusals among them
(`dropped_refused`). Neither counts the link as a subscriber session (§3.3), and neither
enters it in its ledger (§3.4): the link runs under no grant of the room, and the principals
whose objects it carries are metered at the relays whose sessions they are. So each
subscriber is counted once, at the relay that serves it, and a grant's consumption is the sum
of what the relays whose sessions ran under it state (§4.6).
- **The records chain by track.** Of a track carried down a chain of relays to subscribers at
its end, the in entry of each relay after the first counts what the relay before it sent
over their link: a relay whose only out session on the track is the link to the next one
states an out entry the next one's in entry does not exceed, and the two are equal when
nothing was in flight as the link closed. A record that states more in than the hop before
it sent is the contradiction §11 describes.
- **Records in parts chain across their cuts.** Relays that each sign a session's records in
parts, a link between them held open across the parts, end every part at one cut (§3.8). The
in and out entries over the link then chain part by part, as they do for a link that closed,
and the link and the sessions it serves need not end where a part does.
- **Each relay's energy is its own figure.** A relay's `joules` and `class` are its own,
however many relays carry the session, and the receipt restates each with its class
(§4.5): the energy of a chain of relays is a list of figures, one per hop, and is summed
only within a class.
A relay that peers names itself to the next by the key its record names as `hop`, so the
relays a request passed through are the hops whose records the receipt names.
### 3.11 A hop that serves several rooms
A hop's counter reads the hop's process or its package, not one room (§8). Where the hop served
other work in a record's span besides the room (another room's sessions, tracks of no room), its
figure over the span is not the room's, and a record **MUST NOT** carry it whole under a hardware
class. It **MAY** carry the room's **share** of it, under `ModelBased` (energy-measurement §2,
tag 2, no evidence): a share is the hop's figure divided by a model, not a reading of what the
room cost, so it is never stated under the counter's class, and §4.5 keeps it apart from measured
figures. A hop that states shares states, beside the record, the rule it divides by, its figure
over the span with its class and evidence, and each room's measure and share, so that its
operator recomputes every share; the shares of one span sum to no more than the figure, a room
with no session in the span has none, and a share that rounds to nothing, or of a span with no
figure, is the unmetered marker, never 0 µJ. As an attributed share of §4.6 is, a share is an
allocation of the hop's figure: a room that moved less is allocated less of the same figure, and
does not lower the hop's energy by the difference. The reference relay's rule is §8's: by the
payload bytes each room's sessions moved over a window (WAI decision D79).
### 3.12 An egress
An egress ([live-http-egress](live-http-egress.md)) serves a room's tracks over HTTP to clients
that hold no session it can time: a request is answered and the exchange is over, and a player
that stops asking is not seen to stop. Its clients acknowledge nothing either, so no settlement
statement states what it delivered (settlement §9.1). It is a hop of role 4, and its record is
what states it: one record of version 1 per room session, as any hop's (§3.8), which counts:
- **In**: what it received of the room's tracks over its session at a relay (§3.2), each media
track's entry its own and the other tracks together, its `dropped_refused` the objects it
withheld (live-http-egress §3.3), which it never serves;
- **Out**: what it answered 200 over HTTP. A media track's out entry counts the objects whose
bytes a part or a segment it answered carried, each time it carried them, and those bytes: a
part carries one object's, and a segment its parts'. Every other resource it answers (the
playlists, the MPD, the headers, the receipt files, the parts lists and the runs) is counted
in the out entry of kind 3, each response as one object, with the bytes of its body. Each out
entry's `dropped_timeout`, `dropped_closed` and `receipted` are 0;
- **No subscriber time**: `sessions`, `subscriber_us` and `events` are 0, since its clients are
not sessions it can time;
- **Its ledger**: an entry for each grant its requests were answered under (a request carries
the grant its token maps, live-rooms §10.3, or none), the zero grant for a request under none;
each entry's `sessions`, `bytes_in` and `subscriber_us` are 0, and its `bytes_out` is the bytes
of the bodies it answered under that grant, out entries of every kind together.
A response is counted in the record open when the egress answers it, a body sent as it grows (a
segment, live-http-egress §6.3) when its response ends. A request that a deployment admits by
something other than a grant's token is under the zero grant here: who may fetch from an egress
is the deployment's (live-http-egress §1), and this record states bytes, not who fetched them.
**An egress's session at a relay is a subscriber session.** An egress subscribes to the room's
tracks under a grant of the room, as a viewer does, and the relay counts its session as it counts
a viewer's (§3.3): its time by layer class in the relay's record, and the bytes the relay sent it
in the relay's ledger, under the egress's grant. It is not a hop link (§3.10), which joins two
relays of the room under no grant. So a room's `bytes_out` (§4.4) holds both deliveries, each
once: the relay's to the egress, which does not grow with the egress's audience, and the
egress's to its clients, which does. A deployment that sets the egress's grant sees the first as
that grant's consumption (§4.6), apart from every viewer's.
## 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; 2 for a receipt that states the bytes its hops sent and prices them (§4.4, §4.6) |
| `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, in version 2 128 | fifteen u64: `publisher_us`, `subscriber_us` for layer classes 0 to 8, `sessions`, `composition_us`, `events_sent`, `events_delivered`, `recorded_us` (§4.4); in version 2 a sixteenth, `bytes_out` |
| `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, in version 2 3 + 56 | `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); in version 2 a fourteenth, per 10^9 bytes of `bytes_out` |
| `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 |
Version 2 is version 1's fields in the same order, with `units` and `tariff` each one figure
longer, the figure last. A reader of version 1 cannot read a receipt of version 2, and a
receipt of version 1 reads, verifies and prices exactly as before: version 2 is a version of
its own, as a per-hop record's version 2 is (§3.1).
### 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
to 4, 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, the price of bytes among them; 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.
- **`bytes_out`**, in a receipt of version 2: the sum over the hop records of their ledger
entries' `bytes_out` (§3.4), the payload bytes the hops sent the room's sessions. What
delivering a session costs scales with the bytes carried to it as well as with the time it
ran: a viewer of a high layer and one of a low layer subscribe for the same seconds and are
sent different bytes. The ledgers count exactly the sessions that ran under a grant or
under none, so the sum of the grants' `bytes_out` (§4.6) is the receipt's. An egress's
ledger states the bytes it answered over HTTP (§3.12), so they are counted here beside the
relays', at the same price. A hop link's
bytes are in the relays' track entries and in no ledger (§3.10): carriage between the
relays of a room is how its operator built the room, not a session's delivery, and is not
counted here.
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. The allocation by bytes is an accounting convention: a hop's energy over
its span is largely what running the hop costs, not a cost per byte, so a grant that moves
fewer bytes is attributed a smaller share of the same figure and does not lower the hop's
energy by that share, and nothing here states what the network between hops spent;
- `bytes_out`, in a receipt of version 2: the sum of its ledger entries' `bytes_out` over
the hop records;
- **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). In a receipt of
version 2 they are ⌈(1000 × *N* + `bytes_out` × *p*<sub>bytes</sub>) / 10^15⌉ minor units,
*N* in 10^-12 of a minor unit and *p*<sub>bytes</sub> per 10^9 bytes, so the time and the
bytes are rounded up once, together; with a price of bytes of 0 they are version 1's.
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 joule ceiling is held against the
attributed shares, so it caps what is accounted to a grant; it is not a statement that the
grant's use changed by that much what the hops, or the network, spent.
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). In a receipt of version 2 it is 206 bytes: its
`bytes_out` (8) follows `events_sent`. 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.
A hop may hold a grant to its joule ceiling itself, on its own records. The shares above that
its own metered records attribute to a grant are part of what this section sums over every hop,
so a grant whose shares at one hop pass its `joules_ceiling` is past it, whatever the other hops
state. Such a hop may end the sessions it holds under that grant and refuse the grant's next
session. That ends the hop's service to the grant, not the use: the principal is present until
the room's records say otherwise. A record with no figure attributes nothing, so a hop that reads
nothing ends nothing. A funds ceiling needs a tariff, which is the meter's.
### 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`;
each entry names the zero grant or a grant of the room, with that grant's ceilings
and currency; and, for a record of role 4, each entry's `sessions` and `bytes_in` are 0
(§3.12). 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`, `live/ingest` or `live/egress`, 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.
### 6.6 MoQ Feedback
A subscriber of a room's media track **MAY** publish MQF reports on a Feedback Track
(`draft-liu-moq-feedback-01`). What a consumer of those reports writes onward is
[delivery-telemetry](delivery-telemetry.md) §11's: stalls as the observations of §6.1, and no
figure an observation has no member for. Here:
- **The tally.** A consumer keeps, for the Feedback Track, each reported object's latest
status, and from them four counts: received, received late, not received, partially
received. A later report's status for an object replaces an earlier one's, as the draft
lets a status change.
- **Against the hop.** The serving hop's record counts what it sent on the track: the
`objects` of the track's `out` entry (§3.2), over every subscriber. One subscriber cannot
have received more of the track than the hop sent: a tally whose received, late and partial
objects together exceed that count is refused, `more_than_sent`. The check bounds a report;
it proves nothing of delivery, since a report is unsigned and the hop's count is of all its
subscribers.
- **Privacy.** A tally is per subscriber. It stays where a session's observations stay (§6.4):
with the hop that served the subscriber and the room's service. It is in no record and no
receipt, and the room's cell (§4.7) is computed from session logs or aggregates as before.
A relay does not fan a Feedback Track out to the room's participants: a HOP_FEEDBACK track
ends at the adjacent publisher, and an ORIGIN_FEEDBACK track goes only toward the peer its
media track came from (the draft's §4.3).
The reference computes the tally and the check in `joule-moq`'s `feedback` module; the corpus
is `telemetry-conformance/moq-feedback/`. The reference relay keeps the tally of each hop
Feedback Track it consumes and checks it against its own count for that session and track
([delivery-telemetry](delivery-telemetry.md) §11, its paragraph on a consumer in service).
## 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
the same record over a window of sessions still open, from each session's counts in the window
(`Relay::window_record`, §3.8), with no session ended. Relays that carry a room together close a
window at one cut: each closes it and states its own cut (`close` on its operator's surface), the
operator joins them, and each seals at the join (`seal`), refusing a cut below its own; its test
holds a core's and an edge's records of their link to each other across two windows, and an
edge that seals at its own cut is caught. It states a sealed window's counts by session to its
operator, signed with its key (`window`), and its operator checks that the room's record over the
window recomputes from them. It can keep the counts of the window open now on disk
(`control.checkpoint_path`), written whole at a stated interval, so that a relay killed outright
loses at most that interval of its open sessions' counts: started again, it reads the window back
under the same index, and its statement of that window marks it `resumed`, with the span from
the checkpoint to the restart stated as unmetered. No count of that span is stated as zero, and
the room's record over the window carries the unmetered marker for its energy; a checkpoint that
is truncated, another relay's, or of a window already sealed is refused with its reason and
counted nothing, so nothing is counted twice. 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 read from a
counter the host keeps, sampled every 100 ms while the relay runs: on macOS the relay process's own CPU energy (`ri_energy_nj`, unprivileged;
scope **task**: every thread of the relay and nothing else), and on a Linux host the
operator states runs the relay alone, the CPU packages' RAPL energy (scope **package**). A
record carries the counter's advance over its span as `OnChipCounter` (tag 4), filtering
state `Unknown`, `window_us` the span, and a declared uncertainty covering the interpolation
at each end of the span, the counter's lag at each end, for the package counter the noise
its vendor states for a filtered counter at that window, and the rounding to whole
microjoules; and only where the counter was live across the span (energy-measurement §3.1)
and the relay served that room alone in it. Over a window it shared with other work, a room's
record carries the room's share of the relay's figure over the window (§3.11, D79): with *J* the
counter's advance across the window's span, *B* the payload bytes every session of the window
moved in and out on every track, and *b* the bytes the room's sessions moved on its tracks (its
track entries' bytes), ⌊*J* × *b* / *B*⌋ µJ under `ModelBased`; the unmetered marker where
the room moved no bytes, the share rounds to nothing or no session moved a byte; and nothing for
a room no session of the window touched. Its signed statement of the window states the rule
(`wai:relay-room-shares/bytes/1`), *J* with its evidence or why there is none, *B*, and each
room's bytes, what its record carries and the microjoules, and its operator recomputes each share
from the statement's own counts by session. A record over a span of ended sessions, not a window,
carries a figure only where the relay served the room alone. Otherwise, and where the host
offers no counter or the configuration turns it off, the record carries the unmetered marker. A
session's own summary carries the unmetered marker always, since sessions run together in one
process and the counter cannot tell their shares apart. A session another relay holds with it, a hop
link it accepted or one it dialed as its upstream (live-rooms §10.7), is counted as §3.10
counts one; every other 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 with its meter off (§10), so that its bytes do not depend on the host;
its cascading test signs a record at each of three relays in a chain and checks that each
track's in and out entries chain from hop to hop, that only the viewers at the chain's ends
are subscriber sessions, and that the middle relay's ledger is empty.
The reference egress, `joule-moq-http`, signs a record of role 4 (§3.12) of what it carried
(`Gateway::hop_record`), over a span its caller states: the objects it received of the owner's
tracks, each media track by its digest and the receipts tracks, events and catalogs together,
its withheld objects among them as refused; and the bodies it answered 200, each part and
segment by the objects it carries and every other resource together. It reads no token of a
request, so its ledger is the zero grant's one entry. Its energy is the unmetered marker, as a
relay's session summary's is. Its test holds the record to the gateway's own trace: the bytes
out are those of every body its trace records answered 200, and its media objects in are the
objects its trace records arriving.
## 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; the same session in a receipt of version 2, its bytes out summed from the ledgers,
by grant too, and priced in each grant's funds; 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; the reference relay's own
record, signed in its test, in a receipt over the room its test publishes into; and an
egress serving the lesson over HTTP (§3.12), in a room whose key log lists it with
`live/egress`: its record of role 4, its ledger by the grant each request carried, and a
receipt of version 2 whose bytes out count its bytes beside the relay's.
- **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`); and receipts of version 2 stating a byte more than
the ledgers (`unit_mismatch`), whose grants' entries leave their bytes out
(`ceiling_mismatch`), or pricing bytes with no tariff (`session_invalid`), and a receipt of
version 3 (`unknown_version`); and an egress's record that states subscriber sessions, a
DVR window or drops on its way out (`record_invalid`), a ledger entry of an egress's that
counts sessions or bytes in (`ledger_mismatch`), a record of role 4 signed by a key whose
entry does not hold `live/egress` (`hop_not_authorised`), and a receipt whose bytes out leave
the egress's out (`unit_mismatch`).
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; and with each check of §3.12 dropped in turn, requires the
corpus to catch it.
## 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.