extensions/opportunity-report.md: the ad breaks a channel's own signed records prove, per owner and period (WOR1): the cues of a room a share contract names, made into breaks by live-ads and their SCTE 35 sections read for slots, and the program-as-run's ad entries no cue covers, each attributed by its start to the programme on air; a documented JSON and CSV form for a platform's reported figures, never evidence; and the check (WCK1) setting them side by side, with the operator's signed revenue-share statement where given. wai-rs: the opportunity_report module (feature opportunity_report), its SCTE 35 subset reader, the reported statement's reader, the corpus generator wai_opportunity_vectors and tests/opportunity_report.rs with the worked example. revenue_share's the_runs and asset_at become pub(crate), unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
643 lines
38 KiB
Markdown
643 lines
38 KiB
Markdown
# WAI Extension: Opportunity Report
|
||
|
||
> Status: Draft. Registers the record `wai.delivery.opportunity`: the ad opportunities a linear
|
||
> channel's own signed records prove, per content owner and per period, and the check of a
|
||
> platform's reported figures against them. An **opportunity report**, `WOR1`, states for one
|
||
> party of a [revenue-share](revenue-share.md) contract and one period the ad breaks that aired in
|
||
> the programmes of the splits it shares in: each break from the cues of a room the contract
|
||
> names ([live-ads](live-ads.md) §4.3, the SCTE 35 sections they carry read for their slots) or
|
||
> from an ad entry of the channel's signed program-as-run (`wai.video.channel`), placed at its
|
||
> start on a window that aired it and attributed to the programme on air; and, per split and class,
|
||
> the breaks, their seconds and their slots, with the records each line came from. A **reported
|
||
> statement** is what a platform says it counted: a documented JSON or CSV form of impressions and
|
||
> revenue for the period, per day or per programme, which this text reads and never treats as
|
||
> evidence. A **statement check**, `WCK1`, sets the report beside the reported figures: per line,
|
||
> the opportunities, the reported impressions, the implied fill and the revenue per slot, the lines
|
||
> where a reported figure has no break behind it or lies far from the period's own rate, and, where
|
||
> the operator's signed revenue-share statement of the same party and period is given, the signed
|
||
> impressions and revenue beside the reported ones. Integer arithmetic throughout. No new
|
||
> transport, no payment, and no reading of any platform's own report format. The reference
|
||
> implementation is the `opportunity_report` module of `wai-rs` (feature `opportunity_report`);
|
||
> the corpus is [`live-conformance/opportunity/`](../live-conformance/opportunity/), and its second
|
||
> implementation is `live-conformance/opportunity_report_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: the WAI texts this one cites; ANSI/SCTE 35-1 2023r2 through the
|
||
> fields cue-injection §5 writes (its §9.6 to §9.9 and §10.2 and §10.3, as that text read them);
|
||
> ISO/IEC 13818-1 Annex A (the CRC); RFC 4180 (the CSV form of §9.2); RFC 8259; RFC 8032;
|
||
> FIPS 180-4.
|
||
>
|
||
> Why a text of its own, beside revenue-share: a report rests on records the statement does not
|
||
> read (a room's cues and the SCTE 35 sections they carry), needs no invoice, and is useful to an
|
||
> owner whose platform signs no statement; and revenue-share's records keep their bytes. Why the
|
||
> name: the record states the opportunities a channel's records prove, and names both of its
|
||
> parts, the report and the check, under one string.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the evidence a report rests on, the contract, the runs and the rooms (§3); breaks, from
|
||
a room's cues and from the program-as-run, their start, duration and slots (§4); the subset of an
|
||
SCTE 35 section this text reads, and its agreement with the cue that carries it (§5); attribution,
|
||
from a break's start to a split (§6); the report, its lines and citations (§7) and its
|
||
verification (§8); the reported statement, in JSON and in CSV (§9); the check, its lines, figures
|
||
and flags, and the signed statement beside it (§10), and its verification (§11); and the
|
||
operations (§12).
|
||
|
||
Out of scope: a transport; a parser of any platform's own report format, which turns that format
|
||
into §9's; paying an owner, which revenue-share leaves out too; an audience count, which no record
|
||
of this text states (§10.3); and the records this text reads, which their own extensions define,
|
||
verify and leave as they are. No existing record's signed bytes change.
|
||
|
||
Relationship to the core spec: `wai.delivery.opportunity` 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.
|
||
|
||
## 2. Terms
|
||
|
||
- **Contract**, **operator**, **owner**, **party**, **split**, **run**, **window**, **entry**,
|
||
**programme**, **carrier**, **ppm**, **time**, **period**: as revenue-share §2 and §3 define
|
||
them. Time is microseconds since 1970-01-01T00:00:00Z; a period is `[start, end)`.
|
||
- **Room**, **cue**, **break**, **open**, **closed**, **avail**: as live-ads §2 and §4.3.
|
||
- **Issuer**: the Ed25519 key (RFC 8032) that signs a report or a check: the owner itself, the
|
||
operator, or an auditor either chooses. A report's figures are recomputed from the records, so
|
||
an issuer's signature binds the issuer to what it states, and adds nothing a verifier trusts.
|
||
- **Section**: an SCTE 35-1 `splice_info_section()` a cue of form 1 carries (live-events §4.3).
|
||
- **Slots**: the ads one break holds (§4.4).
|
||
- **Day**: a UTC calendar date, written `YYYY-MM-DD`; its span is from its 00:00:00Z (included)
|
||
to the next date's (excluded).
|
||
- *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers in this text's records; the bit fields
|
||
of a section are big-endian, as SCTE 35 writes them. A **digest** is SHA-256 (FIPS 180-4); zero
|
||
is 32 zero bytes. A *text* field is a u16 length and that many bytes of UTF-8.
|
||
|
||
## 3. The evidence
|
||
|
||
### 3.1 The contract
|
||
|
||
The contract is a revenue-share contract (revenue-share §3), in force, and verified as
|
||
revenue-share §8.2 steps 1 and 2 verify one, with the other contracts given for the overlap check.
|
||
It names the channel, the key that signs its runs, its carriers, its owners, its splits and the
|
||
map from each asset to a split, and the rule for an entry that is not a programme.
|
||
|
||
### 3.2 The runs
|
||
|
||
Each run is a `wai.video.channel` receipt in its JSON carriage (revenue-share §4.1), verified as
|
||
revenue-share §8.2 step 4 verifies one: it reads, it verifies, it is of the contract's channel and
|
||
signer, and its window shares no instant with the window of a run before it.
|
||
|
||
### 3.3 The rooms
|
||
|
||
Each room is given as a live-ads verifier is given one (live-ads §9.1), without receipts: its
|
||
`descriptor`, `grants`, `key_log` (the key-log head, or `null`), `chains` and `policy`. It is
|
||
verified as live-ads §9.2 steps 1 to 3 verify one: the room (live-rooms §9), the policy, and the
|
||
schedule its accepted cues make. A room with a gap has no schedule: a schedule read from part of
|
||
the cues is not the room's, and the room is refused `delivery_gap`. The room's id is a carrier of
|
||
kind 1 of the contract, or the room is refused `room_foreign`; a room whose id is that of a room
|
||
given before it is refused `room_repeated`.
|
||
|
||
So the cues are the room's own: each signed by a principal the room's grants permit `cue`, in its
|
||
chain, and held to live-ads' schedule rules. A cue an injector signed ([cue-injection](cue-injection.md))
|
||
is such a cue; its injection record rides in the same chain, and this text does not translate it
|
||
again (cue-injection §8 does).
|
||
|
||
## 4. Breaks
|
||
|
||
### 4.1 Cued breaks
|
||
|
||
Each break of a room's schedule (live-ads §4.3) is a **cued break**:
|
||
|
||
- its **start** is the room descriptor's `t0` plus the `t` of the cue that opens it, in
|
||
microseconds since 1970; a start that does not fit in a u64 lies outside every period;
|
||
- its **duration**, in milliseconds, is a closed break's duration (live-ads §4.3: an out's, from
|
||
its in, or a break's of type 4), and an open break's avail;
|
||
- its **opening cue** is the cue whose digest names it (live-ads §4.4), and its **closing cue**
|
||
the in that closed it, when one did;
|
||
- its **section** is the opening cue's, when the cue is of form 1 (§5).
|
||
|
||
### 4.2 Scheduled breaks
|
||
|
||
The runs are taken in ascending order of `window_start_ms`, and in each the entries of its `aired`
|
||
list in ascending order. An entry of kind `ad` is a **scheduled break**, once: an entry whose leaf
|
||
(revenue-share §4.2) is the leaf of an entry taken in an earlier run is the same break and is
|
||
passed over. Its start is the entry's `start_ms` × 1000, its duration the entry's `duration_ms`,
|
||
and its run and entry those it was taken in. An entry that airs in two windows is one break.
|
||
|
||
A scheduled break is **cued** when the start of a cued break of any room given lies within its
|
||
span, from its start (included) to its start plus its duration × 1000 (excluded). A cued scheduled
|
||
break is counted by its cued break, and is not a break of its own.
|
||
|
||
### 4.3 Classes, and the period
|
||
|
||
A break is in a report when its start lies within the report's period; a break outside it is
|
||
another period's, and is passed over. Every break of a report is of one **class**:
|
||
|
||
| class | name | the break |
|
||
|---|---|---|
|
||
| 1 | **cued** | a cued break (§4.1) |
|
||
| 2 | **scheduled** | a scheduled break no cued break counts (§4.2) |
|
||
|
||
A class is never summed into another: each has its own lines (§7.2).
|
||
|
||
### 4.4 Slots
|
||
|
||
A break's **slots**, and where they come from:
|
||
|
||
1. a cued break whose section states slots (§5.2): those;
|
||
2. another cued break: its room policy's `slots` (live-ads §3.1), the most ads one break holds;
|
||
3. a scheduled break: none. Its record states no slots, and it is **unslotted**.
|
||
|
||
A section that states slots is the content's own signal of the break's placements, and is taken
|
||
over the policy, which states the most a decision fills.
|
||
|
||
## 5. The section a cue carries
|
||
|
||
### 5.1 Reading
|
||
|
||
A section is read as SCTE 35-1 Table 5 lays it out, every field in its order and its width, and
|
||
refused `cue_section_invalid` at the first of these that holds:
|
||
|
||
1. it is shorter than 20 bytes, or its `CRC_32`, the last four bytes, is not the MPEG-2 CRC
|
||
(ISO/IEC 13818-1 Annex A: polynomial 0x04C11DB7, register preset to all ones, no reflection,
|
||
no final inversion) over every byte before them;
|
||
2. its `protocol_version` is not 0;
|
||
3. its `encrypted_packet` is 1: the section is read no further, and states nothing (§5.2);
|
||
4. its command, the `splice_command_length` bytes from byte 14 (after `splice_command_type`, byte
|
||
13), runs past the CRC; a `splice_command_length` of 0xFFF states no length, and the command's
|
||
length is then what step 5 reads of it, or, for a command step 5 does not read, the section
|
||
states nothing past its CRC check;
|
||
5. the command does not read as its table lays it out, filling exactly its length:
|
||
`splice_insert()` (type 0x05): `splice_event_id`, `splice_event_cancel_indicator` and, when it
|
||
is 0, the flags `out_of_network_indicator`, `program_splice_flag`, `duration_flag` and
|
||
`splice_immediate_flag`, then `splice_time()` when the program flag is 1 and the immediate flag
|
||
0, or `component_count` and each component with its `splice_time()` when the immediate flag is
|
||
0, when the program flag is 0; then `break_duration()` (`auto_return`, `duration`) when the
|
||
duration flag is 1; then `unique_program_id`, `avail_num` and `avails_expected`;
|
||
`time_signal()` (type 0x06): one `splice_time()`; `splice_null()` (type 0x00): nothing; a
|
||
`splice_time()` is one byte when its `time_specified_flag` is 0, and five, with `pts_time`,
|
||
when it is 1. Another command is not read;
|
||
6. `descriptor_loop_length` runs past the CRC, or the loop's last descriptor does not end where
|
||
the CRC begins; a descriptor (a tag, a length, that many bytes) runs past the loop;
|
||
7. a descriptor of tag 0x02 whose first four bytes are `CUEI` does not read as a
|
||
`segmentation_descriptor()` (SCTE 35-1 Table 20) fills its length: `segmentation_event_id`,
|
||
`segmentation_event_cancel_indicator`, and, when that is 0, `program_segmentation_flag`,
|
||
`segmentation_duration_flag`, `delivery_not_restricted_flag` and the restriction bits, the
|
||
components when the program flag is 0 (six bytes each), `segmentation_duration` (40 bits) when
|
||
flagged, `segmentation_upid_type`, `segmentation_upid_length` and the UPID,
|
||
`segmentation_type_id`, `segment_num` and `segments_expected`, then, for the types 0x30, 0x32,
|
||
0x34, 0x36, 0x38, 0x3A, 0x44 and 0x46, the two bytes `sub_segment_num` and
|
||
`sub_segments_expected` when two bytes remain. Any other count of bytes remaining refuses it.
|
||
|
||
A section is read only as far as this. Another descriptor is passed over whole.
|
||
|
||
### 5.2 What a section states
|
||
|
||
- **Its id**: a `splice_insert()`'s `splice_event_id`; a `time_signal()`'s opening descriptor's
|
||
`segmentation_event_id`. The **opening descriptor** is the first segmentation descriptor of the
|
||
section not cancelled whose `segmentation_type_id` makes an out (cue-injection §6.2): 0x22, 0x30,
|
||
0x32, 0x34 or 0x36.
|
||
- **Its duration**, in 90 kHz ticks: a `splice_insert()`'s `break_duration()` `duration`, when its
|
||
duration flag is 1; a `time_signal()`'s opening descriptor's `segmentation_duration`, when
|
||
flagged.
|
||
- **Its slots**: the `sub_segments_expected` of its first segmentation descriptor not cancelled
|
||
whose `segmentation_type_id` is a placement opportunity start, 0x34 or 0x36, when that
|
||
descriptor carries the sub-segment fields and `sub_segments_expected` is above 0.
|
||
|
||
A section states nothing more. An encrypted section, a command this text does not read, and a
|
||
`time_signal()` with no opening descriptor state no id and no duration; a form 2 message and a cue
|
||
of form 0 have no section.
|
||
|
||
### 5.3 The section and the cue's summary
|
||
|
||
A producer **MUST NOT** write a summary its message contradicts (live-events §4.3). An opening
|
||
cue's section is refused `cue_section_mismatch` when:
|
||
|
||
- it is a `splice_insert()` whose cancel indicator is 1, or whose `out_of_network_indicator` is 0:
|
||
it states a cancellation or a return, and the cue opens a break;
|
||
- it states an id that is not the cue's `cue_id`;
|
||
- it states a duration *d* ticks and ⌊*d* / 90⌋ is not the cue's `duration_ms`.
|
||
|
||
## 6. Attribution
|
||
|
||
A break is attributed as revenue-share §6 attributes an impression, by its start:
|
||
|
||
- a cued break: its **run** is the run whose window holds its start, and its **entry** the entry of
|
||
that run's schedule whose span holds it. One with no run, or a run and no entry, aired in no
|
||
window the verifier holds, and is refused `outside_window`;
|
||
- a scheduled break: its run and entry are those of §4.2.
|
||
|
||
The entry gives the **asset**: a programme's own `asset_id`, or, for an entry that is not a
|
||
programme, the asset the contract's rule gives (revenue-share §6.2), or none. A break with an asset
|
||
is attributed to the asset's split; an asset the contract does not map is refused
|
||
`asset_unmapped`. A break with no asset is attributed to the contract's between split.
|
||
|
||
## 7. The opportunity report
|
||
|
||
### 7.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WOR1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `issuer` | 32 | the key that signs the report |
|
||
| `contract` | 32 | the contract's digest (revenue-share §3.3) |
|
||
| `party` | 32 | the party it is for: the operator or an owner |
|
||
| `start`, `end` | 8 each | u64: the period it states |
|
||
| `rooms` | 2 + 32 × *m* | a u16 count, then each room's id, in strictly ascending order |
|
||
| `runs` | 2 + 32 × *r* | a u16 count, then each run's identity (revenue-share §4.2), in strictly ascending order |
|
||
| `lines` | 2 + 75 × *l* | a u16 count, then each line (§7.2), in strictly ascending order of (`split`, `class`) |
|
||
| `breaks` | 8 | u64: the sum of the lines' breaks |
|
||
| `break_ms` | 8 | u64: the sum of the lines' `break_ms` |
|
||
| `slots` | 8 | u64: the sum of the lines' slots |
|
||
| `sig` | 64 | §7.4 |
|
||
|
||
### 7.2 Lines
|
||
|
||
A line is 75 bytes:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `split` | 2 | u16: the split's index |
|
||
| `class` | 1 | 1 cued, 2 scheduled (§4.3) |
|
||
| `breaks` | 8 | u64: the breaks of the class attributed to the split |
|
||
| `break_ms` | 8 | u64: the sum of their durations, in milliseconds |
|
||
| `slots` | 8 | u64: the sum of their slots |
|
||
| `unslotted` | 8 | u64: those of them with no slots (§4.4) |
|
||
| `ppm` | 4 | u32: the party's share of the split |
|
||
| `cited` | 4 | u32: the records the line cites (§7.3) |
|
||
| `cites` | 32 | their citation digest (§7.3) |
|
||
|
||
There is one line for each split of which the party holds a share (revenue-share §3.2) and each
|
||
class with at least one break attributed to that split, and none other. A report so states every
|
||
opportunity of every split the party shares in, and the share by which it shares it.
|
||
|
||
### 7.3 Citations
|
||
|
||
Each line cites the records its breaks came from: for a cued break, its opening cue and its
|
||
closing cue (kind 12, each by its event digest, live-events §3.2), its room's policy when its slots
|
||
are the policy's (kind 13, the policy's digest, live-ads §3.2), and its run (kind 10, its identity);
|
||
for a scheduled break, its run (kind 10). `cited` is their number, each record counted once, and
|
||
`cites` is SHA-256 over the bytes `wai:opportunity-cites` (ASCII) and 0x01, followed by each cited
|
||
record's `kind` (u8) and identity (32), in ascending order of (`kind`, identity). Kind 10 is
|
||
revenue-share §7.4's; 12 and 13 are this text's.
|
||
|
||
### 7.4 Digest and signature
|
||
|
||
The **digest** of a report is SHA-256 over its bytes before `sig`. Its signature is Ed25519 by
|
||
`issuer` over the bytes `wai:opportunity-report` (ASCII) and 0x01, followed by the bytes the digest
|
||
covers, 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`.
|
||
|
||
### 7.5 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given as one report, in the order of §7.1, and refuses at
|
||
the first of these that holds: fewer than 4 bytes, `truncated`; a magic other than `WOR1`,
|
||
`malformed`; no fifth byte, `truncated`; a version other than 1, `unknown_version`; the bytes end
|
||
before a field the layout requires, `truncated`; bytes after `sig`, `malformed`. It then refuses
|
||
as `opportunity_report_invalid` a report that breaks any of these:
|
||
|
||
- `start` is below `end`; `issuer` and `party` are not zero;
|
||
- `rooms`, `runs` and `lines` are each in the strictly ascending order §7.1 gives it;
|
||
- each line's `class` is 1 or 2, its `breaks` and `cited` above 0, its `unslotted` at most its
|
||
`breaks`, and its `ppm` 1 to 10^6.
|
||
|
||
## 8. Verifying a report
|
||
|
||
### 8.1 Inputs
|
||
|
||
A verifier is given the **contract**, the **other contracts** it holds of the operator or the
|
||
channel, the **report**, the **runs** and the **rooms**. Its verification document holds them as
|
||
one JSON object (RFC 8259) with exactly the members `contract` and `report` (hexadecimal),
|
||
`contracts` (a list of hexadecimal strings), `runs` (a list of runs in their carriage,
|
||
revenue-share §4.1) and `rooms` (a list of rooms, each an object with exactly the members
|
||
`descriptor` and `policy` (hexadecimal), `grants` (a list of hexadecimal strings), `key_log` (a
|
||
string or `null`) and `chains` (a list of lists of hexadecimal strings, each a principal's events
|
||
in order)). A document that is not such an object is refused `malformed`.
|
||
|
||
### 8.2 Steps
|
||
|
||
It runs these steps and stops at the first refusal, which it reports with the reason and where:
|
||
|
||
1. **The contract**, at `contract`, as revenue-share §8.2 step 1.
|
||
2. **The other contracts**, each at `contracts` and its index, as revenue-share §8.2 step 2
|
||
(`contract_overlap` among its reasons).
|
||
3. **The report**, at `report`: it reads (§7.5: `truncated`, `malformed`, `unknown_version`,
|
||
`opportunity_report_invalid`); its `contract` is the contract's digest and its `party` the
|
||
operator or an owner of the contract: otherwise `contract_mismatch`; its period lies within the
|
||
contract's: otherwise `outside_contract`; its signature verifies under its issuer (§7.4):
|
||
`bad_key`, `bad_signature`.
|
||
4. **The runs**, each at `run` and its index, as revenue-share §8.2 step 4 (`malformed`,
|
||
`run_invalid`, `run_foreign`, `run_overlap`).
|
||
5. **The rooms**, each at `room` and its index: it reads (`malformed`); it verifies (§3.3), a
|
||
refusal being live-ads' reason with live-ads' place after the index, and a gap
|
||
`delivery_gap`; its id is a carrier (`room_foreign`) and not that of a room before it
|
||
(`room_repeated`).
|
||
6. **The cued breaks** in the period (§4.3), the rooms in order and each room's breaks in its
|
||
schedule's order, each at `room`, the room's index, `break` and the break's index in its
|
||
room's schedule: its section, when it has one, reads (§5.1: `cue_section_invalid`) and agrees
|
||
with its opening cue (§5.3: `cue_section_mismatch`); its span shares an instant with the span
|
||
of a cued break in the period of a room before it: `break_overlap` (two rooms that carry the
|
||
channel at once would count one break twice); it is placed and attributed (§6:
|
||
`outside_window`, `asset_unmapped`).
|
||
7. **The scheduled breaks** in the period, in the order of §4.2, each at `run`, the run's index in
|
||
the document, `entry` and the entry's index: attributed (§6: `asset_unmapped`).
|
||
8. **The figures**, at `report`, recomputed as §7.2 and §7.3 compute them for the report's party,
|
||
and compared in this order:
|
||
1. a line whose split and class give the party no break, or that states more breaks than they
|
||
give: `line_unsourced`;
|
||
2. a line whose `cited` or `cites` is not the records it came from: `citation_mismatch`;
|
||
3. any other difference (the rooms, the runs, a line missing, a figure of a line, a total):
|
||
`opportunity_report_mismatch`.
|
||
|
||
A figure that cannot be held in its field is a difference.
|
||
|
||
### 8.3 Outcome
|
||
|
||
- **refuse**: the reason and where.
|
||
- **accept**: the report's digest; the contract's digest; the issuer; the party and whether it is
|
||
the operator or an owner; the period; the rooms and the runs; each break in the period, in the
|
||
order of steps 6 and 7, with its class, its room and its index there (a cued break) or its run
|
||
and entry (a scheduled break), its opening cue's digest or `null`, its start, its duration,
|
||
whether it is open, its slots and where they came from (`section`, `policy` or `null`), what its
|
||
section states (`null` without one), its run, entry and asset, its split, and the records it
|
||
cites; each line, with its cited records; and the totals.
|
||
|
||
A break of a split the party holds no share of is stated with the others, and counted in no line
|
||
of the party's: the outcome shows each party the whole channel's breaks in the period, and its
|
||
report states those it shares in.
|
||
|
||
## 9. The reported statement
|
||
|
||
A **reported statement** is what a platform reports it counted for a channel and a period. It is
|
||
an input, never a signed WAI record and never evidence: a check (§10) names it by the digest of its
|
||
bytes, and states it beside what the records prove. A platform's own report is turned into one of
|
||
these two forms outside this text.
|
||
|
||
### 9.1 JSON
|
||
|
||
One JSON object (RFC 8259) with exactly these members, and optionally `days` and `programmes`:
|
||
|
||
| member | value |
|
||
|---|---|
|
||
| `channel` | a string: the channel's id, 1 to 1024 bytes of UTF-8 |
|
||
| `start`, `end` | integers: the period, in microseconds since 1970 |
|
||
| `currency` | a string of three capitals `A`–`Z` (ISO 4217) |
|
||
| `basis` | `gross`, for revenue before the contract's split, or `share`, for the party's part |
|
||
| `impressions` | an integer: the impressions reported for the period |
|
||
| `revenue` | an integer: the revenue reported, an amount (receipt-ledger §2: millionths of a minor unit) |
|
||
| `days` | a list of objects, each with exactly `day` (a day, §2), `impressions` and `revenue` |
|
||
| `programmes` | a list of objects, each with exactly `asset` (an `asset_id`, 1 to 1024 bytes of UTF-8), `impressions` and `revenue` |
|
||
|
||
An integer is a JSON integer from 0 to 2^64 − 1. An empty list is the same as its member left out.
|
||
|
||
### 9.2 CSV
|
||
|
||
A text of RFC 4180 records, each ended by CRLF or LF (the last line break optional), each of
|
||
exactly four fields. A field is unquoted, holding no comma, double quote, CR or LF, or quoted: a
|
||
double quote, any bytes with each double quote doubled, and a double quote. The first record is
|
||
`scope,key,impressions,revenue`. Then, in this order:
|
||
|
||
- `channel`, `start`, `end`, `currency` and `basis`, each once, its value in `key` and the last two
|
||
fields empty;
|
||
- `period`, once, `key` empty, and the period's `impressions` and `revenue`;
|
||
- each `day`, its day in `key`, then each `programme`, its asset in `key`, each with its
|
||
`impressions` and `revenue`.
|
||
|
||
An integer is `0`, or a digit 1 to 9 followed by digits, at most 2^64 − 1. The text read is the
|
||
JSON form with the same values.
|
||
|
||
### 9.3 Rules
|
||
|
||
A reported statement is refused `reported_invalid` when its text is not one of these forms, or:
|
||
|
||
- its `start` is not below its `end`;
|
||
- a day is not a date of the Gregorian calendar from 1970 to 9999, or its span does not lie within
|
||
the period, or the days are not in strictly ascending order;
|
||
- the programmes are not in strictly ascending order of `asset`'s bytes.
|
||
|
||
It is refused `reported_foreign` when its `channel` is not the contract's, its `currency` not the
|
||
contract's, or its period not the report's.
|
||
|
||
## 10. The statement check
|
||
|
||
### 10.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WCK1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `issuer` | 32 | the key that signs the check |
|
||
| `report` | 32 | the report's digest (§7.4) |
|
||
| `reported` | 32 | SHA-256 over the reported statement's bytes, as given |
|
||
| `form` | 1 | 1 JSON (§9.1), 2 CSV (§9.2) |
|
||
| `statement` | 32 | the digest of the signed revenue-share statement set beside it (revenue-share §7.5), or zero |
|
||
| `floor_ppm`, `ceiling_ppm` | 4 each | u32: the bounds of §10.3, in millionths of the period's rate |
|
||
| `lines` | 2 + … | a u16 count, then each line (§10.2), in strictly ascending order of (`scope`, `key`'s bytes) |
|
||
| `sig` | 64 | §10.5 |
|
||
|
||
### 10.2 Lines
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `scope` | 1 | 1 the period, 2 a day, 3 a programme |
|
||
| `key` | text | empty for the period; the day (`YYYY-MM-DD`); the programme's `asset_id` |
|
||
| `breaks`, `break_ms`, `slots`, `unslotted` | 8 each | u64: the opportunities of the scope (§10.3) |
|
||
| `impressions`, `revenue` | 8 each | u64: the reported figures of the scope |
|
||
| `fill_ppm` | 8 | u64: the implied fill (§10.3) |
|
||
| `revenue_per_slot` | 8 | u64: an amount (§10.3) |
|
||
| `signed` | 1 | 1 when signed figures follow, 0 otherwise |
|
||
| `signed_impressions`, `signed_revenue` | 8 each | only when `signed` is 1: §10.4 |
|
||
| `flags` | 1 | §10.3 and §10.4 |
|
||
|
||
There is one line of the period, and one line for each day and each programme the reported
|
||
statement lists, and none other.
|
||
|
||
### 10.3 Figures and flags
|
||
|
||
The **opportunities** of a line are the report's breaks in the period that are counted in a line
|
||
of the report's (§7.2), restricted to its scope: every one for the period; those whose start lies
|
||
within the day's span for a day; those whose asset (§6) is the programme's for a programme. A break
|
||
with no asset is in no programme's line. `breaks`, `break_ms`, `slots` and `unslotted` are their
|
||
count and sums, as §7.2 sums them.
|
||
|
||
- `fill_ppm` is ⌊`impressions` × 10^6 / `slots`⌋ and `revenue_per_slot` ⌊`revenue` / `slots`⌋,
|
||
computed in integers wide enough not to overflow, a quotient above 2^64 − 1 held as 2^64 − 1;
|
||
both are 0 when `slots` is 0. The implied fill is impressions per slot: a break's slots are
|
||
filled once per viewer, so it is the audience times the share of slots filled, and no record of
|
||
this text states an audience.
|
||
- The **rate** is the period line's `fill_ppm`.
|
||
|
||
A line's `flags` are these bits:
|
||
|
||
| bit | name | set when |
|
||
|---|---|---|
|
||
| 0 | `unproved` | `impressions` is above 0 and `breaks` is 0: a figure no break the records prove stands behind |
|
||
| 1 | `above` | not the period's line, its `slots` and the period's above 0, and `fill_ppm` × 10^6 > rate × `ceiling_ppm` |
|
||
| 2 | `below` | not the period's line, its `slots` and the period's above 0, and `fill_ppm` × 10^6 < rate × `floor_ppm` |
|
||
| 3 | `impressions_above_signed` | `signed` is 1 and `impressions` > `signed_impressions` |
|
||
| 4 | `impressions_below_signed` | `signed` is 1 and `impressions` < `signed_impressions` |
|
||
| 5 | `revenue_above_signed` | `signed` is 1 and `revenue` > `signed_revenue` |
|
||
| 6 | `revenue_below_signed` | `signed` is 1 and `revenue` < `signed_revenue` |
|
||
|
||
Every other bit is 0. `above` and `below` are relative to the period's own rate, at bounds the
|
||
issuer chooses and the check states: a day or programme far from the rest of the period. A
|
||
reported statement low in every line is low against nothing but the signed statement (§10.4).
|
||
|
||
### 10.4 The signed statement beside it
|
||
|
||
Where the operator's signed revenue-share statement of the report's contract, party and period is
|
||
given, verified as revenue-share §8 verifies it, the period's line is `signed`, and states:
|
||
|
||
- `signed_impressions`: the sum of the statement's lines' `impressions`;
|
||
- `signed_revenue`: for the basis `gross`, the sum of its lines' `revenue`; for `share`, its
|
||
`total`, the party's part.
|
||
|
||
So the three stand side by side: the opportunities the cues and runs prove, the impressions and
|
||
revenue the invoices' signed records prove, and the figures the platform reported. A day's or a
|
||
programme's line is never `signed`: the statement's lines are per split, and the party's part is
|
||
rounded once per line (revenue-share §7.3).
|
||
|
||
### 10.5 Digest and signature
|
||
|
||
The **digest** of a check is SHA-256 over its bytes before `sig`. Its signature is Ed25519 by
|
||
`issuer` over the bytes `wai:statement-check` (ASCII) and 0x01, followed by the bytes the digest
|
||
covers, verified strictly (SPEC §7 *Signatures*).
|
||
|
||
### 10.6 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given as one check, as §7.5 reads a report with `WCK1` for
|
||
`WOR1` (`truncated`, `malformed`, `unknown_version`), and refuses as `statement_check_invalid` one
|
||
that breaks any of these:
|
||
|
||
- `issuer` is not zero; `form` is 1 or 2; `floor_ppm` is at most 10^6 and `ceiling_ppm` at least
|
||
10^6;
|
||
- its first line is of scope 1 with an empty key, and no other line is of scope 1; the lines are in
|
||
the strictly ascending order §10.1 gives them; each `scope` is 1, 2 or 3; a day's key is a day
|
||
(§9.3) and a programme's 1 to 1024 bytes of UTF-8;
|
||
- `signed` is 0 or 1, and 1 only on the period's line; no bit of `flags` above bit 6 is set.
|
||
|
||
## 11. Verifying a check
|
||
|
||
### 11.1 Inputs
|
||
|
||
A check's verification document is one JSON object with exactly the members `report` (a report's
|
||
verification document, §8.1), `reported` (an object with exactly `form`, `json` or `csv`, and
|
||
`text`, the reported statement's text), `statement` (a revenue-share verification document,
|
||
revenue-share §8.1, or `null`) and `check` (hexadecimal). A document that is not such an object is
|
||
refused `malformed`. The reported statement's bytes are `text` in UTF-8.
|
||
|
||
### 11.2 Steps
|
||
|
||
It runs these steps and stops at the first refusal:
|
||
|
||
1. **The report**, at `report` and the place §8.2 gives: every reason of §8.
|
||
2. **The reported statement**, at `reported`: it reads in its form and keeps §9.3's rules
|
||
(`reported_invalid`), and is of the contract's channel and currency and the report's period
|
||
(`reported_foreign`).
|
||
3. **The signed statement**, when given, at `statement` and the place revenue-share §8.2 gives:
|
||
every reason of revenue-share §8; it is of the report's contract, party and period, or it is
|
||
refused `statement_foreign`.
|
||
4. **The check**, at `check`: it reads (§10.6: `truncated`, `malformed`, `unknown_version`,
|
||
`statement_check_invalid`); its signature verifies under its issuer (§10.5: `bad_key`,
|
||
`bad_signature`); and its `report`, `reported`, `form` and `statement` are those given and its
|
||
lines those §10.2 to §10.4 compute at its own bounds: otherwise `statement_check_mismatch`.
|
||
|
||
### 11.3 Outcome
|
||
|
||
- **refuse**: the reason and where.
|
||
- **accept**: the check's digest; the issuer; the report's digest, its party and period; the
|
||
reported statement's digest, form and basis; the signed statement's digest or `null`; the
|
||
bounds; and each line with its figures and the names of its flags.
|
||
|
||
## 12. Operations
|
||
|
||
`wai` runs the operations of this extension, under the agent rules of the operation catalogue (the
|
||
`--json` envelope, the exit codes, `--dry-run` on every operation that writes, `--yes` before a file
|
||
is replaced). A refusal by this text has its reason as the operation's code.
|
||
|
||
| operation | command line | does |
|
||
|---|---|---|
|
||
| `opportunity.create` | `wai opportunity create <inputs.json> --party <key> --start µs --end µs --key <file> [--out F]` | the report of a party and a period, computed from a verification document without its report (§8.1), signed by the issuer key, written with its document |
|
||
| `opportunity.verify` | `wai opportunity verify <document.json>` | a report's (§8.1) or a check's (§11.1) verification document: its outcome; exit 1 with the reason on a refusal |
|
||
| `statement.check` | `wai statement check <report.json> <reported> [--statement <document.json>] [--floor ppm] [--ceiling ppm] --key <file> [--out F]` | the check of a reported statement (JSON, or CSV for a file named `.csv`) against a report's verification document and, when given, a signed statement's, at the bounds given (500000 and 2000000 when not), signed by the issuer key, written with its document |
|
||
|
||
## 13. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `room_foreign` | §3.3, §8.2 step 5 |
|
||
| `room_repeated` | §3.3, §8.2 step 5 |
|
||
| `cue_section_invalid` | §5.1, §8.2 step 6 |
|
||
| `cue_section_mismatch` | §5.3, §8.2 step 6 |
|
||
| `opportunity_report_invalid` | §7.5, §8.2 step 3 |
|
||
| `opportunity_report_mismatch` | §8.2 step 8 |
|
||
| `reported_invalid` | §9.3, §11.2 step 2 |
|
||
| `reported_foreign` | §9.3, §11.2 step 2 |
|
||
| `statement_foreign` | §11.2 step 3 |
|
||
| `statement_check_invalid` | §10.6, §11.2 step 4 |
|
||
| `statement_check_mismatch` | §11.2 step 4 |
|
||
|
||
A verifier of this extension also raises, with the meaning this text gives them: `truncated`,
|
||
`malformed` and `unknown_version` for a report or a check that does not read, and `malformed` for a
|
||
document or a room that does not read; `bad_key` and `bad_signature` for their signatures;
|
||
`contract_mismatch` and `outside_contract` (revenue-share §11) for a report of another contract or
|
||
past its period; `delivery_gap` (live-events §6.4) for a room with a gap; `break_overlap` (live-ads
|
||
§4.3) for the breaks of two rooms at once; `outside_window` and `asset_unmapped` (revenue-share §11)
|
||
for a break no window places and an asset the contract does not map; `line_unsourced` and
|
||
`citation_mismatch` for a line, as revenue-share raises them; and, at a contract, a run, a room or
|
||
a signed statement, every code revenue-share and live-ads raise there.
|
||
|
||
## 14. Conformance
|
||
|
||
[`live-conformance/opportunity/`](../live-conformance/opportunity/) holds the corpus, in one file,
|
||
`vectors.json`, written by `wai_opportunity_vectors gen` from fixed seeds. The contracts, runs,
|
||
rooms, ledger documents and statements are held once, by name, and each vector is a report's or a
|
||
check's verification document naming them, with the outcome §8.3 or §11.3 gives it:
|
||
|
||
- **Accepted**: a channel's day under a contract of two owners: two windows airing each owner's
|
||
programmes, a room whose producer cues breaks inside them, with SCTE 35 sections of every kind
|
||
§5 reads (a `splice_insert()` with a break duration, a `time_signal()` with a placement
|
||
opportunity stating its slots, one whose type states none, a `splice_insert()` carrying a
|
||
segmentation descriptor) and cues of form 0, a break inside an ad entry, and an ad entry no cue
|
||
covers; the reports of both owners and of the operator, and one made from the runs alone; and
|
||
checks of reported statements in JSON and in CSV, with and without the operator's signed
|
||
statement of the day, one under-reporting an owner's programme, one reporting impressions in a
|
||
programme with no break.
|
||
- **Refused**: each reason of §13 at least once, each vector a control of one defect; each code
|
||
§13 reuses; and a refusal at a contract, a run, a room and a signed statement, at its place.
|
||
|
||
`wai_opportunity_vectors verify` and the test `tests/opportunity_report.rs` check that the file
|
||
regenerates byte for byte, that `wai-rs` gives every vector its outcome, that every reason of §13
|
||
refuses a vector, and that §13 lists exactly the reference's codes; the test also holds the worked
|
||
example, a channel's day of two owners whose reported statement under-reports one owner's breaks,
|
||
with the check that shows it, computed and then recomputed from the public records alone.
|
||
`live-conformance/opportunity_report_verify.py` is the second implementation, written from this
|
||
text: it shares no code with `wai-rs`, reads every report, check, section and reported statement
|
||
itself, verifies contracts, runs and signed statements with `revenue_share_verify.py` and rooms with
|
||
`live_ads_verify.py`, recomputes every break, attribution, line, citation and flag, gives every
|
||
vector the same outcome, requires every mutated copy of the corpus to fail, and drops each of its
|
||
own checks in turn and requires the corpus to catch the weakened verifier.
|
||
|
||
## 15. Security and privacy considerations
|
||
|
||
- **What a report proves.** A break in a report is one the room's own cues opened, signed by a
|
||
principal the room's grants permit to cue, or an ad entry the channel signer signed as aired,
|
||
placed in a window the channel signer signed. Nothing of it rests on the platform's figures or
|
||
on the issuer's word: anyone holding the records computes the same report.
|
||
- **What it does not.** Opportunities are per break, not per viewer: a report says what was on air
|
||
to be sold, not how many saw it. The fill of a check is therefore impressions per slot, and its
|
||
`above` and `below` flags are relative to the period's own rate. Only a signed statement states
|
||
signed impressions; where none is given, a reported statement uniformly low or high is not
|
||
flagged by the report alone.
|
||
- **What the operator still controls.** A room the contract does not name, or a channel carried
|
||
without cues or runs, proves no opportunity; the contract's carriers and its channel signer are
|
||
what the owners agreed to. Two rooms carrying the channel at once are refused rather than
|
||
counted twice (`break_overlap`), and so are a cue whose message its summary contradicts and a
|
||
break no window places.
|
||
- **The reported statement.** It is labelled and kept apart: a check names it by the digest of its
|
||
bytes, and no figure of it changes a report. A platform that disputes a check recomputes it from
|
||
the same records and its own reported bytes.
|
||
- **What a document reveals.** A report's document holds the room's chains, whose events name the
|
||
principals of the room and its other events (chat among them, live-events §4.4); a check's holds
|
||
a signed statement's, with the ledger's sources (revenue-share §13). Each travels between the
|
||
owner, the operator, the platform and an auditor they choose.
|