The change of wai/ad-consent-supply-chain (44d5f2264..110c53553, 2 commits), on the fix branch: its changelog entries as files of wai/changes/, its wai-conformance.yml edits as step files, corpus files and test tags, and every other file as the branch has it. - f8fe39cba WAI: an ad decision's consent signal and supply chain, bound into what the decider signs - 110c53553 WAI: a GPP string links what carries it; a chain per impression; sellers files read exactly Co-Authored-By: Claude Opus 5.5 <[email protected]>
1031 lines
65 KiB
Markdown
1031 lines
65 KiB
Markdown
# WAI Extension: Live Ads
|
||
|
||
> Status: Draft. Registers the record `wai.live.ads`: server-guided ad insertion for rooms.
|
||
> The room authority signs an **ad policy**, `WAP1`, naming the grant its ad decider runs
|
||
> under and how the room's breaks are presented. The breaks themselves are the room's signed
|
||
> `cue` events ([live-events](live-events.md) §4.3), an out and its in, or a break of known
|
||
> duration, with a post-roll marked by a **break annotation**, `WAB1`, in the cue author's
|
||
> chain. Each break maps, deterministically, onto one interstitial date range of HLS and one
|
||
> alternative-presentation event of DASH, so every edge writes the same playlist and MPD bytes
|
||
> and the manifests stay shared and cacheable. Per-viewer decisioning happens behind the one
|
||
> URI each break names: the **asset list** a player fetches when it reaches the break, filled
|
||
> by `wai.video.ssai`'s deterministic decision. Each viewer's impressions are sealed in a
|
||
> `wai.video.ssai` receipt of its **break form**, which names the break, the decision's
|
||
> inputs and the asset list served, and a verifier recomputes the decision, the asset list and
|
||
> the spend against the decider grant's funds ceiling. A policy of version 2 can require that
|
||
> each impression count only when the player's signed presentation claim verifies
|
||
> ([ad-presentation](ad-presentation.md)), and the spend is then held to the ceiling counting
|
||
> only those impressions. No new transport. The reference
|
||
> implementation is the `live_ads` module of `wai-rs` (feature `live_ads`) with the break form
|
||
> in `wai_video_ssai`; the corpus is [`live-conformance/ads/`](../live-conformance/ads/), and
|
||
> its second implementation is `live-conformance/live_ads_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: `draft-pantos-hls-rfc8216bis-22` (1 May 2026), its §4.4.5.1
|
||
> (`EXT-X-DATERANGE`), §6.2.1, §6.2.4, Appendix D (Interstitials) and Appendix F (Preloading
|
||
> HLS Date Range Resources). Its Appendix H is a glossary and defines no date-range schedule,
|
||
> so nothing here maps onto it. ISO/IEC 23009-1:2026, the sixth edition, through the XML
|
||
> schema that accompanies it (its types `EventType`, `AlternativeMPDEventType`,
|
||
> `AlternativeMPDReplaceEventType`, `ImportedMpdType` and `PresentationType`) and the schema's
|
||
> example MPDs G23, G29-1 and G29-2, at the schema's revision of 2026-06-13; the clause text of
|
||
> its §5.16 was not available to this revision (§6.1). A section number of one of these texts
|
||
> is that revision's.
|
||
>
|
||
> A receipt of version 2 also binds the **request terms** its decision was made under: the
|
||
> consent signal the player gave, a Global Privacy Platform string with the sections that apply
|
||
> and a child-directed flag (§8.6), and the supply chain each of its impressions was sold through,
|
||
> each seller named by its advertising system's domain and its seller id, with references to each
|
||
> system's sellers file and to the inventory owner's authorised-sellers file (§8.7). WAI carries
|
||
> the signal and the chain as the texts below define them, binds them into what the decider
|
||
> signs, and defines no scheme of its own beside them. A policy of version 3 requires every
|
||
> receipt to state its consent signal, child-directed in a room the policy marks as directed to
|
||
> children (§3.1, §9.3 step 13).
|
||
>
|
||
> Texts read for §8.6 and §8.7, each at its source: *Global Privacy Protocol String*, version
|
||
> 1.0, its revision of 3 November 2023: its sections on what the string serves and holds, who
|
||
> makes it, its format, header, delimiters and section encoding, and how a service reached by a
|
||
> URL is given it. *OpenRTB API Specification* Version 2.6: §3.2.2 (`Source`), §3.2.3 (`Regs`),
|
||
> §3.2.25 (`SupplyChain`) and §3.2.26 (`SupplyChainNode`). The *OpenRTB SupplyChain object*, in
|
||
> full. *Sellers.json*, version 1.0, July 2019, in full. *ads.txt Specification* Version 1.1,
|
||
> August 2022: §3 to §5. *Authorized Sellers for Apps (app-ads.txt)*, version 1.0, March 2019:
|
||
> its abstract, its introduction, and its sections on publishing the file and on translating an
|
||
> app's developer URL into the file's path.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the ad policy, its signature and rules (§3); the room's breaks, derived from its
|
||
`cue` events and break annotations, with their identity and refusals (§4); their HLS
|
||
interstitial date ranges (§5) and their DASH alternative-presentation events (§6); the asset
|
||
list, the decision that fills it, and a viewer joining a break already running (§7); the break
|
||
form of the `wai.video.ssai` receipt (§8); and verification end to end, from the room's events
|
||
to the spend (§9).
|
||
The request terms a receipt of version 2 binds, and what a player, a decider and a verifier do
|
||
with them, are in scope too (§8.6, §8.7).
|
||
|
||
Out of scope: a transport; how a decider assembles each viewer's candidate pod (targeting);
|
||
how beacons reach it; the SCTE-35 messages a cue may carry, which WAI carries verbatim and
|
||
never reads (live-events §4.3); the stitched form of `wai.video.ssai`, which is unchanged
|
||
(§8.1); and a pre-roll, which a room does not have: a viewer who joins a running break is
|
||
given what remains of it (§7.4), and one who joins between breaks starts with the program.
|
||
Out of scope, too: what a GPP string's sections state, which the GPP string's texts define and
|
||
WAI does not decode; whether a viewer gave a consent signal, or any party honoured one; and how
|
||
sellers files and authorised-sellers files are fetched and kept fresh, which their own texts
|
||
say.
|
||
|
||
Relationship to the core spec: `wai.live.ads` is a **record** ([SPEC](../SPEC.md) §5): a
|
||
signed record a WAI tool emits and verifies. It names no payload format and is not
|
||
dispatchable.
|
||
|
||
Relationship to [live-events](live-events.md): this extension changes nothing in it. A break
|
||
is opened and closed by `cue` events as they are; the break annotation rides in the `meta`
|
||
kind (§4.2 there), bound to its cue by the chain, as [timed-text](timed-text.md)'s
|
||
annotation is bound to its caption. Live-events' own date range of each cue (§8.3 there) is
|
||
written beside the interstitial date range of this extension, under another `ID` and another
|
||
`CLASS`.
|
||
|
||
Relationship to [live-rooms](live-rooms.md): a cue's author is held to its grant: a principal
|
||
whose grant in effect does not permit `cue` is refused by the room verifier,
|
||
`kind_not_permitted`, before this extension reads its cue (live-rooms §9.2 step 4). The
|
||
decider runs under a room grant, whose funds ceiling bounds the room's ad spend (§3.4), and
|
||
names it in its receipts as live-rooms §4.5 has every receipt name a grant.
|
||
|
||
Why a record of its own, beside `wai.video.ssai`: the policy, the breaks and their renderings
|
||
are verified against a room and its events track, which the stitched form never reads; the
|
||
impressions are `wai.video.ssai`'s, in a second form of its receipt (§8), so that one decision
|
||
rule and one spend rule serve stitched and server-guided insertion alike, and every receipt of
|
||
the stitched form keeps its bytes.
|
||
|
||
## 2. Terms
|
||
|
||
- **Room**, **room clock**, **principal**, **event**, **chain**, **grant**, **room key**,
|
||
**merged order**: as in live-events §2 and live-rooms §2 and §9.2.
|
||
- **Cue**: a `cue` event (live-events §4.3). Its **type** is out (1), in (2), chapter (3) or
|
||
break (4).
|
||
- **Break**: a span of the room's program that ads replace, opened by a cue of type out or
|
||
break (§4.3). A break is **open** from an out until its in, and **closed** otherwise.
|
||
- **Post-roll**: a break played after the program ends (§4.2).
|
||
- **Avail**: the duration, in milliseconds, a break's decision fills (§4.3).
|
||
- **Decider**: the principal that decides each viewer's fill and signs the receipts, under
|
||
the grant the policy names (§3.4).
|
||
- **Pod**: the candidate ads a decision runs over for one viewer and one break, each an
|
||
`ad_id`, a `creative_hash`, a `duration_ms` and a `price_micros` (§7.1).
|
||
- **Fill**: the ads a decision selects, in slot order.
|
||
- **Asset list**: the response to the request a player makes for a break: an HLS asset list
|
||
(§7.2) or a DASH list MPD (§7.3).
|
||
- **Session**: one viewer's playback session, named in a receipt by 16 bytes the decider
|
||
assigns (§8.2); under a policy that requires presentation claims, the 16 bytes the player's
|
||
session key names (ad-presentation §3), which the decider does not choose.
|
||
- **Consent signal**, **GPP string**: §8.6.
|
||
- **Supply chain**, **node**, **sellers file**, **authorised-sellers file**, **domain name**:
|
||
§8.7.
|
||
|
||
Integers in the records of §3 and §4 are little-endian, and a digest there is SHA-256 (FIPS
|
||
180-4), 32 bytes. Integers of the receipt's signed bytes (§8) are big-endian and its hashes
|
||
BLAKE3, as the stitched form's are. Ed25519 is RFC 8032's, verified strictly ([SPEC](../SPEC.md)
|
||
§7 *Signatures*): a key that fails step 1 or 2 is `bad_key`, and a signature that fails a
|
||
later step is `bad_signature`. A *long* field is a u16 length and that many bytes.
|
||
|
||
## 3. The ad policy
|
||
|
||
### 3.1 Layout
|
||
|
||
| offset | size | field | meaning |
|
||
|---|---|---|---|
|
||
| 0 | 4 | `magic` | `WAP1` (ASCII) |
|
||
| 4 | 1 | `version` | 1; 2 for a policy with presentation fields; 3 for one with presentation fields and an audience |
|
||
| 5 | 32 | `room` | the room id (live-rooms §3.2) |
|
||
| 37 | 32 | `decider_grant` | the digest of the room grant the decider runs under (§3.4) |
|
||
| 69 | 1 | `slots` | the most ads one break holds, 1 to 16 |
|
||
| 70 | 4 | `open_avail_ms` | u32: the avail of a break opened with no planned duration (§4.3); 0: such a break is refused |
|
||
| 74 | 4 | `preload_ms` | u32: how long before a break a player may fetch its asset list (§5.3, §6.2); 0: no preload |
|
||
| 78 | 1 | `restrict` | bit 0 **skip**: no seeking forward in a break; bit 1 **jump**: no seeking past a break unplayed |
|
||
| 79 | 2 | `skip_after_s` | u16: seconds of a break played before a viewer may skip the rest; 65535: no skip control |
|
||
| 81 | 1 | `snap` | bit 0 **out**: leave the program at the segment boundary nearest the break's start; bit 1 **in**: return at the one nearest its end |
|
||
| 82 | long | `asset_base` | the base of every asset-list URI (§7) |
|
||
| … | long | `creative_base` | the base of every creative's URI (§7) |
|
||
| … | 4 + long | presentation fields | version 2 and 3: `presentation`, `viewability`, `token_type` and `issuer`, as ad-presentation §8 defines them |
|
||
| … | 1 | `audience` | version 3 only: bit 0 **child**: the room is directed to children (§8.6) |
|
||
| … | 64 | `sig` | §3.2 |
|
||
|
||
A **base** is `https://` followed by 1 to 492 bytes, each of `!` to `~` (0x21–0x7E) other
|
||
than `"`, `#`, `&`, `'`, `<`, `>`, `?` and `\`, the last of them `/`. A base so holds no
|
||
character that HLS's quoted strings, JSON strings or XML attribute values would have to escape,
|
||
and a URI built on it (§7) is the base followed by a name of the break or of the creative.
|
||
|
||
A policy of version 3 carries the presentation fields as a policy of version 2 does, all four
|
||
zero when it requires no claim, and then its `audience`. It requires every receipt to be of
|
||
version 2, stating the consent signal its decision was made under, and with **child** set a
|
||
child-directed one (§9.3 step 13).
|
||
|
||
### 3.2 Digest and signature
|
||
|
||
The **digest** of a policy is SHA-256 over its record from `magic` through `creative_base`, or
|
||
through `issuer` for a policy of version 2, or through `audience` for one of version 3.
|
||
The **signature** is Ed25519 by the room descriptor's `authority` over the 20 bytes
|
||
`wai:live-ads-policy` (ASCII) and 0x01, followed by the bytes the digest covers.
|
||
|
||
### 3.3 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given as one policy, in the order of §3.1, and refuses
|
||
at the first of these that holds:
|
||
|
||
1. Fewer than 4 bytes: `truncated`. A magic other than `WAP1`: `malformed`. No byte after the
|
||
magic: `truncated`. A version other than 1, 2 or 3: `unknown_version`.
|
||
2. The bytes end before a field the layout requires: `truncated`.
|
||
3. Bytes after `sig`: `malformed`.
|
||
|
||
It then refuses as `ad_policy_malformed` a policy whose `slots` is not 1 to 16, whose
|
||
`restrict` or `snap` sets a bit above bit 1, whose `asset_base` or `creative_base` is not a
|
||
base (§3.1), of version 2 or 3, whose presentation fields break a rule of ad-presentation §8,
|
||
or, of version 3, whose `audience` sets a bit above bit 0.
|
||
|
||
### 3.4 The decider
|
||
|
||
The decider is the `principal` of the grant whose digest is `decider_grant`. That grant is one
|
||
of the room's grants, which the room verifier checks (live-rooms §4.3); it need not be used in
|
||
a `join`, since a decider sends no event. Its `funds_ceiling` and `currency` are the room's ad
|
||
budget: every price and every spend of §7 and §8 is in millionths of a minor unit of
|
||
`currency`, as [live-metering](live-metering.md)'s tariff prices are, so a spend of *s* is
|
||
within a ceiling of *c* minor units when *s* ≤ *c* × 10^6. A `funds_ceiling` of 2^64 − 1
|
||
states no ceiling. The decider's receipts name the grant as live-rooms §4.5 has a receipt name
|
||
one (§9.3).
|
||
|
||
A grant is good for a break when it is good at the break's `t` (live-rooms §4.3), and it is
|
||
not revoked through the authority's key log (live-rooms §5).
|
||
|
||
## 4. Breaks
|
||
|
||
### 4.1 Cues
|
||
|
||
A cue (live-events §4.3) carries a `type`, a `cue_id`, a `duration_ms` and, optionally, an
|
||
SCTE-35 message this extension never reads. Its `t` is the instant of the splice on the room
|
||
clock: a publisher **MAY** send a cue before that instant, and a cue anchored to a media object
|
||
(live-events §3.1) splices at that object. Every cue the room accepted, of any principal
|
||
permitted `cue`, the room keys among them, is read here, in the merged order (live-rooms §9.2),
|
||
each once.
|
||
|
||
### 4.2 The break annotation
|
||
|
||
A break is mid-roll unless its author annotates it as a post-roll. The **annotation** is the
|
||
payload of a `meta` event whose first four bytes are `WAB1`, by the same principal, at the
|
||
cue's `seq` + 1 and at the cue's `t`:
|
||
|
||
| offset | size | field | meaning |
|
||
|---|---|---|---|
|
||
| 0 | 4 | `magic` | `WAB1` (ASCII) |
|
||
| 4 | 1 | `version` | 1 in this revision |
|
||
| 5 | 32 | `cue` | the digest of the cue it annotates |
|
||
| 37 | 1 | `placement` | 1 mid-roll, 2 post-roll |
|
||
|
||
An annotation is refused `annotation_malformed` when it is not 38 bytes, its version is not 1,
|
||
or its `placement` is not 1 or 2. It is refused `annotation_misplaced` unless the event before
|
||
it in its author's chain, at `seq` − 1, is a cue of type break, at the same `t`, whose digest
|
||
is `cue`.
|
||
|
||
A **post-roll** is the break of an annotation of placement 2. It ends the program: after it,
|
||
the room has no break and no cue (§4.3, `cue_after_close`), and a room that closes ends with
|
||
it. A player plays it after the program has ended (§5.1, §6.2).
|
||
|
||
### 4.3 The schedule
|
||
|
||
The **schedule** is the room's breaks and chapters, in the order of the cues that open them.
|
||
Over the cues and annotations in the merged order, holding the open break (at most one) and
|
||
the end of the last closed break, a verifier refuses, at the record:
|
||
|
||
- any cue after a post-roll's annotation: `cue_after_close`;
|
||
- **out** (1): when a break is open, or `t` is before the end of the last closed break,
|
||
`break_overlap`. Otherwise it opens a break: its **planned** duration is `duration_ms`, and
|
||
its avail is `duration_ms`, or the policy's `open_avail_ms` when `duration_ms` is 0; with
|
||
both 0, `break_duration_unknown`;
|
||
- **in** (2): unless the open break was opened by an out of this `cue_id` and of the in's
|
||
author, `cue_in_unmatched`. The break's **duration** is ⌊(`t` − the out's `t`) / 1000⌋
|
||
milliseconds: when it is 0, `break_zero_duration`; when the in's `duration_ms` is not 0 and
|
||
is not the duration, `cue_duration_mismatch`. The break is then closed, and its end is the
|
||
in's `t`;
|
||
- **chapter** (3): a chapter of the schedule, its `t` and its `duration_ms` (0 when not known).
|
||
A chapter opens and closes nothing, and may fall inside a break;
|
||
- **break** (4): when `duration_ms` is 0, `break_zero_duration`; when a break is open, or `t`
|
||
is before the end of the last closed break, `break_overlap`. Otherwise it is a closed break
|
||
whose duration and avail are `duration_ms`, ending at `t` + `duration_ms` × 1000;
|
||
- an **annotation** (§4.2): read and placed, or refused; of placement 2, its break is the
|
||
post-roll.
|
||
|
||
A break open at the end is stated open: the room may still be live. A cue of the room that
|
||
does not read is the room's refusal, and an event with a gap before it is a delivery gap the
|
||
room states (§9.2 step 1).
|
||
|
||
### 4.4 A break's identity
|
||
|
||
A break is named by the digest of the cue that opens it. From that digest:
|
||
|
||
- its HLS **ID** is `wai-ad-` followed by the digest's 64 lowercase hexadecimal digits;
|
||
- its DASH **event id** is the digest's first eight bytes read as a big-endian u64, of which
|
||
the low 53 bits are kept, so that a player that holds an id as a double holds it exactly. A
|
||
receiver identifies a break by its digest; the id lets a player tell one event from another
|
||
in an EventStream, where a repeat of 53 bits among a room's breaks is possible but not
|
||
expected.
|
||
|
||
Every edge holding the room's events derives the same identity, and no other event of the room
|
||
has it: live-events' own date range of the cue has the digest alone as its `ID`.
|
||
|
||
## 5. HLS: interstitial date ranges
|
||
|
||
### 5.1 The interstitial tag
|
||
|
||
Each break is one Interstitial `EXT-X-DATERANGE` tag (`draft-pantos-hls-rfc8216bis-22`
|
||
§4.4.5.1, Appendix D.2), written into every Media Playlist of the room when its cue is known.
|
||
Its attributes are written in this order, separated by commas with no spaces, each only where
|
||
its row says:
|
||
|
||
| attribute | value |
|
||
|---|---|
|
||
| `ID` | quoted: the break's ID (§4.4) |
|
||
| `CLASS` | quoted: `com.apple.hls.interstitial`, the class Appendix D.2 defines <!-- allow-name: the interstitial date-range class, a protocol identifier --> |
|
||
| `START-DATE` | quoted: `t0` + `t` of the opening cue, as `YYYY-MM-DDTHH:MM:SS.mmmZ` in UTC, the milliseconds floored (live-events §8.3) |
|
||
| `CUE` | quoted: `ONCE` for a mid-roll, `POST,ONCE` for a post-roll (§4.4.5.1) |
|
||
| `DURATION` | a break of type 4: its duration as decimal seconds with three digits after the point (`30.000`) |
|
||
| `PLANNED-DURATION` | an out with a planned duration: that duration, as `DURATION` |
|
||
| `X-ASSET-LIST` | quoted: the break's HLS asset-list URI (§7.2) |
|
||
| `X-RESUME-OFFSET` | a mid-roll of type 4: its duration, as `DURATION` |
|
||
| `X-PLAYOUT-LIMIT` | a break of type 4: its duration, as `DURATION` |
|
||
| `X-SNAP` | when `snap` is not 0: quoted, `OUT`, `IN` or `OUT,IN` by its bits |
|
||
| `X-RESTRICT` | when `restrict` is not 0: quoted, `SKIP`, `JUMP` or `SKIP,JUMP` by its bits |
|
||
| `X-SKIP-CONTROL-OFFSET` | when `skip_after_s` is not 65535: `skip_after_s` in decimal (Appendix D.3) |
|
||
|
||
- `ONCE` keeps a viewer who seeks back over a break in a window of the room from being shown
|
||
it again; `POST` plays the post-roll once the program has played to its end, which for a
|
||
room is its playlist's `EXT-X-ENDLIST`.
|
||
- `X-RESUME-OFFSET` equal to the break's duration returns the viewer to the program where the
|
||
break ends on the room's timeline, whatever the fill's length, so the program a viewer
|
||
returns to is the program after the in. A post-roll has nothing to resume, and no
|
||
`X-RESUME-OFFSET`.
|
||
- An out whose end is not yet known has no `DURATION`, `X-RESUME-OFFSET` or
|
||
`X-PLAYOUT-LIMIT`: the closing tag adds them (§5.2).
|
||
|
||
### 5.2 The closing tag
|
||
|
||
When an out's in closes it, the break gains a second tag with the same `ID`, as §6.2.1 has a
|
||
server signal the actual end of a date range with a planned duration, and as §4.4.5.1 lets a
|
||
later tag add attributes to a date range:
|
||
|
||
| attribute | value |
|
||
|---|---|
|
||
| `ID` | quoted: the break's ID |
|
||
| `CLASS` | quoted: as §5.1 <!-- allow-name: the interstitial date-range class, a protocol identifier --> |
|
||
| `DURATION` | the break's duration (§4.3), as §5.1 writes durations |
|
||
| `X-RESUME-OFFSET` | the same value |
|
||
| `X-PLAYOUT-LIMIT` | the same value |
|
||
|
||
No attribute of the first tag changes: the closing tag adds three it did not have, and repeats
|
||
`CLASS` with its value. A player inside the break when the closing tag arrives ends the break
|
||
at the playout limit, and returns to the program where the in placed its end.
|
||
|
||
### 5.3 The preload tag
|
||
|
||
When the policy's `preload_ms` is not 0, each break also has a Preload `EXT-X-DATERANGE` tag
|
||
(Appendix F, Appendix D.2), written just before its interstitial tag:
|
||
|
||
| attribute | value |
|
||
|---|---|
|
||
| `ID` | quoted: the break's ID followed by `-preload` |
|
||
| `CLASS` | quoted: `com.apple.hls.preload`, the class Appendix F defines <!-- allow-name: the preload date-range class, a protocol identifier --> |
|
||
| `START-DATE` | quoted: the interstitial's `START-DATE` less `preload_ms`, in its form |
|
||
| `DURATION` | `preload_ms`, as §5.1 writes durations |
|
||
| `X-URI` | quoted: the break's HLS asset-list URI (§7.2) |
|
||
| `X-TARGET-ID` | quoted: the break's ID |
|
||
| `X-TARGET-CLASS` | quoted: the interstitial class <!-- allow-name: the interstitial date-range class, a protocol identifier --> |
|
||
|
||
A player picks a random instant in the preload range and fetches the asset list there
|
||
(Appendix D.2), so the requests of a room's audience for one break are spread over
|
||
`preload_ms` rather than arriving together at its start.
|
||
|
||
### 5.4 What stays shared
|
||
|
||
Every attribute of §5.1 to §5.3 is a function of the signed cue, its annotation, the signed
|
||
policy and the room's `t0`, and of nothing a viewer supplies. Every Media Playlist of the room
|
||
therefore carries the same date ranges with the same attributes, as §6.2.4 requires, and every
|
||
edge that holds the room's events writes the same bytes, so playlists stay cacheable. The one
|
||
URI a break names is where each viewer's decision is made (§7), and the request for it carries
|
||
what makes it a viewer's: the `_HLS_primary_id` and `_HLS_start_offset` query parameters
|
||
Appendix D.4 has a player add.
|
||
|
||
A playlist that carries these tags carries `EXT-X-PROGRAM-DATE-TIME` on the same UTC timeline
|
||
as `t0` (§4.4.5.1). The corpus states the tags of each room in the order a packager writes
|
||
them: a break's preload and interstitial tags at its cue, its closing tag at its in.
|
||
|
||
## 6. DASH: alternative presentation events
|
||
|
||
### 6.1 What this revision read
|
||
|
||
ISO/IEC 23009-1:2026 defines alternative presentation events in its §5.16, whose text this
|
||
revision could not read. It read the sixth edition's MPD schema and its example MPDs. From
|
||
them it takes: the event schemes `urn:mpeg:dash:event:alternativeMPD:replace:2025` and
|
||
`urn:mpeg:dash:event:alternativeMPD:insert:2025` (example G23 and G29-1 use the first); an
|
||
`Event`'s `presentationTime`, `duration` and `id`; its child `ReplacePresentation` or
|
||
`InsertPresentation`, with the attributes `uri` (required), `earliestResolutionTimeOffset`,
|
||
`maxDuration`, `executeOnce`, `skipAfter` and, for a replacement, `returnOffset`, with their
|
||
types; and the **list MPD**, an MPD of `type` `list` whose Periods each import one MPD
|
||
(`ImportedMPD`). The values below are written from those types and examples. Where the
|
||
schema has an attribute whose meaning this revision could not state, it does not write it:
|
||
`noJump`, `serviceDescriptionId`, `startWithOffset` and `clip`, whose defaults stand. A later
|
||
revision checks every value here against §5.16.
|
||
|
||
### 6.2 The event
|
||
|
||
A mid-roll is an `Event` of the replace scheme; a post-roll is one of the insert scheme, since
|
||
after the program there is nothing to replace. With `timescale` 1000 on its EventStream:
|
||
|
||
| attribute | value |
|
||
|---|---|
|
||
| `presentationTime` | `pt0` + ⌊`t` / 1000⌋, `t` the opening cue's, where `pt0` is the presentation time of `t0` in milliseconds on the Period's timeline (live-events §8.4) |
|
||
| `duration` | when the break's duration is known (a break of type 4, or an out its in has closed): that duration in milliseconds |
|
||
| `id` | the break's event id (§4.4) |
|
||
|
||
and its one child, `ReplacePresentation` for a mid-roll and `InsertPresentation` for a
|
||
post-roll:
|
||
|
||
| attribute | value |
|
||
|---|---|
|
||
| `uri` | the break's DASH asset-list URI (§7.3) |
|
||
| `earliestResolutionTimeOffset` | when `preload_ms` is not 0: `preload_ms` |
|
||
| `maxDuration` | when the duration is known: the duration in milliseconds |
|
||
| `executeOnce` | `true`, as HLS's `ONCE` |
|
||
| `skipAfter` | `PT`, then `skip_after_s` in decimal, then `S`; with no skip control, the avail in whole seconds rounded up (`PT30S` for 30 000 ms), so that no viewer skips before the avail has played: the schema's examples read a missing `skipAfter` as a break a viewer may skip at once |
|
||
| `returnOffset` | a `ReplacePresentation` whose duration is known: the duration in milliseconds, as HLS's `X-RESUME-OFFSET` |
|
||
|
||
Attributes are written in the order of the two tables, each as `name="value"`, the element as
|
||
`<Event …><ReplacePresentation …/></Event>`, with no whitespace other than one space before
|
||
each attribute. An out's event is written first without `duration`, `maxDuration` and
|
||
`returnOffset`, and the MPD that follows its in writes the same event with them: an MPD update
|
||
gives the event its end as HLS's closing tag does (§5.2). A planned duration has no DASH
|
||
attribute.
|
||
|
||
### 6.3 The event streams
|
||
|
||
The Period that holds the room's program carries, after every break of the schedule is
|
||
written:
|
||
|
||
1. when the schedule has a mid-roll, `<EventStream schemeIdUri="urn:mpeg:dash:event:alternativeMPD:replace:2025" timescale="1000">`,
|
||
the mid-rolls' events in schedule order, and `</EventStream>`;
|
||
2. when it has a post-roll, the same with the insert scheme and the post-roll's event.
|
||
|
||
Every value is a function of the signed records, the policy and `pt0`, so the MPD stays one
|
||
for every viewer, as the playlists do (§5.4).
|
||
|
||
## 7. The asset list
|
||
|
||
### 7.1 The decision
|
||
|
||
A player that reaches a break fetches its asset list. The decider fills it for that viewer with
|
||
the decision of `wai.video.ssai` (SPEC §5), unchanged:
|
||
|
||
- its inputs are the viewer's pod, the policy's `slots`, an **avail** and the **funds
|
||
remaining**: the break's avail (§4.3) less the viewer's start offset (§7.4), or 0 when the
|
||
offset reaches it; and the funds the decider states it had left of the decider grant's
|
||
ceiling, at most that ceiling (§3.4);
|
||
- the candidates are taken in order of `price_micros` descending, ties by `ad_id` ascending
|
||
(as bytes); each is taken when fewer than `slots` are taken, its `duration_ms` is at most the
|
||
avail not yet taken and its `price_micros` at most the funds not yet taken; the fill is the
|
||
taken candidates in that order, slot 0 first.
|
||
|
||
The fill is a pure function of those inputs, so anyone holding them recomputes it, and the
|
||
receipt binds each of them (§8).
|
||
|
||
### 7.2 HLS asset list
|
||
|
||
The break's **HLS asset-list URI** is `asset_base`, the break's ID, `.json?_HLS_interstitial_id=`
|
||
and the ID again: the query parameter Appendix D.4 has a packager add. The response is the
|
||
JSON object Appendix D.2 defines, written exactly:
|
||
|
||
```
|
||
{"ASSETS":[{"URI":"<creative_base><creative_hash>.m3u8","DURATION":<s>},…]}
|
||
```
|
||
|
||
one Asset-Description for each ad of the fill in slot order, `<creative_hash>` its 64
|
||
lowercase hexadecimal digits and `<s>` its `duration_ms` as decimal seconds with three digits
|
||
after the point, with no whitespace and no byte after the closing brace. An empty fill is
|
||
`{"ASSETS":[]}`, which has a player resume the program without playing an ad (Appendix D.5).
|
||
The asset list carries no `SKIP-CONTROL`: the tag's attributes stand.
|
||
|
||
A creative's URI names it by its hash, so the asset list a viewer is served binds the creative
|
||
bytes the receipt names.
|
||
|
||
### 7.3 DASH list MPD
|
||
|
||
The break's **DASH asset-list URI** is `asset_base`, the break's ID and `.mpd`. The response is
|
||
a list MPD, written exactly as the XML declaration `<?xml version="1.0" encoding="UTF-8"?>`, a
|
||
line feed, and:
|
||
|
||
```
|
||
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:list:2024" type="list" minBufferTime="PT1S"><Period id="0" duration="PT<s>S"><ImportedMPD><creative_base><creative_hash>.mpd</ImportedMPD></Period>…</MPD>
|
||
```
|
||
|
||
one Period for each ad of the fill, its `id` the slot in decimal and `<s>` as §7.2, with no
|
||
whitespace other than that shown and no byte after `</MPD>`. The profile and `minBufferTime`
|
||
are those of the schema's list-MPD example (G29-2). An MPD has at least one Period, so an empty
|
||
fill has no list MPD: the decider answers the request with no body, and the receipt binds the
|
||
empty asset list, zero bytes (§8.2).
|
||
|
||
### 7.4 Joining a break
|
||
|
||
A viewer who reaches a break after its start, by joining the room inside it, has its player
|
||
add the offset into the break at which it starts (`_HLS_start_offset`, Appendix D.4). The
|
||
decider fills the remaining avail only (§7.1), and the receipt states the offset
|
||
(`start_offset_ms`). A viewer who joins between breaks is given no break: a room has no
|
||
pre-roll. The asset list a joining viewer is served is a fill like any other, and every check
|
||
of §9.3 applies to it.
|
||
|
||
## 8. The break form of the `wai.video.ssai` receipt
|
||
|
||
### 8.1 Forms
|
||
|
||
A `wai.video.ssai` receipt has two forms:
|
||
|
||
- **form 1, stitched**: an avail of an attested manifest, named by `avail_id` and
|
||
`manifest_hash`, as SPEC §5 registers it. This revision changes nothing in it: its signed
|
||
bytes, its identity and every receipt of it stay as they were;
|
||
- **form 2, break**: a break of a room's schedule and the asset list one viewer was served for
|
||
it, defined here.
|
||
|
||
The two forms sign under different domains (§8.4), so no signature of one is a signature of
|
||
the other, and they share the impression, the pod hash, the Merkle root, the billable spend and
|
||
the identity domain, so a figure profile of energy-measurement §6.1,
|
||
`wai:video-ssai-receipt-id`, names either.
|
||
|
||
### 8.2 Fields and their JSON carriage
|
||
|
||
A form-2 receipt is carried as one JSON object (RFC 8259) with exactly these members; a byte
|
||
field is lowercase hexadecimal of its exact length, and an integer a JSON integer from 0 to
|
||
2^64 − 1:
|
||
|
||
| member | value |
|
||
|---|---|
|
||
| `form` | 2 |
|
||
| `room` | 32 bytes: the room id |
|
||
| `cue` | 32 bytes: the break's digest (§4.4) |
|
||
| `session` | 16 bytes: the session (§2), one value for every break of one playback session, and no other's |
|
||
| `start_offset_ms` | the viewer's offset into the break (§7.4); 0 for a viewer present at its start |
|
||
| `format` | from 0 to 255: 1, the HLS asset list (§7.2); 2, the DASH list MPD (§7.3) |
|
||
| `asset_list_sha256` | 32 bytes: SHA-256 of the asset list served, its exact bytes |
|
||
| `avail_ms` | the break's avail (§4.3) |
|
||
| `slots` | the policy's `slots` |
|
||
| `funds_remaining_micros` | the funds the decision ran with (§7.1) |
|
||
| `pod_hash` | 32 bytes: §8.3 |
|
||
| `impressions` | an array: each filled slot, `{"slot", "ad_id", "creative_hash", "duration_ms", "price_micros", "quartiles_reached"}`, in slot order; `ad_id` a string, `creative_hash` 32 bytes, `quartiles_reached` 0 to 255 |
|
||
| `merkle_root` | 32 bytes: §8.3 |
|
||
| `budget_spent_micros` | the billable spend (§8.3) |
|
||
| `work_decide_ops` | the candidates the decision considered |
|
||
| `joules_micro` | the decision's energy, or 0 unmetered |
|
||
| `energy_provenance` | its class's label (energy-measurement §2.7: `ModelBased`, `Estimator`, `OnChipCounter`, `CalibratedInstrument`), or `null` unmetered |
|
||
| `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | only for a class that carries them, as energy-measurement §2.7 names and writes them: `{"relative_ppm", "window_us"}`, `relative_ppm` from 0 to 2^32 − 1; `Unknown`, `Off` or `On`; a string |
|
||
| `grant` | `{"grant_hash", "capability", "joule_ceiling_micro", "funds_ceiling"}`: the grant reference (live-rooms §4.5), `funds_ceiling` `null` for none |
|
||
| `signer_pubkey` | 32 bytes: the decider's Ed25519 key |
|
||
| `signer_id` | a string: the decider's `did:key` or origin-bound id |
|
||
| `sig` | 64 bytes: §8.4 |
|
||
| `version` | a receipt of version 2 alone: 2. A receipt with no `version` member is of version 1 |
|
||
| `consent` | version 2 only: the consent signal (§8.6) |
|
||
| `supply_chains` | version 2 only: the supply chains (§8.7), one for each impression, in the impressions' order, each `null` when the decider states none for it |
|
||
|
||
`quartiles_reached` is the stitched form's: 0 never rendered, 1 started (the billable
|
||
impression), 2 to 5 a quarter, a half, three quarters and all of it. A receipt that does not
|
||
read so, or that carries the legacy class label `HwShunt` (energy-measurement §2.6), is
|
||
refused `receipt_malformed`.
|
||
|
||
A receipt of version 1 has none of `version`, `consent` and `supply_chains`, and one of version 2
|
||
has all three; their members are §8.6's and §8.7's, each of its type, or the receipt is refused
|
||
`receipt_malformed`.
|
||
|
||
### 8.3 Hashes
|
||
|
||
BLAKE3, with integers big-endian, as the stitched form computes them:
|
||
|
||
- the **pod hash**: over `wai:video-ssai-pod` 0x01, the candidates' count (u64), and for each
|
||
candidate in the pod's order, `ad_id`'s length (u32) and bytes, `creative_hash`,
|
||
`duration_ms` (u64) and `price_micros` (u64);
|
||
- an impression's **leaf**: over `wai:video-ssai-imp` 0x01, `slot` (u64), `ad_id`'s length
|
||
(u32) and bytes, `creative_hash`, `duration_ms` (u64), `price_micros` (u64) and
|
||
`quartiles_reached` (u8);
|
||
- the **Merkle root** over the leaves in slot order: [jwp-receipts](jwp-receipts.md) §2.2's
|
||
tree, each leaf taken as a content hash (its first leaf form) and the final one duplicated
|
||
on an odd level; all zero for no impression;
|
||
- the **billable spend**: the sum of `price_micros` over the impressions whose
|
||
`quartiles_reached` is at least 1.
|
||
|
||
### 8.4 Signing payload and identity
|
||
|
||
The **signing payload** is the concatenation of:
|
||
|
||
1. `wai:video-ssai-break-receipt` (ASCII) and 0x01;
|
||
2. `room`, `cue`, `session`, `start_offset_ms` (u64), `format` (u8), `asset_list_sha256`,
|
||
`avail_ms` (u64), `slots` (u64), `funds_remaining_micros` (u64);
|
||
3. `pod_hash`, `merkle_root`, `budget_spent_micros` (u64), `work_decide_ops` (u64),
|
||
`joules_micro` (u64), then the class in its signed encoding (energy-measurement §2.7), the
|
||
tag byte 0 for none;
|
||
4. the grant reference: `grant_hash`, the length (u32) and bytes of `capability`,
|
||
`joule_ceiling_micro` (u64), then 1 and `funds_ceiling` (u64), or 0 for none;
|
||
5. the impressions' count (u64), then each one's leaf;
|
||
6. `signer_pubkey`, then the length (u32) and bytes of `signer_id`.
|
||
|
||
`sig` is Ed25519 by `signer_pubkey` over the signing payload. The receipt's **identity**, its
|
||
receipt hash, is BLAKE3 over `wai:video-ssai-receipt-id` 0x01, the signing payload and `sig`:
|
||
the stitched form's identity domain, over a payload whose first bytes no stitched payload has.
|
||
|
||
A receipt of **version 2** signs, in place of item 1, `wai:video-ssai-break-receipt` (ASCII) and
|
||
0x02, and, between items 5 and 6, its consent signal and then its supply chains, each in its
|
||
signed encoding (§8.6, §8.7). No receipt of version 1 is one of version 2, and every receipt of
|
||
version 1 keeps its bytes and its identity.
|
||
|
||
### 8.5 Its own rules
|
||
|
||
A form-2 receipt holds, on its own:
|
||
|
||
1. `format` is 1 or 2;
|
||
2. its signature verifies (`bad_key`, `bad_signature`);
|
||
3. `merkle_root` is the root of its impressions, `budget_spent_micros` their billable spend,
|
||
and when `grant`'s `funds_ceiling` is not `null`, the spend is within it (§3.4);
|
||
4. the impressions' slots are 0, 1, 2, … in order, each `quartiles_reached` is at most 5, and
|
||
`work_decide_ops` is at least their count;
|
||
5. the figure and its class cohere as energy-measurement §2 has them: `joules_micro` 0 with no
|
||
class, or above 0 with a class, a hardware class's declared uncertainty with
|
||
`relative_ppm` and `window_us` above 0 and at the resolution floor (`joules_micro` ×
|
||
`relative_ppm` at least 288 676), and a calibrated instrument's reference holding a
|
||
character that is not Unicode White_Space;
|
||
6. of version 2, its consent signal holds the rules of §8.6, and its supply chains the rules of
|
||
§8.7.
|
||
|
||
A rule of 1, 3, 4 or 5 that fails is `receipt_invalid`. These rules are what a buyer holding
|
||
one receipt checks alone; §9.3 adds what the room, the policy and the pod let a verifier check.
|
||
Rule 6 that fails is `consent_invalid` for the consent signal and `supply_chain_invalid` for the
|
||
chains.
|
||
|
||
### 8.6 The consent signal
|
||
|
||
A receipt of version 2 states, in its member `consent`, the **consent signal** its decision was
|
||
made under:
|
||
|
||
| member | value |
|
||
|---|---|
|
||
| `gpp` | the Global Privacy Platform string (the **GPP string**) the player gave with its request for the asset list, byte for byte; `""` when it gave none |
|
||
| `gpp_sid` | the sections of that string the player gave as applying to the request, each a section id from 1 to 65 535, in the order given; `[]` for none |
|
||
| `child_directed` | 1 when the request is directed to children: the policy sets **child** (§3.1), or the player's request states so; 0 otherwise |
|
||
|
||
They are three of the members OpenRTB 2.6 gives an ad request's `Regs` object, `gpp`, `gpp_sid`
|
||
and `coppa` (its §3.2.3), and the GPP string is the one the *Global Privacy Protocol String*
|
||
defines. The signal holds these rules, or the receipt is refused `consent_invalid` (§8.5 rule 6):
|
||
|
||
1. `gpp` is at most 65 535 bytes, each an ASCII letter or digit, `-`, `_`, `.` or `~`, the
|
||
characters the GPP string's text lists as URL-safe; and when it is not empty, no part of it
|
||
is empty between two of its `~` delimiters, before the first or after the last: the header
|
||
and each section are there;
|
||
2. `gpp_sid` holds at most 16 section ids, no two the same, each at least 1, and none when `gpp`
|
||
is empty;
|
||
3. `child_directed` is 0 or 1.
|
||
|
||
Its **signed encoding** (§8.4), big-endian as every field of the receipt: the length (u32) and
|
||
bytes of `gpp`; the count (u64) of `gpp_sid`, then each section id (u16); `child_directed`
|
||
(u8).
|
||
|
||
What each party does with it:
|
||
|
||
- **The player.** A player that holds a GPP string for the viewer from its platform's consent
|
||
tool **MUST** give it, with the sections that apply, with its request for the asset list, as
|
||
it holds it, by a means this revision does not fix (for one, the request's parameters `gpp`
|
||
and `gpp_sid`, as the GPP string's text names them for a service reached by a URL); a player
|
||
whose request is directed to children states so the same way. A player that holds no string
|
||
gives none. It **MUST NOT** write, alter or complete a GPP string: the GPP string's text has a
|
||
property's owner or its consent tool make one, and every other party pass it on as it is.
|
||
- **The decider.** A decider **MUST** state in the receipt the string and the sections it was
|
||
given, byte for byte, and **MUST** pass them unchanged in every request it makes to fill the
|
||
break (an OpenRTB 2.6 request's `Regs.gpp` and `Regs.gpp_sid`, or a URL's `gpp` and
|
||
`gpp_sid`). It **MUST** set `child_directed` when the policy sets **child** or the request
|
||
states it, and **MUST** then mark every request it makes to fill the break as directed to
|
||
children (an OpenRTB 2.6 request's `Regs.coppa` 1). It **MUST NOT** clear a flag the policy or
|
||
the request set, and **MUST NOT** state a string or a section it was not given.
|
||
- **What no party infers.** An empty `gpp` states that no string was given: not that the viewer
|
||
consented, and not that no law applies to the request. A `child_directed` of 0 states that
|
||
neither the policy nor the request marked the request as directed to children: not that the
|
||
viewer is not a child. Neither the string nor the flag names the viewer, and a decider or a
|
||
verifier **MUST NOT** use either to identify a viewer, or to link one viewer's receipts to one
|
||
another. That rule binds those parties and does not keep receipts from linking: a GPP string
|
||
that is not empty is made for the viewer by a consent tool, not for one request, and can stay
|
||
the same, byte for byte, across the viewer's sessions and rooms, so whoever holds two receipts
|
||
that carry it can link them through it (§12).
|
||
- **The verifier.** A verifier reads the string's characters and its parts, and does not decode
|
||
its sections: what a section states, and whether it permits what was done, is for the GPP
|
||
string's texts and the parties to judge. A receipt that verifies shows what the decider signed
|
||
that it was given and passed on; not that the viewer gave it, and not that any party honoured
|
||
it.
|
||
|
||
### 8.7 The supply chain
|
||
|
||
A receipt of version 2 states, in its member `supply_chains`, the chain of sellers each of its
|
||
impressions was sold through: the `SupplyChain` object (OpenRTB 2.6 §3.2.25 and §3.2.26, and
|
||
the *OpenRTB SupplyChain object*) of the request whose answer gave the decider that
|
||
impression's candidate, with references to the files that let a buyer check each seller. A
|
||
pod's candidates can come from the answers to several requests, each request with its own
|
||
chain, so the member is an array with one entry for each impression, in the impressions' order,
|
||
and two impressions whose candidates answered one request state the same chain. WAI carries the object
|
||
and the references as those texts define them, and defines no supply-chain scheme of its own.
|
||
An entry is `null` when the decider states no chain for its impression, or an object:
|
||
|
||
| member | value |
|
||
|---|---|
|
||
| `ver` | the object's `ver`: its version, `major.minor` (`1.0` in the revision read) |
|
||
| `complete` | its `complete`: 1 when the chain holds every node back to the owner of the inventory, 0 otherwise |
|
||
| `nodes` | its `nodes`, in its order: the first the initial seller (in a complete chain, the owner's own account), the last the one that sent the request; each an object of the members below |
|
||
| `authorised` | the authorised-sellers file the chain is checked against, `{"file", "domain", "sha256"}`, or `null` |
|
||
|
||
A node:
|
||
|
||
| member | value |
|
||
|---|---|
|
||
| `asi` | the canonical domain of the advertising system (the SSP, exchange or other system that bidders connect to) |
|
||
| `sid` | the seller's or reseller's account id in that system |
|
||
| `hp` | 1 when the node is in the flow of payment for the inventory, 0 otherwise |
|
||
| `rid`, `name`, `domain` | the node's request id, the name and the business domain of the entity paid under `sid`, each `null` when the node has none |
|
||
| `sellers` | SHA-256 of the advertising system's **sellers file**, the file at `https://<asi>/sellers.json` (*Sellers.json* 1.0), its bytes as the decider held them when it made the decision; `null` when it held none |
|
||
|
||
In `authorised`: `file` is `ads.txt` for a site's **authorised-sellers file** (*ads.txt
|
||
Specification* 1.1 §3.1) or `app-ads.txt` for an app's (*Authorized Sellers for Apps
|
||
(app-ads.txt)* 1.0), at `https://<domain>/` followed by `file`; `domain` is the host the file is
|
||
at, the site's or the one derived from the app's developer URL; and `sha256` is SHA-256 of the
|
||
file's bytes as the decider held them. The object's `ext` members are not carried.
|
||
|
||
A **domain name** is 1 to 253 bytes: two labels or more separated by `.`, each 1 to 63 ASCII
|
||
letters, digits and `-`, neither beginning nor ending with `-`. The texts read have `asi` and a
|
||
node's `domain` be a bare domain, never a URL. Each chain holds rules 1 to 3, and the member
|
||
rule 4, or the receipt is refused `supply_chain_invalid` (§8.5 rule 6):
|
||
|
||
1. `ver` is one or more digits, `.` and one or more digits, at most 15 bytes, and `complete` is
|
||
0 or 1;
|
||
2. `nodes` holds 1 to 32 nodes; each node's `asi` is a domain name, its `sid` 1 to 255 bytes,
|
||
its `hp` 0 or 1, its `rid` and `name` `null` or 1 to 255 bytes, and its `domain` `null` or a
|
||
domain name;
|
||
3. `authorised`, when it is not `null`, has a `file` of `ads.txt` or `app-ads.txt` and a
|
||
`domain` that is a domain name;
|
||
4. `supply_chains` holds as many entries as the receipt holds impressions.
|
||
|
||
Its **signed encoding** (§8.4), big-endian: the entries' count (u64), then each entry: 0 for
|
||
`null`; or 1, then `ver` (its length, u32, and its bytes), `complete` (u8), the nodes' count
|
||
(u64), and for each node `asi` and `sid` (each its length, u32, and its bytes), `hp` (u8), each
|
||
of `rid`, `name` and `domain` as 0, or 1 and its length (u32) and bytes, and `sellers` as 0, or
|
||
1 and its 32 bytes; then `authorised` as 0, or 1, `file` and `domain` (each its length, u32, and
|
||
its bytes) and `sha256`.
|
||
|
||
**The files.** A verifier finds a file a chain names among the supply files it holds by its
|
||
SHA-256 (§9.1), and reads it so:
|
||
|
||
- a **sellers file** reads when it is UTF-8 with no byte order mark at its start, and a JSON
|
||
text (RFC 8259) within the limits that RFC 8259 leaves a reader to set (its §4, §8.1, §8.2 and
|
||
§9), which are these: its arrays and objects nest at most 64 deep, the file's own object
|
||
counted as the first; its value is an object with a member `sellers` that is an array of
|
||
objects, the **sellers**; each seller has a member `seller_id` that is a string, and when it
|
||
has a `seller_type`, that is a string too; neither the file's object nor a seller has two
|
||
members of one of these three names; and no member name of the file's object or of a seller,
|
||
nor the string of a `seller_id` or a `seller_type`, holds a `\u` escape of a surrogate
|
||
(U+D800 to U+DFFF) unless it is a high one followed at once by a low one, the pair that
|
||
escapes one character (RFC 8259 §7). The rest of the file is read only by RFC 8259's grammar:
|
||
a number is not converted to a value, so one of any size reads, and a string's escapes are
|
||
not decoded, so one that escapes a lone surrogate reads;
|
||
- an **authorised-sellers file** reads when it is UTF-8, and is read as *ads.txt Specification*
|
||
1.1 §3.2 to §3.5 describe it, by these rules: a line ends at LF, CR LF or CR; a `#` and the
|
||
rest of its line are a comment, and dropped; a line is trimmed of spaces and tabs, and passed
|
||
over when it is then empty; a line whose part before its first `=` is not empty and holds no
|
||
`,`, space or tab is a variable record, and passed over; any other line, with the part from
|
||
its first `;` (its extension fields) dropped, is a **data record** when it is three or four
|
||
fields separated by `,`, each not empty once trimmed of spaces and tabs: the advertising
|
||
system's domain, the account id, the relationship and, optionally, a certification
|
||
authority's id. Every other line is passed over, as the text has a consumer read liberally.
|
||
|
||
A node is **listed** in a sellers file that has a seller whose `seller_id` is its `sid`, byte for
|
||
byte; the first such seller is the node's **seller** there. A node is **authorised** by an
|
||
authorised-sellers file that has a data record whose first field is its `asi`, ASCII letters
|
||
compared without regard to case, whose second field is its `sid`, byte for byte, and whose
|
||
relationship, compared without regard to case, is `DIRECT`, or, for any node but the first of a
|
||
complete chain, `RESELLER`. The first node of a complete chain
|
||
is the owner's own account, which *ads.txt Specification* 1.1 §3.3 has the owner list as
|
||
`DIRECT`; an account another party controls to resell is `RESELLER`.
|
||
|
||
What each party does with it:
|
||
|
||
- **The decider.** For each impression, a decider **MUST** state the chain of the request whose
|
||
answer gave it the impression's candidate, node for node as that request carried it, and
|
||
`null` only when that request carried no chain or no request gave the candidate; and it
|
||
**MUST** state as a node's `sellers`, or as `authorised`, only the digest of a file it held
|
||
when it made the decision. It **MUST NOT** state a node the request did not carry, leave out
|
||
one it did, or state for one impression the chain of another impression's request.
|
||
- **The verifier.** A verifier that holds a file a chain names checks the chain against it
|
||
(§9.3 step 14); a part whose file it does not hold stays unchecked, and the verification says
|
||
so (§9.4). What it checks is the chain against the bytes the decider bound: whether those are
|
||
the files the advertising systems and the owner publish now is for the parties, who fetch
|
||
them, to judge.
|
||
|
||
## 9. Verification
|
||
|
||
### 9.1 Inputs
|
||
|
||
An ads verifier is given what a room verifier is given (live-rooms §9.1: a descriptor, grants,
|
||
a key-log head or none, and the chains), and:
|
||
|
||
- **policy**: the room's ad policy;
|
||
- **pt0**: the presentation time of `t0`, in milliseconds on the Period's timeline (§6.2);
|
||
- **receipts**: form-2 receipts in JSON (§8.2), in a list, each with the **pod** its decision
|
||
ran over, the candidates in the order the decider gave them;
|
||
- under a policy that requires presentation claims, the **claims**, **creatives** and
|
||
**issuers** of ad-presentation §9.1;
|
||
- **supply files**: the sellers files and authorised-sellers files it holds, by their bytes, in
|
||
any order; a receipt's chains name each by its SHA-256 (§8.7).
|
||
|
||
### 9.2 Steps
|
||
|
||
It runs these steps and stops at the first refusal, which it reports with the reason and
|
||
where:
|
||
|
||
1. **The room** (live-rooms §9): a refusal is the ads verifier's, at `room` with the room
|
||
verifier's own place. A gap is the outcome **gap**: a schedule read from part of the cues
|
||
is not the room's, so the verifier states the gaps and nothing more.
|
||
2. **The policy**, at `policy`: it reads (§3.3: `truncated`, `malformed`, `unknown_version`,
|
||
`ad_policy_malformed`); its `room` is the room id (`wrong_room`); its signature verifies
|
||
under the descriptor's `authority` (`bad_key`, `bad_signature`); `decider_grant` is the
|
||
digest of a grant the room verifier holds (`decider_grant_unknown`); and that grant is not
|
||
revoked through the key log the room verifier read (live-rooms §5, `grant_revoked`).
|
||
3. **The schedule** (§4.2, §4.3), over the events the room accepted in the merged order, each
|
||
once: the refusal, at the record's chain and its index in that chain.
|
||
4. **The renderings** (§5, §6): the tags, the events and the asset-list URIs of every break.
|
||
No refusal.
|
||
5. **The receipts**, in the list's order, each by §9.3: the refusal, at `receipt` and its
|
||
index. Under a policy that requires presentation claims, the claims are read before the
|
||
first receipt, and each receipt's claims are judged after its step 11 (ad-presentation
|
||
§9.2); a claim's verdict never refuses the room.
|
||
6. **The outcome** (§9.4).
|
||
|
||
### 9.3 Each receipt
|
||
|
||
For receipt *k*, with its pod, in this order:
|
||
|
||
1. It reads as §8.2 has it: `receipt_malformed`.
|
||
2. Its `room` is the room id: `wrong_room`.
|
||
3. Its own rules (§8.5): `bad_key`, `bad_signature` or `receipt_invalid`.
|
||
4. `signer_pubkey` is the decider (§3.4): `receipt_not_decider`.
|
||
5. `grant` is the decider grant's reference: `grant_hash` its digest, `capability`
|
||
`wai.live.room`, `joule_ceiling_micro` its `joules_ceiling`, and `funds_ceiling` its
|
||
`funds_ceiling`, or `null` when that is 2^64 − 1 (live-rooms §4.5):
|
||
`receipt_grant_mismatch`.
|
||
6. `cue` names a break of the schedule: `receipt_break_unknown`.
|
||
7. The decider grant is good at the break's `t` (§3.4): `outside_grant_window`.
|
||
8. `pod_hash` is the pod hash (§8.3) of the pod given: `pod_mismatch`.
|
||
9. `avail_ms` is the break's avail, `slots` the policy's, and the impressions, less
|
||
`quartiles_reached`, are the fill §7.1 gives for the pod, the slots, the avail less
|
||
`start_offset_ms` and `funds_remaining_micros`: `decision_mismatch`.
|
||
10. `asset_list_sha256` is SHA-256 of the asset list of the fill in the receipt's `format`
|
||
(§7.2, §7.3): `asset_list_mismatch`.
|
||
11. No receipt before it in the list names this `cue` and this `session`: `receipt_duplicate`.
|
||
A viewer is filled once for a break.
|
||
12. **Spend-equivalence.** `funds_remaining_micros` is within the decider grant's ceiling, and
|
||
so is the spend that counts of this receipt and every receipt before it, together (§3.4):
|
||
otherwise `funds_exceeded`. The spend that counts is a receipt's billable spend, or, under
|
||
a policy that requires presentation claims, its presented spend: the prices of the
|
||
impressions whose claim verifies (ad-presentation §9.3).
|
||
|
||
Step 12 holds the room's ad spend to one ceiling across every viewer and break, in the order the
|
||
receipts are given. A decision's stated funds are the decider's statement; that the spend of
|
||
every receipt together stays within the ceiling is checked, whatever was stated.
|
||
|
||
13. **The consent signal**, under a policy of version 3: the receipt is of version 2
|
||
(`request_terms_absent`); and when the policy sets **child**, its `child_directed` is 1
|
||
(`child_directed_unset`).
|
||
14. **The supply chains**, of a receipt of version 2: for each chain it states, in the
|
||
impressions' order, and in it each node in order whose sellers file the verifier holds, that
|
||
file reads (`supply_file_malformed`) and lists the node (`supply_seller_unlisted`); then,
|
||
when the verifier holds the authorised-sellers file the chain names, that file reads
|
||
(`supply_file_malformed`) and authorises every node, in order (`supply_seller_unauthorised`)
|
||
(§8.7). A chain two impressions state is checked for each.
|
||
|
||
Steps 13 and 14 read what the receipt signed and the bytes of the files held; neither changes
|
||
the decision, the asset list or the spend.
|
||
|
||
### 9.4 Outcomes and what a verification states
|
||
|
||
- **refuse**: the reason and where (§9.2).
|
||
- **gap**: the room has a gap: its gaps, under live-events' code `delivery_gap`.
|
||
- **accept**: no refusal and no gap.
|
||
|
||
With accept, the verifier states:
|
||
|
||
- each break, in schedule order: its digest, ID and event id, its author, `cue_id`, `t` and
|
||
type (out or break), its placement, its planned duration and avail, and, once closed by an
|
||
in, the in's digest, its end and its duration; or that it is open;
|
||
- each chapter: its digest, `t` and `duration_ms`;
|
||
- the HLS tags, in the order of §5.4; each break's HLS and DASH asset-list URIs; the DASH event
|
||
streams (§6.3), and each out's event as it was written before its in;
|
||
- each receipt: its identity (§8.4), the asset list it binds, written by the verifier, and its
|
||
billable spend; and the spend of every receipt together, the spend that counts (§9.3 step
|
||
12);
|
||
- under a policy that requires presentation claims, what ad-presentation §9.4 states;
|
||
- each receipt of version 2: its consent signal, as §8.6 writes it; and its supply chains, one
|
||
for each impression in its order, each `null` when it states none, or: its `complete`, and for
|
||
each node its `asi` and `sid`, whether the verifier held its sellers file (`sellers_checked`),
|
||
the `seller_type` of its seller there (§8.7), as written, or `null`, and, when the
|
||
authorised-sellers file was held, the relationship of the first record that authorises it, in
|
||
capitals, or `null`; and whether that file was held (`authorised_checked`).
|
||
|
||
## 10. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `ad_policy_malformed` | §3.3, §9.2 step 2 |
|
||
| `decider_grant_unknown` | §9.2 step 2 |
|
||
| `annotation_malformed` | §4.2 |
|
||
| `annotation_misplaced` | §4.2 |
|
||
| `cue_after_close` | §4.2, §4.3 |
|
||
| `break_overlap` | §4.3 |
|
||
| `break_duration_unknown` | §4.3 |
|
||
| `break_zero_duration` | §4.3 |
|
||
| `cue_in_unmatched` | §4.3 |
|
||
| `cue_duration_mismatch` | §4.3 |
|
||
| `receipt_malformed` | §8.2, §9.3 step 1 |
|
||
| `receipt_invalid` | §8.5, §9.3 step 3 |
|
||
| `receipt_not_decider` | §9.3 step 4 |
|
||
| `receipt_grant_mismatch` | §9.3 step 5 |
|
||
| `receipt_break_unknown` | §9.3 step 6 |
|
||
| `pod_mismatch` | §9.3 step 8 |
|
||
| `decision_mismatch` | §9.3 step 9 |
|
||
| `asset_list_mismatch` | §9.3 step 10 |
|
||
| `receipt_duplicate` | §9.3 step 11 |
|
||
| `funds_exceeded` | §9.3 step 12 |
|
||
| `consent_invalid` | §8.5 rule 6, §8.6, §9.3 step 3 |
|
||
| `supply_chain_invalid` | §8.5 rule 6, §8.7, §9.3 step 3 |
|
||
| `request_terms_absent` | §9.3 step 13 |
|
||
| `child_directed_unset` | §9.3 step 13 |
|
||
| `supply_file_malformed` | §8.7, §9.3 step 14 |
|
||
| `supply_seller_unlisted` | §8.7, §9.3 step 14 |
|
||
| `supply_seller_unauthorised` | §8.7, §9.3 step 14 |
|
||
|
||
An ads verifier also raises every code a room verifier raises (live-rooms §11 and the
|
||
live-events codes it lists), at the room: `kind_not_permitted` among them for a cue or an
|
||
annotation by a principal whose grant does not permit its kind; `truncated`, `malformed` and
|
||
`unknown_version` for a policy that does not read; `wrong_room`, `bad_key` and `bad_signature`
|
||
for a policy or a receipt of another room or whose signature fails; `grant_revoked` and
|
||
`outside_grant_window` for the decider grant (§9.2 step 2, §9.3 step 7); and `delivery_gap` for
|
||
a gap, which is not a refusal.
|
||
|
||
## 11. Conformance
|
||
|
||
[`live-conformance/ads/`](../live-conformance/ads/) holds the corpus, written from fixed seeds
|
||
by `wai_live_ads_vectors gen`: `vectors.json`, and each accepted room's HLS tags as a file of
|
||
its own, `NAME.m3u8`, one tag a line in the order of §5.4. It has:
|
||
|
||
- **Rooms**, each a room (descriptor, grants, key-log head or none, chains), its policy, `pt0`
|
||
and its receipts with their pods, and the outcome §9.4 gives them with everything it states:
|
||
- a live break, an out with no planned duration and its in, a viewer present at its start
|
||
filled from an HLS asset list, and a viewer who joins it running, filled from a DASH list
|
||
MPD with what remains of it;
|
||
- a mid-roll of known duration under a policy that restricts skipping and jumping, snaps
|
||
both ways and offers a skip control, with a viewer whose pod nothing fits;
|
||
- a program with chapters, a mid-roll, and a post-roll at its close;
|
||
- chapters, and an out with a planned duration closed by its in;
|
||
- one refused room for each reason of §10, each a control of one defect; one for each
|
||
other rule of §8.5, each refused `receipt_invalid`; a cue by a principal whose grant does
|
||
not permit `cue`, which the room refuses `kind_not_permitted`; and a decider grant revoked
|
||
through the key log, and one whose window ends before the break;
|
||
- a room with a gap;
|
||
- the request terms (§8.6, §8.7): the live break under a policy of version 3, its first
|
||
viewer's receipt stating a GPP string with its applicable section and, for its two ads, the
|
||
chains of the two requests they answered, one from the channel's own account at an SSP
|
||
through an exchange and one from the channel's own account at the exchange, each checked
|
||
against the sellers files and the channel's app-ads.txt, and its second viewer's stating no
|
||
string, a child-directed request and an incomplete chain whose sellers file the verifier
|
||
does not hold; a room the policy marks as directed to children; one chain two impressions
|
||
state, of four nodes that the channel's ads.txt authorises only when it is read by each rule
|
||
of §8.7 (a record of four fields, other letter cases, CR LF and a CR alone, an extension
|
||
field); and a sellers file that lists its node only when what §8.7 does not read is read by
|
||
RFC 8259's grammar alone (numbers past binary64's range and of 400 digits, escapes of lone
|
||
surrogates in members skipped, a member 64 deep);
|
||
- refused there: four receipts whose GPP string, child-directed flag, a node's seller id or
|
||
the order of its chains was changed after the decider signed them, each `bad_signature`; a
|
||
receipt stating version 3, one of version 1 carrying a consent signal, and one of version 2
|
||
without its chains (`receipt_malformed`); a receipt for each rule of §8.6
|
||
(`consent_invalid`) and for each rule of §8.7 (`supply_chain_invalid`); a receipt of
|
||
version 1 under a policy of version 3 (`request_terms_absent`) and one with no
|
||
child-directed flag in a room directed to children (`child_directed_unset`); a sellers file
|
||
with no sellers, one nested 65 deep, one whose second seller's id escapes a lone surrogate,
|
||
one with `sellers` twice and one holding `NaN`, and an app-ads.txt that is not UTF-8 (each
|
||
`supply_file_malformed`); a node its sellers file does not list (`supply_seller_unlisted`);
|
||
a first node of a complete chain authorised only as a reseller, a node the app-ads.txt does
|
||
not name, and one it names only in lines a reader passes over, a comment and a line of five
|
||
fields (each `supply_seller_unauthorised`); and a policy whose `audience` sets a bit above
|
||
bit 0 (`ad_policy_malformed`).
|
||
|
||
The sellers files and the authorised-sellers files the vectors hold are files of their own,
|
||
`supply.NAME`, each named in the `supply_files` of the vectors that hold it, read as bytes;
|
||
every one is a fixture written for its case under `.example` names.
|
||
|
||
A conforming verifier gives every room the outcome, the reason, the place and everything else
|
||
the corpus states, and every tag file byte for byte. `wai_live_ads_vectors verify` and the test
|
||
`tests/live_ads.rs` check that every file regenerates byte for byte, that `wai-rs` gives every
|
||
case its outcome, that every reason of §10 appears, and that §10 lists exactly the reference's
|
||
codes. `live-conformance/live_ads_verify.py` is the second implementation, written from this
|
||
text: it shares no code with `wai-rs`; it verifies each room with the live-rooms corpus's
|
||
second implementation (`live_rooms_verify.py`), every signature with WAI's strict rule
|
||
(`claim-conformance/_ed25519.py`) and every BLAKE3 hash with `claim-conformance/_blake3.py`; it
|
||
writes every tag, event and asset list from §5 to §7 and requires their bytes; it reads every
|
||
receipt's request terms and every sellers file and authorised-sellers file by §8.6 and §8.7
|
||
itself; and it mutates copies of the corpus and requires every copy to fail.
|
||
|
||
## 12. Security and privacy considerations
|
||
|
||
- Every break is a cue its author signed, under a grant that permits `cue`; a relay or an edge
|
||
can withhold a cue, which is a gap in its author's chain, but cannot add or move a break. The
|
||
date ranges and events an edge writes are recomputable from the signed records, and a
|
||
playlist whose interstitials differ from them is one the edge altered.
|
||
- The decision is a pure function of its inputs, and every input is bound: the pod by its
|
||
hash, the avail by the break, the slots by the policy, the funds by the receipt, and the
|
||
asset list served by its digest, whose URIs name each creative by its hash. A decider that
|
||
serves an ad the decision did not select, or a creative other than the one it bills, signs a
|
||
receipt that fails §9.3.
|
||
- The funds a decision states are the decider's statement. What is checked is the bound: no
|
||
receipt spends past the ceiling, and no receipts together do, so an overspent room fails
|
||
verification at the receipt that crosses the ceiling.
|
||
- `quartiles_reached` is the decider's statement of the beacons it received; a receipt binds it
|
||
and its billing, not the beacons. The impressions a buyer and a seller reconcile are the same
|
||
signed bytes. A policy of version 2 can require more: that an impression count only when the
|
||
player's signed presentation claim verifies, reaching the quartile the receipt states under
|
||
a viewability rule, optionally with a device attestation ([ad-presentation](ad-presentation.md)
|
||
§9, §13 for what that does and does not prove).
|
||
- A receipt names a session by 16 bytes the decider assigns. A decider **SHOULD** assign a
|
||
value with no meaning outside its own records, and **MUST NOT** derive it from a viewer's key
|
||
or another identifier a third party holds, so that receipts that carry no GPP string (§8.6)
|
||
do not link a viewer across rooms; within a room, one session's receipts are linkable to one
|
||
another by design, so that a viewer is filled once for a break (§9.3 step 11). Under a
|
||
policy that requires presentation claims the session is derived from a key the player makes
|
||
for that session of that room alone (ad-presentation §3), which is no viewer's key and links
|
||
nothing across rooms.
|
||
- The asset-list request carries the viewer's playback-session parameter (Appendix D.4) to the
|
||
decider, and nowhere else: the manifests every viewer shares carry nothing of any viewer.
|
||
- The policy is signed by the room authority and the receipts by the decider. A decider whose
|
||
grant is revoked or out of its window has its receipts refused (§9.2, §9.3).
|
||
- **The consent signal** is bound into what the decider signs: a receipt whose string, sections
|
||
or child-directed flag changed after signing fails its signature (§8.5 rule 2), as one whose
|
||
chains changed does. A verified receipt shows what §8.6 says it shows, what the decider stated
|
||
it was given and passed on, and no more. The string travels with the asset-list request to the
|
||
decider, from the decider to those it requests ads from, and in the receipt to whoever holds
|
||
it.
|
||
- **A GPP string links what carries it.** A consent tool makes the string for a viewer, not for
|
||
one request, and it can stay the same, byte for byte, across the viewer's sessions and rooms.
|
||
A receipt whose `gpp` is not empty can therefore be linked, through the string, to every other
|
||
receipt that carries the same string, in any room, and to a stitched pod-input record that
|
||
carries it (stitched-presentation §4.7), by whoever holds them: the same viewer's among them,
|
||
for as long as the string stays the same. §8.6's rule that a decider and a verifier not use
|
||
the string to link receipts binds those parties; it does not remove the link. The string is
|
||
in the signed bytes, so a holder cannot strip it from a receipt and keep the receipt
|
||
verifiable. What the session item above says of rooms holds of a receipt whose `gpp` is
|
||
empty.
|
||
- **A room for children.** A policy that sets **child** binds every decision of the room to a
|
||
child-directed signal: a receipt that does not carry one is refused (§9.3 step 13). That the
|
||
decider marked each of its requests so is its statement, as the funds a decision states are.
|
||
- **The supply chain** and the digests of the files it names are the decider's signed statement.
|
||
A verifier that holds the files checks every node against them; each file is fetched from the
|
||
domain it belongs to, by its own text's access method, and a verifier that needs them to be
|
||
the files published now fetches them itself and compares their digests with the receipt's.
|