Skip to main content

WAI Extension: Opportunity Report

Mirrored from the canonical text at commit 260cbd09 ().

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 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 §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/, 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 §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable.

2. Terms

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) 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:

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:

classnamethe break
1cueda cued break (§4.1)
2scheduleda 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

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:

6. Attribution

A break is attributed as revenue-share §6 attributes an impression, by its start:

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

fieldsizemeaning
magic4WOR1 (ASCII)
version11 in this revision
issuer32the key that signs the report
contract32the contract’s digest (revenue-share §3.3)
party32the party it is for: the operator or an owner
start, end8 eachu64: the period it states
rooms2 + 32 × ma u16 count, then each room’s id, in strictly ascending order
runs2 + 32 × ra u16 count, then each run’s identity (revenue-share §4.2), in strictly ascending order
lines2 + 75 × la u16 count, then each line (§7.2), in strictly ascending order of (split, class)
breaks8u64: the sum of the lines’ breaks
break_ms8u64: the sum of the lines’ break_ms
slots8u64: the sum of the lines’ slots
sig64§7.4

7.2 Lines

A line is 75 bytes:

fieldsizemeaning
split2u16: the split’s index
class11 cued, 2 scheduled (§4.3)
breaks8u64: the breaks of the class attributed to the split
break_ms8u64: the sum of their durations, in milliseconds
slots8u64: the sum of their slots
unslotted8u64: those of them with no slots (§4.4)
ppm4u32: the party’s share of the split
cited4u32: the records the line cites (§7.3)
cites32their 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 §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 §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:

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

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:

membervalue
channela string: the channel’s id, 1 to 1024 bytes of UTF-8
start, endintegers: the period, in microseconds since 1970
currencya string of three capitals A–Z (ISO 4217)
basisgross, for revenue before the contract’s split, or share, for the party’s part
impressionsan integer: the impressions reported for the period
revenuean integer: the revenue reported, an amount (receipt-ledger §2: millionths of a minor unit)
daysa list of objects, each with exactly day (a day, §2), impressions and revenue
programmesa 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:

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:

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

fieldsizemeaning
magic4WCK1 (ASCII)
version11 in this revision
issuer32the key that signs the check
report32the report’s digest (§7.4)
reported32SHA-256 over the reported statement’s bytes, as given
form11 JSON (§9.1), 2 CSV (§9.2)
statement32the digest of the signed revenue-share statement set beside it (revenue-share §7.5), or zero
floor_ppm, ceiling_ppm4 eachu32: the bounds of §10.3, in millionths of the period’s rate
lines2 + …a u16 count, then each line (§10.2), in strictly ascending order of (scope, key’s bytes)
sig64§10.5

10.2 Lines

fieldsizemeaning
scope11 the period, 2 a day, 3 a programme
keytextempty for the period; the day (YYYY-MM-DD); the programme’s asset_id
breaks, break_ms, slots, unslotted8 eachu64: the opportunities of the scope (§10.3)
impressions, revenue8 eachu64: the reported figures of the scope
fill_ppm8u64: the implied fill (§10.3)
revenue_per_slot8u64: an amount (§10.3)
signed11 when signed figures follow, 0 otherwise
signed_impressions, signed_revenue8 eachonly when signed is 1: §10.4
flags1§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.

A line’s flags are these bits:

bitnameset when
0unprovedimpressions is above 0 and breaks is 0: a figure no break the records prove stands behind
1abovenot the period’s line, its slots and the period’s above 0, and fill_ppm × 10^6 > rate × ceiling_ppm
2belownot the period’s line, its slots and the period’s above 0, and fill_ppm × 10^6 < rate × floor_ppm
3impressions_above_signedsigned is 1 and impressions > signed_impressions
4impressions_below_signedsigned is 1 and impressions < signed_impressions
5revenue_above_signedsigned is 1 and revenue > signed_revenue
6revenue_below_signedsigned 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:

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:

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

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.

operationcommand linedoes
opportunity.createwai 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.verifywai 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.checkwai 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:

reasonraised 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/ 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:

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