All checks were successful
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 2m1s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 2m3s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 8s
WAI conformance / does this change touch it (pull_request) Successful in 9s
WAI energy receipts / does this change touch it (pull_request) Successful in 6s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 7s
wai-web wasm32 / does this change touch it (pull_request) Successful in 8s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 54s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 23s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 15s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 26s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m41s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m29s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 10m1s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m16s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 18m47s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 23s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m10s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m2s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 22m44s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 6s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m44s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 5s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m16s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m6s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 27s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 8m36s
wai-pack / wai-pack · linux (pull_request) Successful in 2m7s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 9m2s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 3m30s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 19m33s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 14m33s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 19s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 43s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 4m38s
wai-pack / wai-pack · macos (pull_request) Successful in 1m4s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m44s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m30s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 4m50s
The link after the table's [start, end) interval read, to the section reference check, as one link text reaching back to energy-measurement §2.7; and "keeps none signs" matched the public-text check's "none signs". Co-Authored-By: Claude Opus 5.5 <[email protected]>
778 lines
50 KiB
Markdown
778 lines
50 KiB
Markdown
# 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 and what of them reaches the receipt (§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).
|
||
|
||
## 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.
|
||
|
||
## 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).
|