open-standards/wai/extensions/live-metering.md
Transaction Science f8b199a657
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
live-metering §3.1: the version 2 sentence cites live-recording §8 in prose
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]>
2026-10-20 04:06:13 -04:00

50 KiB
Raw Permalink Blame History

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, 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/, 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 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 §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 §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 §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 §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 §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 §7.1), strictly ascending
compose 4 + 32 u32: the composition receipts it aggregates (live-compose §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 tj+1 − tj 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 Σc ∈ S Tc for some set S of the receipt's classes that holds X and another, where Tc is the sum of the figures of class c, and does not equal TX. 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 × ppub + Σc subscriber_us[c] × psub,c + composition_us × pcomp + recorded_us × prec + events_sent × pev × 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 §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) or its composite, under energy-binding'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/ 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).