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]>
168 lines
12 KiB
Markdown
168 lines
12 KiB
Markdown
# live-conformance/ads — live ads
|
|
|
|
The corpus of [`extensions/live-ads.md`](../../extensions/live-ads.md) §11: rooms whose `cue`
|
|
events open ad breaks, each a room (its `WRD1` descriptor, `WRG1` grants, key-log head or none,
|
|
and each principal's `WEV1` chain, the break annotations `WAB1` among them), its `WAP1` ad
|
|
policy, the presentation time of `t0` on the DASH Period (`pt0_ms`), and the break-form
|
|
`wai.video.ssai` receipts of its viewers, each with the pod its decision ran over; what a
|
|
verifier states of them; and each accepted room's HLS tags, byte for byte. Receipts of version
|
|
2 bind the consent signal their decision was made under and the supply chain of each impression
|
|
(§8.6, §8.7), and the vectors that hold them name the sellers files and authorised-sellers
|
|
files they are checked against.
|
|
|
|
**Ads equivalence.** For each vector, a conforming ads verifier given its `descriptor`,
|
|
`grants`, `key_log`, `chains`, `policy`, `pt0_ms`, `receipts` and `pods`, and the files its
|
|
`supply_files` names when it has that member, each as its bytes (§9.1), reaches the outcome the
|
|
vector states
|
|
(§9.4):
|
|
|
|
- `refuse`: the `reason` and where, `at`: `["room", …]` with the room verifier's own place
|
|
(live-rooms §9.1), `["policy"]`, `["record", c, i]` for record `i` of chain `c`, or
|
|
`["receipt", k]`;
|
|
- `gap`: the room's `gaps`, each `principal`, `first` and `last`;
|
|
- `accept`: the `room` id; the `breaks` (§4.3), each its `digest`, `id` and `event_id` (§4.4),
|
|
`author`, `cue_id`, `t`, `type` (`out` or `break`), `placement` (`mid` or `post`),
|
|
`planned_ms`, `avail_ms`, `duration_ms`, `closed_by` and `end` (`null` while it is open),
|
|
and its `hls_list_uri` and `dash_list_uri` (§7.2, §7.3); the `chapters`, each `digest`, `t`
|
|
and `duration_ms`; `hls`, the file holding the room's HLS tags; `dash`, the Period's event
|
|
streams (§6.3); `dash_open`, each out's event as written before its in, by the out's index
|
|
among the `breaks`; `receipt_facts`, each receipt's `identity` (§8.4), the `asset_list` it
|
|
binds as the verifier writes it, and its `spend_micros`, and, for a receipt of version 2,
|
|
its `consent` (`gpp`, `gpp_sid`, `child_directed`) and its `supply_chains`, one for each
|
|
impression, each `null` or its `complete`, its `nodes` (each `asi`, `sid`,
|
|
`sellers_checked`, `seller_type` and `relationship`) and `authorised_checked`; and
|
|
`spend_micros`, the spend of every receipt together.
|
|
|
|
Each tag file, `NAME.m3u8`, is the HLS tags of vector `NAME` (§5.1 to §5.3), one tag a line in
|
|
the order §5.4 gives, each line ended by a line feed. It is the date ranges of the room's Media
|
|
Playlists, not a whole playlist: the segments are the room's tracks.
|
|
|
|
A verifier that reaches another outcome, reason, place, break, tag byte, event byte, asset-list
|
|
byte, identity or spend does not conform.
|
|
|
|
**Provenance.** Generated here: `wai_live_ads_vectors gen` writes every file from rooms
|
|
authored in code, from fixed seeds. Each principal's key is SHA-256 of a fixed label and the
|
|
principal's index; each creative's hash and each session id likewise; every time is a
|
|
constant (the rooms' `t0` is 2026-10-16T00:00:00Z); every origin and base URI is an
|
|
`.example` name (RFC 2606); and Ed25519 signing is deterministic (RFC 8032), so every file is
|
|
the same bytes on every machine. The GPP string is the example the GPP string's text gives for
|
|
a service reached by a URL, with its section 2; the sellers files and the authorised-sellers
|
|
files are fixtures written for their cases under `.example` names, none fetched from a service.
|
|
The generator asserts that each vector reaches the outcome it
|
|
was written to have, and that the corpus gives every reason of §10, before it writes. No
|
|
recorded data.
|
|
|
|
## Layout
|
|
|
|
- `vectors.json`: `principals` (the keys, by name, informative) and `vectors`.
|
|
- `NAME.m3u8`: the HLS tags of each accepted room.
|
|
- `supply.NAME`: the sellers files (`supply.ssp.example.sellers.json`,
|
|
`supply.exchange.example.sellers.json`, and `supply.gateway.example.sellers.json`, which
|
|
reads only by RFC 8259's grammar outside what §8.7 reads), the channel's authorised-sellers
|
|
files (`supply.channel.example.app-ads.txt`, and `supply.channel.example.ads.txt`, whose
|
|
records need each reading rule of §8.7), and files that do not read or authorise:
|
|
`supply.not-sellers.json` (no sellers), `supply.too-deep.sellers.json`,
|
|
`supply.lone-surrogate.sellers.json`, `supply.sellers-twice.sellers.json`,
|
|
`supply.nan.sellers.json`, `supply.not-utf8.app-ads.txt` and
|
|
`supply.passed-over.app-ads.txt`. They are read as bytes: some end lines with CR LF or a CR
|
|
alone, and one is not UTF-8. `wai/.gitattributes` keeps every line ending as it is.
|
|
|
|
Every room has a producer, whose grant permits `cue`, `meta` and `chat`; a viewer, whose grant
|
|
permits `chat`; and the decider, whose grant permits no event and carries the room's ad budget,
|
|
2 minor units of EUR (2 000 000 millionths), and which never joins. The clock admits the
|
|
producer and the viewer at the room's start.
|
|
|
|
The rooms:
|
|
|
|
- `live_break`: an out at 60 s with no planned duration and its in at 90 s, under a policy
|
|
whose open avail is 30 s and whose preload is 4 s. One viewer, present at the break's start,
|
|
is filled with two 15 s ads from an HLS asset list (its decision energy model-based); a
|
|
second joins 12 s into the break and is filled with what remains, one ad, from a DASH list
|
|
MPD (its decision energy an on-chip counter's reading). No pre-roll.
|
|
- `mid_roll`: a 30 s break, annotated mid-roll, under a policy that restricts skipping and
|
|
jumping, snaps both ways and offers a skip control after 5 s, with `pt0_ms` 5000. One
|
|
viewer's pod has two ads at one price, ordered by `ad_id`, and three fill the avail; another
|
|
viewer's only ad costs more than its funds, so its fill is empty and its DASH list MPD is no
|
|
bytes.
|
|
- `post_roll`: two chapters, a 15 s mid-roll at 30 s, and a 20 s break annotated post-roll at
|
|
120 s; one viewer filled in both breaks, another in the post-roll only.
|
|
- `chapters`: a chapter of a stated length and one of none, then an out with a planned
|
|
duration of 15 s and its in, which states no duration.
|
|
- `gap`: `live_break`'s room as a subscriber holds it when the out was not delivered.
|
|
- refused, one defect each: `cue_kind_not_permitted` (refused by the room,
|
|
`kind_not_permitted`), `policy_slots_zero`, `policy_wrong_room`, `policy_bad_signature`,
|
|
`decider_grant_unknown`, `decider_grant_revoked` (a room whose key log revokes the decider
|
|
grant, `grant_revoked`), `annotation_placement`, `annotation_on_out`, `cue_after_post_roll`,
|
|
`overlapping_breaks`, `open_break_no_avail`, `break_of_no_duration`, `in_without_out`,
|
|
`in_duration_mismatch`, `receipt_extra_member`, `receipt_wrong_room`,
|
|
`receipt_bad_signature`, `receipt_quartile_six` and, for the other rules of §8.5 each
|
|
broken alone, `receipt_format_three`, `receipt_work_below_fill`,
|
|
`receipt_figure_without_class`, `receipt_below_resolution` and `receipt_over_its_ceiling`
|
|
(all `receipt_invalid`), `receipt_not_decider`,
|
|
`receipt_grant_mismatch`, `receipt_names_the_in`, `decider_grant_expired`
|
|
(`outside_grant_window`), `pod_mismatch`, `decision_mismatch`, `asset_list_mismatch`,
|
|
`receipt_duplicate` and `funds_exceeded` (a budget of 1 minor unit, and two viewers each
|
|
billed 600 000 millionths).
|
|
- `consent_and_chain`: `live_break`'s room under a policy of version 3. The first viewer's
|
|
receipt states the GPP string its player gave, with section 2, and a chain for each of its
|
|
two ads, which answered two requests: a complete chain from the channel's own account at
|
|
`ssp.example` (`pub-1001`, `PUBLISHER`, `DIRECT`) through `exchange.example` (the SSP's
|
|
account `ssp-77`, `INTERMEDIARY`, `RESELLER`), and a complete chain of one node, the
|
|
channel's own account at the exchange (`pub-2001`, `PUBLISHER`, `DIRECT`), each checked
|
|
against the sellers files and the channel's app-ads.txt. The joining viewer's states no
|
|
string, a child-directed request, and an incomplete chain of one node whose sellers file the
|
|
verifier does not hold: unchecked.
|
|
- `child_room`: a 15 s break in a room whose policy marks it as directed to children; the
|
|
receipt states the request child-directed and no chain for its ad.
|
|
- `authorised_read_liberally`: the first viewer's two ads answered one request, so its receipt
|
|
states one chain twice: four nodes, each authorised by `supply.channel.example.ads.txt` only
|
|
when it is read by §8.7's rules (a record of four fields; one whose advertising system is in
|
|
other letter cases and that ends with CR LF; one whose relationship is in lower case and that
|
|
ends with a CR alone; one with an extension field).
|
|
- `sellers_read_by_grammar`: the first viewer's first ad sold through the gateway's seller
|
|
`pub-4001`, which `supply.gateway.example.sellers.json` lists only when what §8.7 does not
|
|
read is read by RFC 8259's grammar alone: a number past binary64's range, one of 400 digits,
|
|
escapes of lone surrogates in members skipped, a member name escaping a character with a
|
|
pair of surrogates, a member nested 64 deep with the file's object, and a seller id that
|
|
escapes one of its characters. Its second ad states no chain.
|
|
- refused, one defect each: `receipt_consent_altered`, `receipt_child_flag_cleared`,
|
|
`receipt_chain_altered` and `receipt_chains_swapped` (a GPP string, a child-directed flag, a
|
|
first node's seller id and the order of the two chains changed after signing, each
|
|
`bad_signature`); `receipt_version_three`, `receipt_v1_with_consent` and
|
|
`receipt_v2_without_chains` (`receipt_malformed`); `receipt_gpp_empty_section`,
|
|
`receipt_gpp_not_url_safe`, `receipt_sections_without_string`, `receipt_section_repeated`
|
|
and `receipt_child_flag_two` (`consent_invalid`); `receipt_chain_ver`,
|
|
`receipt_chain_complete_two`, `receipt_asi_url`, `receipt_authorised_file`,
|
|
`receipt_authorised_domain` and `receipt_chains_count` (`supply_chain_invalid`);
|
|
`receipt_without_terms` (a receipt of version 1 under a policy of version 3,
|
|
`request_terms_absent`); `child_flag_unset` (`child_directed_unset`);
|
|
`sellers_file_malformed`, `sellers_file_too_deep`, `sellers_file_lone_surrogate`,
|
|
`sellers_file_sellers_twice`, `sellers_file_nan` and `authorised_file_not_utf8`
|
|
(`supply_file_malformed`); `seller_unlisted` (`supply_seller_unlisted`);
|
|
`seller_unauthorised` (a chain stated complete whose first node is a reseller's account),
|
|
`seller_absent_from_authorised` and `authorised_record_passed_over` (a node named only in a
|
|
comment and in a line of five fields) (`supply_seller_unauthorised`); and
|
|
`policy_audience_bit` (`ad_policy_malformed`).
|
|
|
|
## Running
|
|
|
|
```
|
|
cd ../../wai-rs
|
|
cargo run --no-default-features --features live_ads --bin wai_live_ads_vectors -- verify ../live-conformance/ads
|
|
cargo run --no-default-features --features live_ads --bin wai_live_ads_vectors -- gen ../live-conformance/ads # regenerate
|
|
cargo test --no-default-features --features live_ads --test live_ads
|
|
cd ../live-conformance
|
|
python3 -I live_ads_verify.py ads
|
|
```
|
|
|
|
`wai_live_ads_vectors verify` and `tests/live_ads.rs` require every file to regenerate byte for
|
|
byte, no other file but this one to be there, and `wai-rs` to give every case its outcome; the
|
|
test also requires §10 to list exactly the reasons the reference reports, and the generator
|
|
that the corpus gives each of them. `live_ads_verify.py` is the second implementation, written
|
|
from the extension's text and sharing no code with `wai-rs`: it verifies each room with
|
|
`live_rooms_verify.py`, writes every tag, event and asset list itself, verifies every receipt's
|
|
signature with WAI's strict Ed25519 rule and computes its BLAKE3 hashes from the specification,
|
|
reads every receipt's request terms and every supply file itself, as bytes, reads §10 from the
|
|
extension, gives every case the same outcome, requires every reason of §10 to appear, and then mutates
|
|
copies of the corpus, one defect each, each of which must fail.
|
|
Pass = an exit status of 0 from each.
|