# 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. Every kind a citation digest names, these among them, is registered in [receipt-ledger](receipt-ledger.md) §5.2. ### 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 --party --start µs --end µs --key [--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 ` | 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 [--statement ] [--floor ppm] [--ceiling ppm] --key [--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.