open-standards/wai/extensions/stitched-presentation.md
Transaction Science 939d82548a
All checks were successful
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 27s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 27s
wai-app and Create / wai-app · does this change touch it (push) Successful in 30s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 48s
wai-ledger service / does this change touch it (push) Successful in 25s
wai-quantum for JavaScript / does this change touch it (push) Successful in 26s
wai-audit collector and player integration / does this change touch it (push) Successful in 45s
wai-key-proxy / does this change touch it (push) Successful in 44s
wai-quantum studio and desktop / does this change touch it (push) Successful in 53s
WAI operation catalogue / does this change touch it (push) Successful in 1m2s
wai-fleet service / does this change touch it (push) Successful in 1m1s
WAI Studio / does this change touch it (push) Successful in 35s
wai-sink-android native sink library / does this change touch it (push) Successful in 48s
wai-sink-swift native sink package / does this change touch it (push) Successful in 36s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m0s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 14s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 35s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m15s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 50s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 6m10s
wai-ledger service / service · linux (push) Successful in 2m42s
wai-ledger service / service · macos (push) Successful in 1m11s
wai-key-proxy / proxy · linux (push) Successful in 1m3s
wai-quantum for JavaScript / javascript · linux (push) Successful in 4m0s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 7m36s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m14s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m6s
wai-audit collector and player integration / collector · linux (push) Successful in 5m18s
wai-fleet service / service · linux (push) Successful in 2m24s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m8s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m3s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 6m45s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m6s
wai-quantum studio and desktop / studio · browser (push) Successful in 7m35s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m4s
wai-fleet service / service · macos (push) Successful in 45s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 12m29s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m4s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 9m18s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 10m18s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 9m50s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 15m22s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m34s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 7m17s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 14m15s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m40s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m5s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m44s
Invalid traffic refused by a verifier: six rules decided from signed records
ad-presentation §14 and stitched-presentation §12: a verifier applies six
invalid-traffic rules to the impressions it states, each a comparison of signed
bytes or integers with no score or model: one impression presented twice, a
span past its creative or a media time in two claims, a claim with no
attestation where the terms require one, more sessions under one device key
than the policy's cap, an address in a pinned list of ranges, and a user agent
holding a token of a pinned list of declared crawlers.

New records: the traffic policy (WTP1), the session origin an edge signs
(WSO1), the device binding a device key and the session key sign (WDB1), and
the two lists pinned by digest with their source (WAL1, WUL1). Each impression
is valid, invalid with every rule and the records it rests on, or undecided
where a turned-on rule had no record; invalid impressions that counted are
excluded and counted, undecided ones withheld, and the valid spend is what
remains (by currency for a stream). No claim's verdict or record changes.

live_ads::traffic and stitched_presentation::traffic read a finished
verification; live_ads::verify and stitched_presentation::verify are
unchanged. Corpus live-conformance/ad-traffic/ (wai_ad_traffic_vectors),
tests/ad_traffic.rs, and the second implementation ad_traffic_verify.py; a
verifier with any one rule off is caught by the vectors in both. Decision D5
records what is and is not a rule here.

Co-Authored-By: Claude <[email protected]>
2026-11-10 22:32:57 -05:00

