WAI Extension: Live Metering
Mirrored from the canonical text at commit f871af55 ().
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’sc2pa.environmental-sustainabilityassertion. No new transport. The reference implementation is thelive_meteringmodule ofwai-rs(featurelive_metering); the reference relay,joule-moq-relay, signs a per-hop record from its session summaries; the corpus islive-conformance/metering/, and its second implementation islive-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-00anddraft-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,videoorscreen, and whose name is a layer from0to7; an events track one whose namespace endseventsand whose name is0(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’sMTHwith 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
joulesis 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_ppmandwindow_usare 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:
roleis 1, 2 or 3;startis belowend;- the track entries are in strictly ascending order of (
kind,track,dir), eachkind1 to 3 and eachdir1 or 2; a media entry’strackis not zero and another entry’s is; - an entry in has
dropped_timeout,dropped_closedandreceipted0, anddropped_refused+dropped_duplicateat mostobjects; an entry out hasdropped_refusedanddropped_duplicate0, andreceiptedat mostdropped_timeout+dropped_closed; - with
sessions0, everysubscriber_usandeventsare 0; ledgeris 32 zero bytes exactly whengrantsis 0;- in a version 2 record (§3.9),
dvr_objects_outis at most the sum ofobjectsover the out entries, anddvr_bytes_outat most the sum of theirbytes;dvr_objects_storedis at most the sum ofobjectsover the in entries, anddvr_bytes_storedat most the sum of theirbytes.
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:
startis belowend;- each list is in the strictly ascending order §4.1 gives it; each hop entry’s
roleis 1, 2 or 3, and its class reads and meets §3.6’senergy_invalidrules; each total’stagis 2 to 5 and itsfiguresabove 0; compose’s digest is zero exactly when its count is, and so isqoe’s;- with no tariff (
currencyzero) every price is 0; otherwisecurrencyis three capitalsA–Z; - each past grant’s
flagsis 1, 2 or 3; suppressedis 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:
- 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. - 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.
- 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_usfor layer classes 0 to 8, andsessions: the sums over the hop records of theirsubscriber_usand theirsessions(§3.3).composition_us, composition-microseconds: over each composed track, the receipts of one compositor, one media kind and one output track, sorted byt: 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, whosetis in the session: every principal’s, the room keys’ lifecycle events included.events_delivered: the sum over the hop records of theirevents(§3.3).recorded_us, recorded-microseconds: over each take, the committed capture: with o the offset of its first held commitment and s thesizeof 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.
- o,
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’st;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_outof 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
currencyis the grant’s: ⌈N / 10^12⌉ minor units of it, with N =publisher_us× ppub + Σcsubscriber_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:
- The room (live-rooms §9): its refusal, at
room, with the room’s own place. A gap in a room key’s chain islifecycle_incomplete(live-compose §4.1), atroom: 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. - The receipt (§4.2), at
receipt:truncated,malformed,unknown_version,classes_summed,session_invalid;wrong_roomwhen itsroomis not the room id;bad_key,bad_signature. - The signer (§4.3), at
receipt:signer_not_authorised,signer_revoked. - Each hop entry, in order, at
hopand the entry’s index:- a record whose digest is the entry’s
recordis given, with a ledger: otherwiserecord_missing. (The digest of given bytes is SHA-256 over all but their last 64.) - Its length is at most the bound of its version, read from its fifth byte (§3.7):
otherwise
record_too_large. - It reads as a record of version 1 or 2 (§3.6):
truncated,malformed,unknown_version,energy_invalid,record_invalid; itsroomis the room id (wrong_room); its signature verifies (bad_key,bad_signature). - The entry’s
hop,role,joulesandclassare the record’s: otherwiseunit_mismatch. - The hop may sign for its role (§5.2):
hop_not_authorised,hop_revoked. - Its span is within the session:
start≤ itsstartand itsend≤end. Otherwisespan_outside_session. - Each media entry’s
trackis a track the room published in the session (§5.3): otherwisetrack_unknown. - Its ledger reads (§3.6,
ledger_invalid); hasgrantsentries and the rootledger; and each entry names the zero grant or a grant of the room, with that grant’s ceilings and currency. Otherwiseledger_mismatch. - Its
subscriber_usare the sums of its ledger’s, class by class: otherwiseunit_mismatch.
- a record whose digest is the entry’s
- Each take, in the receipt’s order, at
takeand 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. - The composition receipts, at
compose: the receipts given are as many ascomposestates and have its list digest, otherwiserecord_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 itstis in the session (span_outside_session). - The logs, at
qoe: whenqoestates 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 asqoestates (unit_mismatch). - The units (§4.4), at
units:unit_mismatch. - The energy (§4.5), at
energy:unmeteredis 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 isclasses_summedwhen the sum it states is one §4.5 describes andunit_mismatchotherwise, and states that class’s number of figures; and the totals name every class of the metered entries. Otherwiseunit_mismatch. - The QoE cell (§4.7), at
qoe:qoe_not_suppressed, thenunit_mismatch. - The ceilings (§4.6), at
ceilings: the number of grants, the consumption root and the grants past a ceiling recompute; otherwiseceiling_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 tolayers− 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(itse=rr, which that draft’s §3 bars) and apresentthat is not the session’s first are not carried; - the record’s wallclock index
Tis the report’sts, andtsis not in itsdata(that draft’s §4.2.1); dataholds the report’s members that the draft’s §3 permits and the catalog’scmcdConfigenables,valways, 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.sfand 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
sidis 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: thenenergy_kwhis that class’s total, derived (1 µJ is 2.8 × 10^-13 kWh, energy-binding §4), andmeasurementMethodnames 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_kgco2eandwater_litresare written only when the emitter’s caller supplies them, each with the method that gave it, which the companion carries ascarbonMethodandwaterMethod; the standard assertion is then written whether or not it holdsenergy_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 anenergy_kwhbeside 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).