819 lines
53 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Stitched Presentation
> Status: Draft. Registers the record `wai.video.stitched_presentation`: a player's signed
> claim, `WSC1`, that it presented one ad another party wrote into the stream, either stitched
> into the segments of an HLS or DASH presentation or served as an HLS interstitial, beside the
> **pod-input record**, `WSI1`, that the stitcher or a proxy in front of the ad server signs for
> each ad decision. A claim names its ad by the **marker** the stream carries for it (an
> `EXT-X-DATERANGE` tag, an asset of an interstitial's asset list, or an `Event` of a DASH
> `EventStream`) and the ad identity that marker carries, in the manifest whose SHA-256 it
> states; it carries ad-presentation's spans of visible and audible play, its viewability rules
> and quartile, and its device attestation; and, for an ad shown in a composited frame (a
> squeeze or a double box of [live-compose](live-compose.md) §5.9), the share of the picture the
> ad's region holds. The pod-input record states the digest of the ad server's response, the
> ads it chose with their markers, creatives and prices, the avail, the frame and the
> measurement terms. A verifier recomputes, from the claims that verify, each ad's
> presentations, the quartiles they reached and the spend that counts; the choice stays the ad
> server's. No new transport. The reference implementation is the `stitched_presentation`
> module of `wai-rs` (feature `stitched_presentation`); the corpus is
> [`live-conformance/stitched/`](../live-conformance/stitched/), and its second implementation
> is `live-conformance/stitched_presentation_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision, each at its source:
>
> - *Video Ad Serving Template (VAST)* 4.3, December 2022: §3.1 to §3.4.4, §3.4.8, §3.6 to
> §3.7.1 and §3.8 to §3.8.1, beside the sections [ad-presentation](ad-presentation.md) read.
> - *Server-Side Ad Insertion and OTT Guidance*, August 2021, §1.2, §2 and §3.
> - For HLS and DASH, the texts [live-ads](live-ads.md) read, at the revisions it names:
> `draft-pantos-hls-rfc8216bis-22` §4.2 (attribute lists), §4.4.5.1 (`EXT-X-DATERANGE`) and
> Appendix D (interstitials and the asset list); ISO/IEC 23009-1:2026's MPD schema (its
> `EventStream` and `Event`). Nothing here depends on a part of them live-ads did not read.
> - RFC 8259 (JSON) and XML 1.0, for the subset of §3.5.
> - For the consent signal a record of version 2 carries (§4.7), the texts
> [live-ads](live-ads.md) read for its §8.6, at the revisions it names.
>
> A section number of one of these texts is that revision's.
>
> A pod-input record of version 2 also carries the consent signal its decision was made under,
> a Global Privacy Platform string with the sections that apply and a child-directed flag, bound
> into what the signer signs (§4.7); a verifier states it beside the record's verdict.
## 1. Scope
In scope: the marker of an ad in a stream, its body and digest, and how each is read from a
playlist, an asset list or an MPD (§3); the pod-input record, its rules and signature, and a
form-1 `wai.video.ssai` receipt as its response (§4); the stitched claim, its session, its
rules, its signature and its attestation challenge (§5); composited frames (§6); verification:
each record's and each claim's verdict, each ad's presentations and quartiles, and the spend
that counts, by currency (§7); and the reference producer, the tool and the service (§8).
The consent signal a record of version 2 carries, and what a player and a signer do with it, are
in scope too (§4.7), and so are the invalid-traffic rules a verifier applies to what §7 states
(§12).
Out of scope: a transport; how a player's session key reaches the signer (§4.6 says what the
signer does with it, not how it arrives); the ad decision itself, which stays the ad server's;
how a stitcher conditions a creative into segments; the contents of a winning bid's document
and of a VMAP document, which a record binds by digest and this revision does not read; the
SCTE-35 messages a stream may carry; a billing period, an invoice or a payment, which an
operator's system makes from what §7 states; and the detection of invalid traffic by any means
but the rules of §12, which stays statistical and the measurer's (ad-presentation §13).
Relationship to the core spec: `wai.video.stitched_presentation` 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 [ad-presentation](ad-presentation.md): that extension's claim names an
impression of a break-form receipt of a room; it puts the stitched form of `wai.video.ssai`
out of its scope, since such receipts name no session of a room. This extension brings stitched
and interstitial ads into scope with a claim of its own. It takes from ad-presentation, by
reference: the creative digest (§4 there); the span and its rules (§5.2, §5.4 rules 4 to 6);
the viewability rules, the qualifying run and the quartile (§6); the token, its challenge and
its issuer's verifier (§7); and the presentation fields of a policy (§8), here carried in the
record. It changes nothing in ad-presentation.
Relationship to [live-ads](live-ads.md) and `wai.video.ssai`: a room's breaks keep live-ads'
policy and break-form receipts. A stitcher that runs `wai.video.ssai`'s own deterministic
decision seals a form-1 receipt as before; a record whose response is that receipt (§4.5) lets
a verifier set the receipt's stated quartiles and billable spend beside the ones the players'
claims give. Every receipt of either form keeps its bytes.
Relationship to [live-compose](live-compose.md): a record's frame (§4.1) is live-compose
§5.9's ad frame, and the ad's region and its share are that section's. A record of a session can
show its ad in a room's ad frame on the session's own ad track, which its signer binds to the
room's composition with live-compose §5.10's ad-track binding (§4.6, §6).
## 2. Terms
- **Stream**: an HLS presentation or a DASH presentation, live or on demand.
- **Stitcher**: the server that writes ads into a stream: into its segments (server-side ad
insertion), or as interstitials a player fetches (server-guided ad insertion).
- **Ad server**: the party whose response decides which ads fill a break.
- **Response**: the ad server's answer, as the signer received it: a VAST document, a VMAP
document, or a form-1 `wai.video.ssai` receipt (§4.5).
- **Signer**: the key that signs a pod-input record: the stitcher's, or that of a proxy that
forwards the stitcher's ad requests to the ad server and their responses back.
- **Break**, **avail**: a span of the stream that ads fill, and its duration.
- **Manifest**: a document a player reads a marker from: a Media Playlist, an asset list or an
MPD (§3).
- **Marker**: the fields that place one ad in a stream (§3).
- **Ad identity**: the string a marker carries for its ad (§3), 1 to 255 bytes of UTF-8.
- **Player**, **session key**, **creative**, **span**, **geometry**, **token**, **issuer**,
**basis point**: as in ad-presentation §2.
- **Record**: a pod-input record (§4). **Claim**: a stitched claim (§5).
- **Presented**: the verdict of a claim that passes every step of §7.3.
Integers are little-endian unless this text says otherwise. A digest is SHA-256 (FIPS 180-4),
32 bytes. 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 *short* field is a u8 length and that many bytes; a *long* field is a u16
length and that many bytes. A *text* field is a short field of 1 to 255 bytes of UTF-8.
## 3. Markers
### 3.1 The marker, its body and its digest
A marker is one of three kinds, by its **carriage**:
| carriage | where the stream carries it | its fields |
|---|---|---|
| 1 | an `EXT-X-DATERANGE` tag of a Media Playlist (§3.2) | the tag's `ID` and `START-DATE` |
| 2 | an asset of an HLS interstitial's asset list (§3.3) | the interstitial date range's `ID` and `START-DATE`, and the asset's index in its asset list |
| 3 | an `Event` of a DASH `EventStream` (§3.4) | the `id` of its `Period`, the `schemeIdUri` and `value` of its `EventStream`, and its own `id` |
A marker's **body** is its fields in that order, each a long field of its bytes as the
manifest writes them (an attribute value with its quotes removed; an XML attribute value with
its references replaced, §3.5), with an absent `Period` `id` or `EventStream` `value` empty;
and, for carriage 2, the asset's index as a u16 after the two long fields. A body of a carriage
**reads** when it is exactly those fields, with no byte after them.
The **marker digest** is SHA-256 over `wai:stitched-marker` (ASCII) and 0x01, the carriage (one
byte) and the body.
An HLS date range's `ID` names it within its playlist, and its `START-DATE` is fixed for the
`ID` (`draft-pantos-hls-rfc8216bis-22` §4.4.5.1); together they place an ad on the stream's
timeline, and stay the same across the reloads of a live playlist. A DASH event is named by its
scheme, its value and its `id` within its Period.
### 3.2 Carriage 1: a date range
A Media Playlist is read as its lines: its bytes are UTF-8, split at each line feed with a
carriage return before it dropped, and its first line is `#EXTM3U`. Each line that begins
`#EXT-X-DATERANGE:` is a **date range tag**, and the rest of the line its **attribute list**:
`NAME=VALUE` pairs separated by single commas, each name of `A`–`Z`, `0`–`9` and `-` and given
once; a value is a quoted string, `"` then bytes other than `"`, CR and LF then `"`, or else one
or more bytes other than `"`, `,` and white space. A tag whose list does not read so, or whose
`ID` or `START-DATE` is not a quoted string, names no ad. (These are §4.2's rules for an
attribute list, as live-ads reads them.)
An ad of carriage 1 is a date range tag that carries the attribute the record's `id_attr` names
(§4.1). Its marker is the tag's `ID` and `START-DATE`, and its ad identity is that attribute's
value: a quoted string's contents, or the value as written. A stitcher writes the ad identity
in an attribute of the date range of each ad it stitches; the attribute's name is the
stitcher's (a client attribute, `X-` and a name, by §4.4.5.1), and a record names it.
### 3.3 Carriage 2: an interstitial's asset
An **interstitial date range** is a date range tag (§3.2) whose `CLASS` is the quoted string
`com.apple.hls.interstitial` (Appendix D.2, as live-ads §5.1 writes it). <!-- allow-name: the interstitial date-range class, a protocol identifier -->
Its asset list is the JSON object (RFC 8259) Appendix D.2 defines: its `ASSETS` member is an
array of objects, each with a string member `URI` (live-ads §7.2). An ad of carriage 2 is the
asset at index *k* of the asset list a player was served for an interstitial date range. Its
marker is the date range's `ID` and `START-DATE` and *k*, and its ad identity is the asset's
`URI`.
The asset list is the manifest a claim of carriage 2 names (§5.1), since each viewer may be
served another one under the same date range; the playlist that carries the date range is held
beside it (§7.3 step 6).
### 3.4 Carriage 3: an event
An MPD is read as XML by the subset of §3.5. Its root element is `MPD`; each child `Period` of
the root, each child `EventStream` of a Period that has a `schemeIdUri`, and each child `Event`
of such an EventStream that has an `id` is considered. An ad of carriage 3 is such an Event
that carries the attribute the record's `id_attr` names. Its marker is its Period's `id`, its
EventStream's `schemeIdUri` and `value`, and its own `id`; its ad identity is the value of that
attribute.
### 3.5 The XML subset
An XML document (an MPD of §3.4, a VAST document of §7.2) is read over this subset of XML 1.0,
and one outside it does not read:
- its bytes are UTF-8 with no byte order mark;
- before and after its one root element there are only white space, comments and processing
instructions (an XML declaration among them); a document type declaration, or any other
markup that begins `<!` other than a comment or a CDATA section, is outside the subset;
- an element is a start tag, its content and its end tag with the same name, or an empty-element
tag; a tag's attributes each follow white space and are a name, `=` and a value in double or
single quotes holding no `<`, each name once;
- content is character data, CDATA sections, comments, processing instructions and elements,
nested at most 64 deep, the root counted as 1;
- in character data and attribute values, the references `&lt;`, `&gt;`, `&amp;`, `&quot;`,
`&apos;`, `&#`*n*`;` (decimal) and `&#x`*h*`;` (hexadecimal) are replaced by the character
they name; any other `&` is outside the subset. An attribute value is otherwise taken as
written: no white space in it is normalised.
Elements and attributes are named by their **local name**: the part after the last `:`. An
element's **text** is its character data and CDATA children, joined in order, with white space
(space, tab, CR, LF) removed from both ends.
## 4. The pod-input record
### 4.1 Layout
| size | field | meaning |
|---|---|---|
| 4 | `magic` | `WSI1` (ASCII) |
| 1 | `version` | 1; or 2 for a record that carries the consent signal (§4.7) |
| 1 | `carriage` | how the stream marks its ads, 1 to 3 (§3.1) |
| 16 | `session` | the session the decision was made for (§5.2); 16 zero bytes for a **shared** break, one every viewer of the stream is shown (§4.6) |
| short | `avail_id` | the break, as the signer names it: a text field |
| 4 | `avail_ms` | u32: the avail the ad request asked to fill, in milliseconds, at least 1 |
| 1 | `response_format` | 1 a VAST document; 2 a VMAP document; 3 a form-1 `wai.video.ssai` receipt (§4.5) |
| 32 | `response` | formats 1 and 2: SHA-256 of the response's bytes as the signer received them; format 3: the receipt's identity (its receipt hash) |
| 1 | `id_kind` | what an ad's `serving_id` is: 1 the `AdServingId` of its `InLine` (VAST 4.3 §3.4.3); 2 the `id` of its `Ad` (VAST 4.3 §3.3.2); 3 a form-1 impression's `ad_id` |
| short | `id_attr` | the attribute that carries the ad identity: for carriage 1 an HLS attribute name of `A`–`Z`, `0`–`9` and `-`; for carriage 3 an XML name (§4.4); for carriage 2 no bytes |
| 3 | `currency` | the prices' currency: an ISO 4217 alphabetic code, three of `A`–`Z` |
| 1 | `exponent` | its minor unit, as ISO 4217 lists it: 0 to 4 |
| 13 | `frame` | the frame the break is shown in (§4.3) |
| 4 + long | measurement terms | `presentation`, `viewability`, `token_type` (u16) and `issuer` (long), as ad-presentation §8 defines them |
| 1 | `n` | the ads' count, 1 to 16 |
| … | `ads` | `n` ads, §4.2 |
| long + 2 + 2 × `k` | consent signal | version 2 only: `gpp`, `k`, `gpp_sid` and `child_directed` (§4.7) |
| 32 | `signer` | the signer's Ed25519 key |
| 64 | `sig` | §4.4 |
### 4.2 An ad
| size | field | meaning |
|---|---|---|
| 1 | `slot` | its place in the pod: 0, 1, 2, … in order |
| 32 | `marker` | its marker digest (§3.1) |
| short | `ad_id` | the ad identity its marker carries (§3): a text field |
| short | `serving_id` | its identity in the response, as `id_kind` names it: a text field |
| short | `creative_id` | its creative's universal identifier, a `UniversalAdId`'s text (VAST 4.3 §3.7.1), or no bytes |
| short | `registry` | that identifier's `idRegistry`; no bytes exactly when `creative_id` has none |
| 32 | `creative_hash` | the creative digest (ad-presentation §4) of the media objects the stream serves for it |
| 4 | `duration_ms` | u32: the sum of those objects' durations, at least 1 |
| 8 | `price_micros` | u64: the price of one presented impression, in millionths of a minor unit of `currency`, at most 10^12 |
| 1 | `price_source` | 1 the response's `Pricing` (VAST 4.3 §3.4.8) of model CPM, checked (§7.2); 2 a winning bid's price; 3 a price the signer states |
| 32 | `evidence` | for `price_source` 2, SHA-256 of the winning bid's document, which this revision binds and does not read; otherwise 32 zero bytes |
A price is per presented impression, so that the spend of §7.4 is a count of presentations
times a price. A CPM price *P* in a currency of exponent *e* is *P* × 10^(*e* + 3) millionths of
a minor unit per impression: *P* × 10^*e* minor units a thousand impressions, times 10^6 over
1000. CPM 25.00 in a currency of two decimals is 2 500 000.
### 4.3 The frame
| size | field | meaning |
|---|---|---|
| 1 | `kind` | 0 the full picture; 3 a squeeze; 4 a double box (live-compose §5.9) |
| 2 | `width` | u16: the picture's width in pixels |
| 2 | `height` | u16: its height |
| 1 | `corner` | live-compose §5.9's `corner`: a squeeze's program window's corner, 1 to 4; a double box's program side, 1 left, 2 right |
| 2 | `window_w` | u16: a squeeze's program window's width; otherwise 0 |
| 2 | `window_h` | u16: its height; otherwise 0 |
| 2 | `gap` | u16: a double box's gap; otherwise 0 |
| 1 | `aspect` | a double box's tile aspect, live-compose §3.1's `aspect`, 0 to 3; otherwise 0 |
A frame of kind 0 has every other field 0. A squeeze or a double box has a `width` and a
`height` live-compose §3.4 admits (each even, 16 to 1920, at most 2 073 600 pixels), and
fields live-compose §3.3 admits for its layout: a squeeze's `window_w` from 1 to `width` − 1 and
`window_h` from 1 to `height` − 1, with `gap` and `aspect` 0; a double box's `corner` 1 or 2,
`window_w` and `window_h` 0, and `gap` + 2 at most `width`.
### 4.4 Digest, signature and rules
The **digest** of a record is SHA-256 over its bytes from `magic` through `signer`. The
**signature** is Ed25519 by `signer` over `wai:stitched-pod-input` (ASCII) and 0x01, followed by
the bytes the digest covers.
A reader reads exactly the bytes it is given as one record, in the order of §4.1, and refuses
it `pod_input_malformed` when the bytes end before a field the layout requires or go on after
`sig`, or when:
1. its magic is not `WSI1` or its version not 1 or 2, or its `carriage` is not 1, 2 or 3;
2. `avail_id` is not a text field, or `avail_ms` is 0;
3. `response_format` is not 1, 2 or 3; or `id_kind` is not 1 or 2 with format 1 or 2, or not 3
with format 3;
4. `id_attr` is, for carriage 1, not 1 to 255 bytes of `A`–`Z`, `0`–`9` and `-`; for carriage
3, not an XML name of 1 to 255 bytes (an ASCII letter or `_`, then ASCII letters, digits,
`_`, `-` and `.`); for carriage 2, not empty;
5. `currency` is not three of `A`–`Z`, or `exponent` is above 4;
6. the frame breaks §4.3;
7. the measurement terms break ad-presentation §8, or do not set its bit 0, **claim**: an ad
of a record counts only through a claim;
8. `n` is 0 or above 16; an ad's `slot` is not its index; `ad_id` or `serving_id` is not a text
field; `creative_id` and `registry` are not both empty, or both text fields; `duration_ms`
is 0; `price_micros` is above 10^12; `price_source` is not 1, 2 or 3, is 1 with a response
format other than 1, is 2 with format 3, or is other than 3 with format 3; `evidence` is not
zero exactly when `price_source` is not 2;
9. two ads have one `marker`;
10. of version 2, its consent signal breaks a rule of live-ads §8.6, or, in a shared record
(§4.6), carries a GPP string.
### 4.5 A form-1 receipt as the response
A stitcher that decides by `wai.video.ssai`'s own deterministic decision (SPEC §5) seals a
form-1 receipt, whose signed bytes are unchanged here. A record of format 3 names it by its
identity, and a verifier that holds it checks it (§7.2). Its **JSON carriage**, for a verifier
to be given it, is one JSON object with exactly these members, a byte field lowercase
hexadecimal of its exact length:
| member | value |
|---|---|
| `form` | 1 |
| `avail_id` | a string |
| `manifest_hash` | 32 bytes: the attested manifest's hash |
| `pod_hash` | 32 bytes |
| `impressions` | an array, each `{"slot", "ad_id", "creative_hash", "duration_ms", "price_micros", "quartiles_reached"}`, as live-ads §8.2 writes a form-2 impression |
| `merkle_root` | 32 bytes |
| `budget_spent_micros`, `work_decide_ops`, `joules_micro` | integers |
| `energy_provenance` | its class's label, or `null` unmetered; and the members a class with evidence carries, as live-ads §8.2 has them. The legacy label `HwShunt` does not read in this carriage |
| `grant` | `{"grant_hash", "capability", "joule_ceiling_micro", "funds_ceiling"}`, `funds_ceiling` `null` for none |
| `parent_receipt_hash` | 32 bytes, or `null` |
| `signer_pubkey` | 32 bytes |
| `signer_id` | a string |
| `sig` | 64 bytes |
Its **signing payload** is the concatenation of `wai:video-ssai-receipt` (ASCII) and 0x01; the
length (u32) and bytes of `avail_id`; `manifest_hash`; `pod_hash`; `merkle_root`;
`budget_spent_micros`, `work_decide_ops` and `joules_micro` (u64 each); the class in its signed
encoding (energy-measurement §2.7), the tag byte 0 for none; the grant reference as live-ads
§8.4 writes it; 1 and `parent_receipt_hash`, or 0; the impressions' count (u64) and each one's
leaf (live-ads §8.3); and `signer_pubkey`, then the length (u32) and bytes of `signer_id`. Its
integers are big-endian. `sig` is Ed25519 by `signer_pubkey` over the payload, and the
receipt's identity is BLAKE3 over `wai:video-ssai-receipt-id` (ASCII) and 0x01, the payload
and `sig`. A receipt **verifies** when its signature verifies; its `merkle_root` is the root of
its impressions (live-ads §8.3); its `budget_spent_micros` is their billable spend and, when
`funds_ceiling` is not `null`, at most it; `work_decide_ops` is at least the impressions'
count; its slots are 0, 1, 2, … in order with each `quartiles_reached` at most 5; and its
figure and class cohere as energy-measurement §2 has them.
### 4.6 The session and the shared break
A record of a session is a decision made for one playback session: a personalised stitched
playlist, or the asset list one viewer was served. Its `session` is the session the player's
session key names (§5.2). The signer **MUST** take it from the player's session key, which the
player gives with its request for the session's playlist or asset list, by a means this revision
does not fix (for one, a parameter of that request, which a stitcher forwards in the ad request
it makes for the session). A signer **MUST NOT** derive a session from anything else, and a
claim counts toward a record of a session only when its key names that session (§7.3).
A **shared** record is a decision every viewer of the stream is shown: a break of a linear
channel, or a composited break whose ad region every viewer sees showing the same ad. Each
viewer's player that presents it signs a claim under its own session key, and each claim of a
distinct session that is presented is one presentation (§7.4). A shared break is where a party
that makes session keys of its own can sign claims for sessions nobody watched; a record that
buys such breaks **SHOULD** set ad-presentation §8's **attest** bit, so that each presentation
costs that party a vouched device (ad-presentation §13).
A composited break whose ad region shows each session its own ad, on the session's ad track
(live-compose §5.10), is decided session by session: each session's ads are a record of that
session, as a personalised playlist's are, and never a shared record. The record's frame is the
room's ad frame, and its signer binds each ad to the composition with an ad-track binding
(live-compose §5.10.2), which a verifier refuses when the record is shared, is another
session's, or states a frame other than the descriptor's (live-compose §5.10.3). The program
around the ad stays every viewer's.
### 4.7 The consent signal
A record of version 2 carries, after its ads and before `signer`, the consent signal its
decision was made under: live-ads §8.6's, in this record's layout.
| size | field | meaning |
|---|---|---|
| long | `gpp` | the GPP string the player gave, byte for byte; no bytes when it gave none |
| 1 | `k` | the count of `gpp_sid`, 0 to 16 |
| 2 × `k` | `gpp_sid` | u16 each: the string's sections the player gave as applying, in the order given |
| 1 | `child_directed` | 1 when the request is directed to children, 0 otherwise |
The signal holds live-ads §8.6's rules (§4.4 rule 10), and the parties do with it what that
section says, the signer in the decider's place:
- **The player** gives the string, its sections and a child-directed request with its request
for the session's playlist or asset list, as it gives its session key (§4.6), by a means this
revision does not fix, and **MUST NOT** write, alter or complete a GPP string.
- **The signer** states in the record what it was given, byte for byte, **MUST** pass it
unchanged in every ad request it makes for the decision (an ad request URL's `gpp` and
`gpp_sid` parameters, as the GPP string's text names them, or an OpenRTB 2.6 request's
`Regs.gpp`, `Regs.gpp_sid` and `Regs.coppa`), and **MUST NOT** state a string, a section or a
flag it was not given, or clear a flag it was given.
- **A shared record** (§4.6) is a decision every viewer is shown, and carries no viewer's
string: its `gpp` and `gpp_sid` are empty, and its `child_directed` is 1 when the stream is
directed to children. A signer **MUST NOT** apply one viewer's signal to a break every viewer
sees.
- **What no party infers** is live-ads §8.6's: an empty string is no string given, a flag of 0
is no mark given, and neither names the viewer. A string that is not empty can still link
every record and receipt that carries it (§11).
The signal is in the bytes the digest and the signature cover (§4.4): a record whose signal
changed after signing fails its signature.
## 5. The stitched claim
### 5.1 Layout
| size | field | meaning |
|---|---|---|
| 4 | `magic` | `WSC1` (ASCII) |
| 1 | `version` | 1 in this revision |
| 1 | `carriage` | the marker's carriage (§3.1) |
| 32 | `manifest` | SHA-256 of the manifest the player read the marker from: the Media Playlist for carriage 1, the asset list for carriage 2, the MPD for carriage 3 |
| 16 | `session` | the session its key names (§5.2) |
| 32 | `creative_hash` | the creative presented: its creative digest (ad-presentation §4) |
| 32 | `player` | the session key, which signs the claim |
| 1 | `geometry` | ad-presentation §5.1's: 1 the ad's own pixels (in a frame, its region, §6), 2 a full-screen television output, 3 the player's area |
| 2 | `share_bp` | u16: the share of the picture the ad's region holds, in basis points (§6) |
| 4 | `frames` | u32: as ad-presentation §5.1 |
| long | `marker` | the marker's body (§3.1) |
| short | `ad_id` | the ad identity the marker carries: a text field |
| 2 | `n` | u16: the presented objects' count |
| 32 × `n` | `objects` | the content hashes of the creative's objects the spans touch, in order |
| 2 | `m` | u16: the spans' count |
| 11 × `m` | `spans` | ad-presentation §5.2's spans, over the ad's media time |
| long | `attestation` | a token (ad-presentation §7.1), or no bytes |
| 64 | `sig` | §5.3 |
### 5.2 The session key and the session
A player **MUST** make a fresh Ed25519 key for each playback session of a stream, from its
platform's random generator, and **MUST NOT** use it for another session or derive it from an
identifier of the viewer, the device or an account (ad-presentation §3). The **session** a key
names is the first 16 bytes of SHA-256 over `wai:stitched-presentation-session` (ASCII) and
0x01, and the key (32 bytes). It is a function of a key made for one session alone, so it links
nothing across sessions.
### 5.3 Digest and signature
The **digest** of a claim is SHA-256 over its bytes from `magic` through `attestation`. The
**signature** is Ed25519 by `player` over `wai:stitched-presentation-claim` (ASCII) and 0x01,
followed by the bytes the digest covers.
### 5.4 Reading and rules
A reader reads exactly the bytes it is given as one claim, in the order of §5.1, and refuses it
`stitched_claim_malformed` when the bytes end before a field the layout requires or go on after
`sig`, or when:
1. its magic is not `WSC1` or its version not 1;
2. `carriage` is not 1, 2 or 3, or `marker` is not a body of that carriage that reads (§3.1);
3. `geometry` is not 1, 2 or 3, or `share_bp` is not 1 to 10 000;
4. `ad_id` is not a text field;
5. its objects and spans break ad-presentation §5.4 rules 4 to 6.
### 5.5 The attestation challenge
A token a claim carries answers ad-presentation §7.2's TokenChallenge with the record's
`token_type` and `issuer` and this **redemption context**: SHA-256 over
`wai:stitched-presentation-attest` (ASCII) and 0x01, `carriage` (one byte), the marker digest,
`session`, `creative_hash`, `player`, and the length (one byte) and bytes of `ad_id`, the
claim's own. It is a function of the ad's place in the stream and of the session key, each
known before the ad plays, so a token answers one presentation of one session key; lifted onto
another claim, its `challenge_digest` is not that claim's. The token is verified as
ad-presentation §7.3 verifies one.
## 6. Composited frames
A break may show the ad and the program in one picture: a **squeeze**, the program shrunk into
a window in one corner with the ad in the L around it; or a **double box**, the program and the
ad in two boxes side by side. A record states such a frame (§4.3), and the ad's **region** is
then live-compose §5.9's ad region of a canvas of the frame's `width` and `height` under its
layout, `corner`, program window (`window_w` × `window_h` as `pip_width` × `pip_height`),
`gap` and `aspect`: an integer function of the signed fields, so a player and a verifier compute
the same rectangles. A frame of kind 0 is the full picture, and the region is the whole of it.
- The **share** of the picture the region holds is live-compose §5.9's, in basis points; 10 000
for the full picture. A claim states it as `share_bp`, and a claim whose `share_bp` is not its
record's frame's share is refused (§7.3 step 9).
- A player that measures the ad's own pixels (geometry 1) **MUST** measure the fraction in
view of the ad's **region**, not of the whole picture: the share of the region's area, as the
picture is displayed, that lies within the visible part of its player (ad-presentation §10).
One that cannot place the region states geometry 3, its own area, as ad-presentation §10
allows.
- The viewability rules of ad-presentation §6.1 then apply to the region's pixels: half of the
ad's region in view for the video rule, all of it for the cross-media rule. No text read for
this revision defines a viewability rule of its own for a composited ad; a buyer that weighs a
composited presentation by how much of the picture the ad held reads `share_bp` among the
claim's facts.
- **One presentation is one claim per signing player.** When the room's ad fills the ad
region, every viewer of a composited break sees the same picture, and each player that
presents it signs its own claim; a record of the break is shared (§4.6), and each presented
claim of a distinct session is one presentation. When each session is shown its own ad on its
ad track (live-compose §5.10), the program is every viewer's and the ad region the session's:
the break has a record of each session, whose frame is the composition's ad frame, and the
session's claim of its ad is that record's one presentation (§7.4). Either way, a count of
devices that requested the stream is not a count of presentations.
## 7. Verification
### 7.1 Inputs
A verifier is given:
- **records**: pod-input records, a list of byte strings, in an order;
- **claims**: claims, a list of byte strings, in an order, at most 2^20 of them at once, so that
no spend of §7.4 passes 2^64 − 1 (a price is at most 10^12, and a claim is one presentation
of one ad at most);
- **manifests**: the manifests the players read (playlists, asset lists and MPDs), by their
bytes;
- **responses**: VAST and VMAP documents, by their bytes; and form-1 receipts in their JSON
carriage (§4.5);
- **creatives**: the creatives it holds (ad-presentation §4), each its objects;
- **issuers**: the attestation issuers it trusts (ad-presentation §7.3);
- **signers**: the keys whose records it accepts: the stitchers and proxies the buyer and the
seller agreed on.
### 7.2 Each record
The records are judged first, in order. Each gets one verdict, **accepted** or the reason of
the first step it fails:
1. It reads (§4.4): `pod_input_malformed`.
2. Its signature verifies under `signer` (§4.4): `bad_key`, `bad_signature`.
3. `signer` is among the signers: `pod_signer_unknown`.
4. No record before it that was accepted has its `session` and an ad of a `marker` it has:
`pod_duplicate`. A decision is recorded once.
5. **The response.** When the verifier holds it, the record agrees with it, as below:
otherwise `pod_response_mismatch`.
- Format 1, the response whose SHA-256 is `response`: it reads as XML (§3.5) with a root of
local name `VAST`; and for each ad, exactly one `Ad` child of the root has the ad's
`serving_id` as its identity (for `id_kind` 1, the text of the first `AdServingId` child
of its first `InLine` child; for 2, its `id` attribute); that `Ad` has an `InLine` child;
when `creative_id` is not empty, a `UniversalAdId` child of a `Creative` child of a
`Creatives` child of that `InLine` has the text `creative_id` and the `idRegistry`
`registry`; and when `price_source` is 1, that `InLine`'s first `Pricing` child has
`model` `CPM` and `currency` the record's `currency`, and its text is a decimal number
(digits, then optionally `.` and digits) *P* with *P* × 10^(`exponent` + 3) an integer of
at most 2^64 − 1 equal to `price_micros`.
- Format 3, the receipt whose identity is `response`, among those that read (§4.5): it
verifies (§4.5), and its impressions are the record's ads slot for slot: each one's
`slot` the ad's, its `ad_id` the ad's `serving_id`, and its `creative_hash`,
`duration_ms` and `price_micros` the ad's.
- Format 2 is bound by its digest and not read.
A record whose response the verifier does not hold is accepted with its response unchecked,
and the verification states so (§7.5).
A record that is not accepted names no ad: a claim of one of its ads is judged as though it
were not there.
### 7.3 Each claim
Then the claims, in order. Each gets one verdict, **presented** or the reason of the first
step it fails:
1. It reads (§5.4): `stitched_claim_malformed`.
2. Its signature verifies under `player` (§5.3): `bad_key`, `bad_signature`.
3. Its `player` names its `session` (§5.2): `claim_key_unbound`.
4. **Its record.** Its record is the first accepted record of its `carriage` that has an ad
whose `marker` is the claim's marker digest and whose `ad_id` is the claim's, and whose
`session` is the claim's or which is shared. When an accepted record has such an ad but none
is the claim's this way: `claim_session_unbound`. When none has such an ad:
`claim_no_decision`. From here on the claim names that record and that ad.
5. **Its manifest.** The verifier holds a manifest whose SHA-256 is `manifest`:
`claim_manifest_unknown`.
6. **Its marker.** The marker, with the claim's ad identity, is read from that manifest by §3
under the record's `id_attr`: for carriage 1, the manifest is a Media Playlist with a date
range tag of the marker's `ID` and `START-DATE` whose `id_attr` attribute's value is the
ad identity; for carriage 2, it is an asset list whose asset at the marker's index has the
ad identity as its `URI`, and a manifest the verifier holds is a Media Playlist with an
interstitial date range of the marker's `ID` and `START-DATE`; for carriage 3, it is an MPD
with an Event of the marker's fields whose `id_attr` attribute's value is the ad identity:
`claim_marker_absent`.
7. **One claim a session.** No claim before it passed steps 1 to 6 for the same record, ad and
session: `claim_duplicate`. The first that does is that session's **own claim** of the ad,
whatever its later verdict; steps 8 to 11 are for own claims alone.
8. **The asset.** Its `creative_hash` is the ad's; the verifier holds a creative of that
creative digest whose durations sum to the ad's `duration_ms`; every span ends at most at
that `duration_ms`; and `objects` is exactly the content hashes of the creative's objects
the spans touch, in order (ad-presentation §9.2 step 6): `claim_asset_mismatch`.
9. **The frame.** Its `share_bp` is the share of its record's frame (§6):
`claim_frame_mismatch`.
10. **Viewability.** It is viewable under the record's rule (ad-presentation §6.2, with
`geometry` and the **audible** bit as there): `claim_not_viewable`.
11. **Attestation**, when the record sets **attest**: its token is not empty
(`attestation_absent`); it reads as a token of the record's `token_type`, its
`challenge_digest` is SHA-256 of the TokenChallenge of §5.5, and its issuer's verifier
accepts it under the record's `issuer` (`attestation_invalid`).
### 7.4 Presentations, quartiles and the spend that counts
For each ad of each accepted record:
- its **presentations** are its own claims (one per session, step 7) that are presented. A
record of a session has at most one; a shared record has one for each session whose claim is
presented;
- the **quartile** a presentation reaches is ad-presentation §6.3's, over the claim's spans
with *D* the ad's `duration_ms`: 1 started, then a quarter, a half, three quarters and all of
it, at VAST 4.3 §3.14.1's marks. The ad's **quartiles** are five counts: its presentations
whose quartile is at least 1, 2, 3, 4 and 5;
- its **verdict** is presented when it has a presentation; otherwise `claim_absent` when it has
no own claim, or the verdict of its first own claim;
- its **presented spend** is its `price_micros` times its presentations.
A record's presented spend is the sum over its ads. The **spend** of a verification is, for
each currency, the sum of the presented spend of the accepted records of that currency. Spends
of different currencies are never summed.
A quartile is recomputed from what a player signed, not compared with a beacon count: an ad
whose segments were fetched ahead of its break (a prefetch) and never presented has no claim,
so no presentation and no spend, whatever a request-based count says. For a record of format 3
whose receipt was checked, the verification states the receipt's `quartiles_reached` for each
ad and its `budget_spent_micros` beside the presented ones, so a buyer and a seller see both
figures from the same signed bytes.
### 7.5 What a verification states
- For each record, in order: its digest when it reads, and its verdict; and for an accepted
record, whether its response was checked, each ad's slot, own claims (their indices among the
claims given), presentations, verdict, quartiles, presented spend and, for a checked format-3
response, its stated quartile; the record's presented spend, and its stated spend for a
checked format-3 response; and its frame's share (§6) and ad region, the rectangles of
live-compose §5.9, none for the full picture; and, for a record of version 2, its consent
signal (§4.7), as live-ads §8.6 writes it.
- For each claim, in order: its digest when it reads; its verdict; from step 4 on, the record
and the ad's slot it names; and for an own claim, from step 8 on, the quartile it reaches,
its run and its viewable time (ad-presentation §6.2).
- The spend, by currency.
A verification refuses nothing as a whole: each record and each claim gets its own verdict.
## 8. The producer, the tool and the service
**The reference producer** is `web/ads/stitched.js` of the browser client `joule-moq-web`,
beside ad-presentation's `web/ads/presentation.js`, whose span recorder it uses. From the
manifest a player fetched, it reads the markers and ad identities of §3 (`hlsAds`,
`interstitialAds`, `mpdAds`); it measures the fraction in view of an ad's region in a frame
(`regionVisibleBp`); and it writes and signs the claim with the platform's Ed25519. It needs no
player of a particular kind: a player integration hands it the manifest bytes, the ad's media
time and the observations ad-presentation §10 lists. Its tests require the corpus's claims from
the corpus's manifests, byte for byte.
**The tool**, `wai_stitched_presentation` (feature `stitched_presentation`): `verify` reads a
bundle (the inputs of §7.1 as JSON, in the corpus README's form) and prints what §7.5 states;
`sign-pod` writes and signs a record from a draft with the signer's key; `vectors` writes and
checks the corpus. **The service**, `wai_stitched_presentation serve`, answers `POST /v1/verify`
with the same statement for a bundle and `GET /v1/health`, over HTTP/1.1 on a loopback address
for a reverse proxy to publish; it reads one request a connection, bounds the head, the body
and the time it waits, and holds no state between requests.
## 9. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `pod_input_malformed` | §4.4, §7.2 step 1 |
| `pod_signer_unknown` | §7.2 step 3 |
| `pod_duplicate` | §7.2 step 4 |
| `pod_response_mismatch` | §7.2 step 5 |
| `stitched_claim_malformed` | §5.4, §7.3 step 1 |
| `claim_session_unbound` | §7.3 step 4 |
| `claim_manifest_unknown` | §7.3 step 5 |
| `claim_marker_absent` | §7.3 step 6 |
| `claim_frame_mismatch` | §7.3 step 9 |
A verification also gives live-events' `bad_key` and `bad_signature` (§7.2 step 2, §7.3
step 2), and ad-presentation's `claim_key_unbound` (§7.3 step 3), `claim_no_decision` (step 4),
`claim_duplicate` (step 7), `claim_asset_mismatch` (step 8), `claim_not_viewable` (step 10),
`attestation_absent` and `attestation_invalid` (step 11), and `claim_absent` (§7.4), each at the
step named. None of them refuses a verification: each is one record's, one claim's or one ad's.
## 10. Conformance
[`live-conformance/stitched/`](../live-conformance/stitched/) holds the corpus, written from
fixed seeds by `wai_stitched_presentation vectors gen`: `vectors.json`, which holds the
creatives, the attestation issuer (RFC 9578 Appendix A.2's first test key) and the signers the
verifier holds, the players' session keys, and each vector's records, claims and facts; and
each vector's documents as files of their own. Every document is a fixture written for its
case. The vectors:
- **hls_stitched**: two viewers' personalised playlists with ads stitched in, each ad under a
date range that carries its ad identity; a record per viewer priced from its VAST response;
viewer 2 left before its second ad, whose segments had been fetched ahead of the break, so
that ad has no presentation; a control of each check of a record (an unknown signer, a price
the response does not give, a decision recorded twice, terms that count without a claim, a
flipped signature) and of a claim (a duplicate, a key that does not name the session, a
session the record was not decided for, a marker no record names, a manifest not held, a
manifest without the marker, a truncated claim, a flipped signature);
- **hls_interstitial**: one shared playlist with an interstitial date range and an asset list
for each viewer; records under the cross-media rule with an audible run and attestation, one
ad priced by a winning bid whose document is bound by digest; attested presentations, a claim
with no token and one carrying another presentation's token;
- **squeezeback**: a squeeze on a DASH channel marked by an event; one shared record; three
presentations, among them one that stopped at the midpoint, and claims whose share is not the
frame's, whose region was too little in view, given twice, and of another object;
- **double_box**: a double box on an HLS channel, its record in another currency at a price the
signer states, its VMAP response bound and not read; a presentation measured over the ad's box
and one over the player's area;
- **form1_prefetch**: a stitcher's own decision sealed in a form-1 receipt whose beacons counted
the second ad as started when its first segment was fetched ahead of the break; the viewer
left after the first ad; and a record naming the same receipt at another price;
- **consent**: two viewers' personalised playlists, each with one stitched ad, and a record of
version 2 per viewer: one carrying the GPP string its player gave with its applicable
section, one a child-directed request with no string, each presented; a record whose GPP
string was changed after signing (`bad_signature`), whose ad's claim then names no decision;
a shared record carrying a viewer's string, and a record stating a section with no string
(each `pod_input_malformed`).
A conforming verifier gives every record and claim its verdict and facts, every ad its
presentations, quartiles and spend, and every vector its spend by currency, as the corpus
states. `wai_stitched_presentation vectors verify` and the test `tests/stitched_presentation.rs`
check that every file regenerates byte for byte, that `wai-rs` gives every vector its facts,
and that §9 lists exactly the codes the reference adds; the generator checks that the corpus
gives every reason of §9 and every other code of a claim or a record but `bad_key`.
`live-conformance/stitched_presentation_verify.py` is the second implementation, written from
this text: it shares no code with `wai-rs`; it verifies Ed25519 with WAI's strict rule
(`claim-conformance/_ed25519.py`), BLAKE3 with `claim-conformance/_blake3.py`, a form-1
receipt's leaves and Merkle root as the live-ads corpus's second implementation computes them,
the spans, the run and the quartile and RSASSA-PSS with the ad-presentation corpus's second
implementation, and an ad frame with the live-compose corpus's; it reads playlists, asset lists,
MPDs and VAST documents itself; and it mutates copies of the corpus and requires every copy to
fail. The reference producer's tests (§8) require the corpus's claims from its manifests.
## 11. Security and privacy considerations
- **What a claim proves.** As ad-presentation §13 states for its claim: that the holder of the
session key stated these facts of this ad, at this place of a manifest it holds the bytes of.
Without attestation it removes the third party that strips, redirects or forges beacons, and
the count of segment requests as a count of impressions; it does not remove a dishonest player
or a party that makes session keys of its own, which a shared record (§4.6) is open to.
- **What a record proves.** That its signer stated these inputs of one decision: the response's
digest, the ads, their creatives and prices. A record of format 1 checked against the
response shows the ads and the CPM prices are the response's; one of a winning bid binds the
bid's document without reading it; one of format 3 checked against its receipt shows the
stitcher's decision and its stated quartiles. The signer is the party a buyer trusts for the
decision, and §7.1's signers are who those parties are.
- **The manifest.** A claim names the manifest by its digest, and a verifier that holds the
bytes checks that the stream carried the marker with that ad identity. A personalised playlist
is a viewer's; its bytes reach whoever verifies the claim, and a collector **SHOULD** keep
them no longer than the reconciliation they serve.
- **Linking.** A session key is made for one playback session, and the session derived from it
alone, so claims link nothing across sessions. A record of a session carries that session; a
shared record carries none. A claim carries no identifier of the viewer, the device or an
account.
- **Withholding.** A party between the player and the verifier can withhold a claim; the ad
then has no presentation, which is the seller's lost spend, never the buyer's overpayment.
- **A composited frame.** The region is fixed by the record's frame; a player that states
another share is refused, and one that measures the whole picture where it should measure the
region overstates its fraction in view, which attestation does not bound: a buyer that needs
the measurement itself bounded needs the player's integrity, as ad-presentation §13 says of
every claim. For a session's own ad on its ad track, the binding holds the record's frame to
the room's descriptor (live-compose §5.10.3), so the region the claim's share is checked
against is the one the session's sink draws.
- **The consent signal.** A record of version 2 binds the signal into what the signer signs, so
one whose string, sections or flag changed after signing is refused at its signature, and its
ads count nothing. A verified record shows what the signer stated it was given and passed on
(live-ads §8.6), not that the viewer gave it or that the ad server honoured it. A shared
record carries no viewer's string, so a break every viewer sees links to no viewer's signal.
A record of one session whose `gpp` is not empty can be linked through it, by whoever holds
them, to every other record and every live-ads receipt that carries the same string, in any
session or room, as live-ads §12 says of a receipt: the string is made for the viewer, not for
one session, and is in the bytes the signature covers. What **Linking** above says of claims
does not extend to such a record.
## 12. Invalid traffic rules
### 12.1 The rules and their impressions
A verifier applies [ad-presentation](ad-presentation.md) §14's rules to what §7 states: the
same six rules, the same two marks, the same records (a traffic policy, session origins,
device bindings and the pinned lists, its §14.2 to §14.5), decided the same way from signed
bytes and integers, with no score and no model. This section says only what differs for a
stream. The rules change nothing §7 stated: every record and claim keeps its verdict, every ad
its presentations and its presented spend.
An **impression** here is one session's own claim of one ad of one accepted record (§7.3 step
7). Its **claims** are that own claim and every claim after it that §7.3 refused
`claim_duplicate` for the same record, ad and session; its session key is its own claim's
`player`, its `duration_ms` and `price_micros` the ad's, and it **counted** when its own claim
is presented. A record of a session has at most one impression an ad; a shared record (§4.6)
has one for each session whose claim passed step 7.
The rules read as ad-presentation §14.6 has them, with these terms:
- `ivt_unattested` reads the record's measurement terms: the record sets **attest** (§4.1)
and the own claim's `attestation` has no bytes;
- a device key's sessions are counted over the session keys of every impression of the
verification, across its records. A shared break is where one party can sign claims under
many session keys (§4.6); under **cap** each of those sessions needs an accepted binding,
and one device key binds no more of them than the cap.
### 12.2 The traffic policy and the origin
A traffic policy for a stream is of form 2 (ad-presentation §14.2), its `room` 32 zero bytes.
It is **bound** when its `authority` is one of the verifier's signers (§7.1): the parties
trusted for the decisions are the parties trusted to pin the lists and name the edges.
A session origin's edge is the party that terminated the player's own connection: for the
session's playlist or asset list, or, where the stitcher fetches on the player's behalf, for
the request the player made to the stitcher. An ad request a stitcher makes to an ad server
comes from the stitcher's address, which is never a session's origin. The addendum
ad-presentation read notes of server-side insertion that tracking "coming from one IP address"
is open to being filtered (§4.2.2 there); an origin signed at the player's own connection is
what keeps the address rule from reading the stitcher's.
### 12.3 What a verification states
As ad-presentation §14.7, with each impression named by its record, its ad's slot and its own
claim's index, and every sum of prices stated **by currency** (§7.4): for each currency of an
accepted record, the presented spend, the excluded spend, the withheld spend and the valid
spend, and for each rule and mark the excluded or withheld sum; and each record's valid
spend. Sums of different currencies are never added. A policy that is not bound, or whose
list is not held or does not read, refuses the traffic verification with ad-presentation
§14.8's reason, and §7's statement stands.
### 12.4 Conformance
`stitched.json` of [`live-conformance/ad-traffic/`](../live-conformance/ad-traffic/) holds one
stream, a linear channel's shared break that nine sessions claimed and one session's
personalised playlist in another currency, with everything §7.5 states of it, and vectors
over it: a policy with every rule on (three sessions of one device key in the shared break
against a cap of two, a session at the first address of a listed range, one in a listed IPv6
range, a declared crawler, a session that signed two claims of one ad, a claim given twice
and a session with no binding), the same stream with no policy, and a policy signed by a key
that is not a signer and one of the room form, each refused. Its tools, its test and its
second implementation are ad-presentation §14.9's.