From 372f063bcb06287bcce83a981f03ca0ca18f7a87 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 29 Oct 2026 18:12:30 -0400 Subject: [PATCH 1/5] wai: the opportunity report and the statement check (wai.delivery.opportunity) 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 --- wai/extensions/opportunity-report.md | 643 +++ wai/live-conformance/opportunity/README.md | 87 + wai/live-conformance/opportunity/vectors.json | 5106 +++++++++++++++++ wai/wai-rs/Cargo.toml | 18 + wai/wai-rs/src/bin/wai_opportunity_vectors.rs | 61 + wai/wai-rs/src/lib.rs | 8 + wai/wai-rs/src/opportunity_report.rs | 1548 +++++ wai/wai-rs/src/opportunity_report/corpus.rs | 835 +++ wai/wai-rs/src/opportunity_report/reported.rs | 341 ++ wai/wai-rs/src/opportunity_report/section.rs | 468 ++ wai/wai-rs/src/revenue_share.rs | 10 +- wai/wai-rs/tests/opportunity_report.rs | 173 + 12 files changed, 9294 insertions(+), 4 deletions(-) create mode 100644 wai/extensions/opportunity-report.md create mode 100644 wai/live-conformance/opportunity/README.md create mode 100644 wai/live-conformance/opportunity/vectors.json create mode 100644 wai/wai-rs/src/bin/wai_opportunity_vectors.rs create mode 100644 wai/wai-rs/src/opportunity_report.rs create mode 100644 wai/wai-rs/src/opportunity_report/corpus.rs create mode 100644 wai/wai-rs/src/opportunity_report/reported.rs create mode 100644 wai/wai-rs/src/opportunity_report/section.rs create mode 100644 wai/wai-rs/tests/opportunity_report.rs diff --git a/wai/extensions/opportunity-report.md b/wai/extensions/opportunity-report.md new file mode 100644 index 000000000..6aa6b1254 --- /dev/null +++ b/wai/extensions/opportunity-report.md @@ -0,0 +1,643 @@ +# 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 --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. diff --git a/wai/live-conformance/opportunity/README.md b/wai/live-conformance/opportunity/README.md new file mode 100644 index 000000000..432618e63 --- /dev/null +++ b/wai/live-conformance/opportunity/README.md @@ -0,0 +1,87 @@ +# live-conformance/opportunity — the opportunity report and the statement check + +The corpus of [`extensions/opportunity-report.md`](../../extensions/opportunity-report.md) §14, in +one file, `vectors.json`: + +- `contracts`: share contracts (revenue-share §3), by name, each hexadecimal; +- `runs`: the channel's program-as-run windows, by name, each in its carriage (revenue-share §4.1); +- `rooms`: rooms, by name, each as a report's document holds one (§8.1): its `descriptor`, `grants`, + `key_log`, `chains` and `policy`; +- `ledgers`: ledger documents (receipt-ledger §7.1), by name, each whole, for the signed + statements; +- `reported`: reported statements (§9), by name, each its `form` and `text`; +- `vectors`: verification documents, each of `kind` `report` (§8.1: the names of its `contract`, + other `contracts`, `runs` (or `null`) and `rooms`, and its `report` in hexadecimal) or `check` + (§11.1: such a `report`, the name of its `reported` statement, its signed `statement` (the name + of its contract, the statement in hexadecimal, the names of its runs and ledgers) or `null`, and + its `check` in hexadecimal), with the `outcome` §8.3 or §11.3 gives it; +- `keys`: the operator's, the two owners', the channel signer's, the auditor's, the room + producer's and the ad decider's public keys. + +**Equivalence.** For each vector, a conforming verifier given its parts, resolved by name, reaches +the outcome the vector states: + +- `refuse`: the `reason` and where, `at`: `[]` for the document; `["contract"]` and the places + revenue-share §8.2 gives; `["report"]`; `["run", k]`; `["room", k]`, followed by live-ads' own + place for a room live-ads refuses; `["room", k, "break", i]` for a cued break (its index in its + room's schedule); `["run", k, "entry", e]` for a scheduled break; for a check, `["report", …]`, + `["reported"]`, `["statement", …]` and `["check"]`; +- `accept`, for a report: its digest, the contract's, the `issuer`, the `party` and its `role`, + `start` and `end`, the `rooms` and `runs`, each break in the period (its class, its room and + index or the run and entry it was taken in, its opening `cue`, its `start`, `duration_ms`, + whether it is `open`, its `slots` and where they came from, what its `section` states, its run, + entry, asset and split, and the records it `cites`), each line (split, class, breaks, + `break_ms`, slots, unslotted, `ppm`, `cites`), and the `totals`; +- `accept`, for a check: its digest, the `issuer`, the report's digest, `party`, `start` and + `end`, the reported statement's digest, `form` and `basis`, the signed `statement`'s digest or + `null`, the bounds, and each line with its figures, its `signed` figures or `null`, and the names + of its `flags`. + +The channel, `fast-9`, airs two windows of 2026-10-16: the first hour (owner A's `a/ep-1`, the ad +slot `slate-90`, owner B's `b/doc-1`) and the second (A's `a/ep-2`, the ad slot `mid-60`, B's +`b/doc-2`). Its contract maps each owner's programmes to a split of its own (a third to A and two +thirds to the operator; three fifths to B and two fifths to the operator), names a between split +of halves to the two owners, and the rule preceding. The room's producer cues eight breaks: an out +and its in carrying a `splice_insert()` with a break duration (slots from the room's policy, two), +breaks of type 4 carrying `time_signal()`s with provider and distributor placement opportunities +stating three or four slots, one of them inside `slate-90`, an out and its in with such a section, +a `splice_insert()` carrying a placement opportunity of three slots, a provider advertisement +start (a type that states no slots), and a cue of form 0. No cue covers `mid-60`, which is a +scheduled break, unslotted. The ledger document bills three viewers' receipts for every break, +two impressions each; the signed statements are the operator's, of the same day. + +The accepted vectors: + +- `owner_a_day`, `owner_b_day`, `operator_day`: the day's reports, each signed by the party it is + for; +- `owner_a_runs_alone`: A's day from the runs alone, both ad slots scheduled; +- `owner_b_two_rooms`: B's day under a contract naming a second room, whose break overlaps none; +- `check_owner_b_under_reported`: B's platform reports twelve impressions for `b/doc-1` and two for + `b/doc-2`, which holds as many slots: `b/doc-2` is `below` the day's rate, and the day is below + the signed statement's twenty-four impressions and B's signed part; +- `check_owner_a_csv`: A's reported statement in CSV, per day, as the signed statement states it: + nothing flagged; +- `check_owner_b_without_statement`, `check_owner_b_reports_a_programme`: B's report alone beside + the same figures, and beside figures listing A's episode, which is `unproved` (no break of B's + aired in it). + +Every other vector is refused, a control of one defect: each reason of §13, each code it reuses, +and a refusal at a contract, a run, a room and a signed statement, at its place. + +**Provenance.** Generated here: `wai_opportunity_vectors gen` writes `vectors.json` from fixed +seeds (the operator, the owners, the channel signer, the auditor, the buyer, and the room's +authority, clock, producer and decider). Nothing here was fetched from outside the repository. + +Regenerate and check: + + cd wai/wai-rs + cargo run -q --no-default-features --features opportunity_report --bin wai_opportunity_vectors -- gen ../live-conformance/opportunity + cargo run -q --no-default-features --features opportunity_report --bin wai_opportunity_vectors -- verify ../live-conformance/opportunity + cargo test -q --no-default-features --features opportunity_report --test opportunity_report + python3 -I ../live-conformance/opportunity_report_verify.py ../live-conformance/opportunity + +The second implementation, `opportunity_report_verify.py`, shares no code with `wai-rs`. It 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, requires every mutated copy of the corpus to fail, and +requires the corpus to catch it with each of its own checks dropped. diff --git a/wai/live-conformance/opportunity/vectors.json b/wai/live-conformance/opportunity/vectors.json new file mode 100644 index 000000000..e1eff5f2e --- /dev/null +++ b/wai/live-conformance/opportunity/vectors.json @@ -0,0 +1,5106 @@ +{ + "corpus": "wai.delivery.opportunity", + "extension": "extensions/opportunity-report.md", + "keys": { + "operator": "c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd", + "owner_a": "50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a9", + "owner_b": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "channel_signer": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "auditor": "81640fe9c7b63a1babed05713ed2f9958aa011b8d31edcf9c611f37513868737", + "producer": "a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b", + "decider": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad" + }, + "contracts": { + "main": "5752433101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd00a0cd18bc5c06000040e2b52b5f06004555520600666173742d392825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972010200010001ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c70250e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a972e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7603002b2c0a00010015160500801a06000101c027090000000000020020a107000120a10700040000000600612f65702d3100000600612f65702d3200000700622f646f632d3101000700622f646f632d320100f97abb9f0e59098e4321c556fd094f1a426a7936ecfa8a44ac17f62a9569d99dd44c64bc5522f10c794ad1da659f463844d0e13c3f4172be887a5a1f698d160266c30b368b5cdc7d60df370f2181b7937e96f7ac783277fd944a2b643a75d58d2f017b333e619553fe46f889aff345c5d2eb067399e3320c0cbb8de84bc4ac00a21cc105df9d1457ac788e6d897a18e8da473939a78973aa55033fbbedf4078a985032d46620ad4c4888b64a23c65a9d855940458a6ff8d776785b807bf19002", + "offered": "5752433101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd00a0cd18bc5c06000040e2b52b5f06004555520600666173742d392825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972010200010001ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c70250e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a972e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7603002b2c0a00010015160500801a06000101c027090000000000020020a107000120a10700040000000600612f65702d3100000600612f65702d3200000700622f646f632d3101000700622f646f632d320100f97abb9f0e59098e4321c556fd094f1a426a7936ecfa8a44ac17f62a9569d99dd44c64bc5522f10c794ad1da659f463844d0e13c3f4172be887a5a1f698d160266c30b368b5cdc7d60df370f2181b7937e96f7ac783277fd944a2b643a75d58d2f017b333e619553fe46f889aff345c5d2eb067399e3320c0cbb8de84bc4ac0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "two_rooms": "5752433101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd00a0cd18bc5c06000040e2b52b5f06004555520600666173742d392825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f97201020002000115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c2101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c70250e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a972e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7603002b2c0a00010015160500801a06000101c027090000000000020020a107000120a10700040000000600612f65702d3100000600612f65702d3200000700622f646f632d3101000700622f646f632d320100ccd9527d72ba978dc645ca188d50904b6ba6de991bafe4f2eec7ac6308e70bcdf3220ab06d58fcce694bcd8201a12c3b821c2729f6316902afd01dcaa6f1b40a9e90684b04af4cfee06d4d8f3d924ffd2113ddda0f443b7b81381fc220dd8912ff409d5b015674c0dc9a3ba629e1114a043eee3d9568181fda31254a5f60e80bb27549feb7b1542217fb9f3ac8afd7f121f42cdb668e5c026291a419ffbef91d74583e6239a4a02d6d40d2db43fd54999c5c8801250ba1bdb5aa77f99a24ee0f" + }, + "runs": { + "hour_1": { + "channel_id": "fast-9", + "schedule_root": "454671e80ae1720fa153767d918c74478fc6f1dace0f556f17de9b0bfa6f8a82", + "schedule": [ + { + "asset_id": "a/ep-1", + "start_ms": 1792108800000, + "duration_ms": 1800000, + "kind": "program" + }, + { + "asset_id": "slate-90", + "start_ms": 1792110600000, + "duration_ms": 90000, + "kind": "ad" + }, + { + "asset_id": "b/doc-1", + "start_ms": 1792110690000, + "duration_ms": 1710000, + "kind": "program" + } + ], + "window_start_ms": 1792108800000, + "window_end_ms": 1792112400000, + "aired": [ + 0, + 1, + 2 + ], + "work_schedule_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "capability": "video.channel", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": null + }, + "parent_receipt_hash": null, + "signer_pubkey": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "signer_id": "did:key:channel-signer", + "sig": "de5d3d894f7e2a683a581f6bc78ae9a0e65efa3a0d5911f37e0cf0099036d9e71f4d2c3260571ea85fc57b951de2f0898e8a0f84fbb19f7a0b016f7f7c98ac05" + }, + "hour_1_aired_misstated": { + "channel_id": "fast-9", + "schedule_root": "454671e80ae1720fa153767d918c74478fc6f1dace0f556f17de9b0bfa6f8a82", + "schedule": [ + { + "asset_id": "a/ep-1", + "start_ms": 1792108800000, + "duration_ms": 1800000, + "kind": "program" + }, + { + "asset_id": "slate-90", + "start_ms": 1792110600000, + "duration_ms": 90000, + "kind": "ad" + }, + { + "asset_id": "b/doc-1", + "start_ms": 1792110690000, + "duration_ms": 1710000, + "kind": "program" + } + ], + "window_start_ms": 1792108800000, + "window_end_ms": 1792112400000, + "aired": [ + 0 + ], + "work_schedule_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "capability": "video.channel", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": null + }, + "parent_receipt_hash": null, + "signer_pubkey": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "signer_id": "did:key:channel-signer", + "sig": "e16381690e0aa7f595645b2852900f851197019b6bd70e07081776f37601bc554a39e3dd85321110af2cfae7cca3b676d6fa581c1cdf3aa7274b14106a8f5b0a" + }, + "hour_2": { + "channel_id": "fast-9", + "schedule_root": "962dff9c86732707b10ab4965bb6e8670c7f32ad0359db65604f5c9370ed3ff6", + "schedule": [ + { + "asset_id": "a/ep-2", + "start_ms": 1792112400000, + "duration_ms": 1740000, + "kind": "program" + }, + { + "asset_id": "mid-60", + "start_ms": 1792114140000, + "duration_ms": 60000, + "kind": "ad" + }, + { + "asset_id": "b/doc-2", + "start_ms": 1792114200000, + "duration_ms": 1800000, + "kind": "program" + } + ], + "window_start_ms": 1792112400000, + "window_end_ms": 1792116000000, + "aired": [ + 0, + 1, + 2 + ], + "work_schedule_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "capability": "video.channel", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": null + }, + "parent_receipt_hash": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "signer_pubkey": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "signer_id": "did:key:channel-signer", + "sig": "7d59007843abbce0cc2839c622297c80ca2ab10f9891ee64efe1620e152b9b73c7fa813f8807831530205889caff3e91de4f1376700aa081e21a0cabbb6b010b" + }, + "hour_2_unmapped": { + "channel_id": "fast-9", + "schedule_root": "8806b6120f25d613d15a1ee0e19e42558c72c197fb3a1930366c73510d9c1bd2", + "schedule": [ + { + "asset_id": "a/ep-2", + "start_ms": 1792112400000, + "duration_ms": 1740000, + "kind": "program" + }, + { + "asset_id": "mid-60", + "start_ms": 1792114140000, + "duration_ms": 60000, + "kind": "ad" + }, + { + "asset_id": "c/film-9", + "start_ms": 1792114200000, + "duration_ms": 1800000, + "kind": "program" + } + ], + "window_start_ms": 1792112400000, + "window_end_ms": 1792116000000, + "aired": [ + 0, + 1, + 2 + ], + "work_schedule_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "capability": "video.channel", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": null + }, + "parent_receipt_hash": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "signer_pubkey": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "signer_id": "did:key:channel-signer", + "sig": "85e94571a0b8f3692b6f8e984ca3ef609785521da566ed838ed9fd2995bac6ca6d1e5d1a251edb134e2ef33f320c4752ddc3e3dbf9c8e7bd2081425612260901" + } + }, + "rooms": { + "backup": { + "descriptor": "5752443101ef57dcb19a2edd125601041956c18e9442b1188c8eddad3df9bffda0c7c7b1c817104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00406cd8e95d060040420f000000000000000f0000000400000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020870726f6475636572000101000e000000000000000000000000000000000000000000000000000000000000000764656369646572000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000355b66c9006f967782196c567a5a7cda3555fc69f1b46d20868e26b8a63b13094fe6e20d7bb93fa5333fb9661b39640019dd1b8294f0897e08ff30ffba4a220a", + "grants": [ + "575247310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a00000000000001010000000000000000000000000000000000000000000000000000000000000000000057455031010e000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff0000000000000028ef1d708ccad44b6e6567347fc6cfda69f4f3020807d10bfa8e36e603ce586d0c469675acd4e40e894130997db6b93cb9cbab8e1ed16fbeba6b3426018d4809", + "575247310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000100000000000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffff0a0000000000000045555200000000cf2674edd0b2bd2fdf31d1a91f190c17e4745f70dd0627ac8cddda4dd3411512a512466c4d10d51f552bf47940f05b0d79a03acbe8e2173164d67b8dba5ca707" + ], + "key_log": null, + "chains": [ + [ + "574556310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c2117104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027bd894bb4700bdc343bcb21a86b2c8ce90614e8b886278e159b50e02f0d1762398b04f1d28aeba12dc8ddfacac5b94c12a4f1abf77997072efd2eeb62485cc50b7ef288ffb680e676fa8523317ba8d7e62d5c5e32843eaa2f0c50b8c9135e15e04" + ], + [ + "574556310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b000000000000000000000000000000000000000000000000000000000000000000000000000000000001b2c40000000000020a00040900000060ea000000348a01a1f64e36932aaf28e4a1501ef3f96eb92a089ad89f308b682715b63b6b486589b0df4c0b9643fa40e63229ec3d19ee0736b000da403d9ed2826f05600c" + ] + ], + "policy": "574150310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21e7b2ef80c780ac88a9eeb975bc452e7f929a870fafd2ab5904a57e982308540902307500000000000000ffff00210068747470733a2f2f6164732e6368616e6e656c2e6578616d706c652f736761692f260068747470733a2f2f63646e2e6368616e6e656c2e6578616d706c652f6372656174697665732fec70c2ddad6afc237ddd51d8d632250b72f134b2b2a67217d2e57aaf565c929fc764e75c384153459373f4bfaea92df5aa2ab01b9f0e34324c0c51d5e1ca8d0c" + }, + "backup_overlapping": { + "descriptor": "5752443101ef57dcb19a2edd125601041956c18e9442b1188c8eddad3df9bffda0c7c7b1c817104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00406cd8e95d060040420f000000000000000f0000000400000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020870726f6475636572000101000e000000000000000000000000000000000000000000000000000000000000000764656369646572000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000355b66c9006f967782196c567a5a7cda3555fc69f1b46d20868e26b8a63b13094fe6e20d7bb93fa5333fb9661b39640019dd1b8294f0897e08ff30ffba4a220a", + "grants": [ + "575247310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a00000000000001010000000000000000000000000000000000000000000000000000000000000000000057455031010e000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff0000000000000028ef1d708ccad44b6e6567347fc6cfda69f4f3020807d10bfa8e36e603ce586d0c469675acd4e40e894130997db6b93cb9cbab8e1ed16fbeba6b3426018d4809", + "575247310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000100000000000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffff0a0000000000000045555200000000cf2674edd0b2bd2fdf31d1a91f190c17e4745f70dd0627ac8cddda4dd3411512a512466c4d10d51f552bf47940f05b0d79a03acbe8e2173164d67b8dba5ca707" + ], + "key_log": null, + "chains": [ + [ + "574556310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c2117104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027bd894bb4700bdc343bcb21a86b2c8ce90614e8b886278e159b50e02f0d1762398b04f1d28aeba12dc8ddfacac5b94c12a4f1abf77997072efd2eeb62485cc50b7ef288ffb680e676fa8523317ba8d7e62d5c5e32843eaa2f0c50b8c9135e15e04" + ], + [ + "574556310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000804f50490000000000020a00040900000060ea0000007967f4b3b8bdb07fe925add41fae9cfc903fdb761f2db44e27413c8a1e786c30470c8b6c496e5f9b3e4cb81b059194fd1f672ec62d3b63c187b868e9375fd20e" + ] + ], + "policy": "574150310115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21e7b2ef80c780ac88a9eeb975bc452e7f929a870fafd2ab5904a57e982308540902307500000000000000ffff00210068747470733a2f2f6164732e6368616e6e656c2e6578616d706c652f736761692f260068747470733a2f2f63646e2e6368616e6e656c2e6578616d706c652f6372656174697665732fec70c2ddad6afc237ddd51d8d632250b72f134b2b2a67217d2e57aaf565c929fc764e75c384153459373f4bfaea92df5aa2ab01b9f0e34324c0c51d5e1ca8d0c" + }, + "main": { + "descriptor": "5752443101ef57dcb19a2edd125601041956c18e9442b1188c8eddad3df9bffda0c7c7b1c817104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00406cd8e95d060040420f00000000000000100000000400000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020870726f6475636572000101000e0000000000000000000000000000000000000000000000000000000000000007646563696465720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008e2ec6bfee0d3ac5f11a3ca45077e066da2f84d25727df1ecabb651b8281440af6824393ec384a96c4b2c515a5592b26725d6c320a7cc9c89681567e768c6105", + "grants": [ + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a00000000000001010000000000000000000000000000000000000000000000000000000000000000000057455031010e000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000000000b42c6a5b47cd4cb88940a50e2c458a07474950de2c0c7be9b46539ad5c7d0e62ef337994293bfb7a7eff6032e33c2e85ff691975d7ca85a20c62504342f8750e", + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000100000000000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffff0a000000000000004555520000000003a6092c6fa234b83194db16a2abcf8cad23bae40b17d3fae8d22547643a37147c79f08e7965b132d1c085db7d7fa25385993a75e8abe7295f8b6225e8d4b107" + ], + "key_log": null, + "chains": [ + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c717104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027bf6b581a6a9261e664af857758e3172c3a5a7b18b2e4220ece372840b5f0c06ea174c80ddde468fb5caa2c9d8bb8deea77cc0fa583e0a7aa7f8f5659492cadcd48a8c2c6ea9c7a2f0f08f908888b664593f56344f69f89a27cdf164d7d39d8c02" + ], + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b000000000000000000000000000000000000000000000000000000000000000000000000000000000046c3230000000000022f00010100000060ea0000012300fc3020000000000000fffff00f05000000017ffffe005265c000010000000090cb184629de507fed28c91a366fa3c4608305cab08c42b37f079e16f73af315d9e45f43b74badbed60125a6e1ca1036dd75c5b98058710d05ac1095de136c94c57e4d00", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b01000000000000003f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e00cd56270000000000020a00020100000060ea0000005e96dcc956cdfd60ecfff9fff68fa1440dc3da8befab62521ade41c16362777c469750536696580fc037fa7930670ef538b962c3f5295fceac6baa52964f0d0f", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b02000000000000008f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569008c86470000000000023d000402000000905f0100013100fc302e000000000000fffff00506fe066ff3000018021643554549000000027fff00007b98a000003401010103d486eb5029eb3afab357cfcc9a0c9fdb9c95818b291e5671ca9c3df2bd6394e2078885f58ea8899fc957682ba07e48c7a62b5821676899407e18f71a21ec5c4b47fedf04", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0300000000000000b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a525300d2496b0000000000023d000403000000905f0100013100fc302e000000000000fffff00506fe09a7ec800018021643554549000000037fff00007b98a0000036010101032e9cea7e4f4ec7ceac95aca1d9cd9930932f7a2e8608ece49c0035892597e4582d52e39fe3d2df79b5a7ca50fa57d05906c62225c28cd1f2c83fa73b6d4c7a3854cac406", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b04000000000000005ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec6606600180d8f0000000000023d000104000000c0d40100013100fc302e000000000000fffff00506fe0cdfe6000018021643554549000000047fff0000a4cb800000340101010464e2595dd300c68d72677a2381a8097b9d02d56438b6312ffb8dd528a993d1a75151dc641ea114503971f96fc54d78002e4ad1476be62196337297cd190cda75953a5201", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b050000000000000076819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94002634960000000000020a000204000000c0d401000037f639e9f15d2bf8bf540de28b446b1d661b3b019207721bb95991bd189325d6bdc5bc8687edec2ef1aa41d5fc2547cf46aa614d66b7e23be5fe5538ddaa440a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b060000000000000089adab1875539315934024864137ef3354839e876c204027a254eef806417f49005ed0b20000000000020a00040500000060ea00000000b431f191e5dabb21c7b2e7562a39988b7f799dd8670630f5d313d20ed01b3c6e502fca0ce718d1e8d4575170d3323010642f43e91888cd8ca30129f422a60a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b070000000000000055a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a00ea56fa00000000000242000406000000905f0100013600fc3033000000000000fffff00f05000000067ffffe007b98a0000100000013021143554549000000067fbf0000340101010377d8c3a09adb5d92881c6f75aed346bc959072fb3d3b301d6a586fb621dae87fd4d2d6a11d0449f577684328b1c5262b45aae867841f6ff0795e6fefff660691b1cc4901", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0800000000000000384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa04461300bca0650100000000023d000407000000c0d40100013100fc302e000000000000fffff00506fe202fbf000018021643554549000000077fff0000a4cb8000003401010104daa171b358fc78de86459172ef832cfb698b73bd9f68cff517a43e1f1445111290b123ffba5b64c29e4e06582b3769aa6b90764510a9cf8d85905aa8f86fc4a868137e0e", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0900000000000000c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f000264890100000000023b00040800000060ea0000012f00fc302c000000000000fffff00506fe2367b8800016021443554549000000087fff00005265c00000300101e54b0b44c01f66fd9da9a152c3da88d3e8f1f1a16ce02e347180e183d84e8d996ac04b22105cc54c85651a286413f61d1bffc597507e5745c4d5f6cbc198b94066b91c05" + ] + ], + "policy": "5741503101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f7802307500000000000000ffff00210068747470733a2f2f6164732e6368616e6e656c2e6578616d706c652f736761692f260068747470733a2f2f63646e2e6368616e6e656c2e6578616d706c652f6372656174697665732fe891f431a55fe4c12938d97e9d41b8566c5a1ff11c5cf6e220243a86fa13e027eb1f9f426306c865f3b4b350e0a0910f3645a587585a1940fbbfc6b2a9b93105" + }, + "main_crc_flipped": { + "descriptor": "5752443101ef57dcb19a2edd125601041956c18e9442b1188c8eddad3df9bffda0c7c7b1c817104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00406cd8e95d060040420f00000000000000100000000400000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020870726f6475636572000101000e0000000000000000000000000000000000000000000000000000000000000007646563696465720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008e2ec6bfee0d3ac5f11a3ca45077e066da2f84d25727df1ecabb651b8281440af6824393ec384a96c4b2c515a5592b26725d6c320a7cc9c89681567e768c6105", + "grants": [ + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a00000000000001010000000000000000000000000000000000000000000000000000000000000000000057455031010e000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000000000b42c6a5b47cd4cb88940a50e2c458a07474950de2c0c7be9b46539ad5c7d0e62ef337994293bfb7a7eff6032e33c2e85ff691975d7ca85a20c62504342f8750e", + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000100000000000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffff0a000000000000004555520000000003a6092c6fa234b83194db16a2abcf8cad23bae40b17d3fae8d22547643a37147c79f08e7965b132d1c085db7d7fa25385993a75e8abe7295f8b6225e8d4b107" + ], + "key_log": null, + "chains": [ + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c717104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027bf6b581a6a9261e664af857758e3172c3a5a7b18b2e4220ece372840b5f0c06ea174c80ddde468fb5caa2c9d8bb8deea77cc0fa583e0a7aa7f8f5659492cadcd48a8c2c6ea9c7a2f0f08f908888b664593f56344f69f89a27cdf164d7d39d8c02" + ], + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b000000000000000000000000000000000000000000000000000000000000000000000000000000000046c3230000000000022f00010100000060ea0000012300fc3020000000000000fffff00f05000000017ffffe005265c000010000000090cb184629de507fed28c91a366fa3c4608305cab08c42b37f079e16f73af315d9e45f43b74badbed60125a6e1ca1036dd75c5b98058710d05ac1095de136c94c57e4d00", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b01000000000000003f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e00cd56270000000000020a00020100000060ea0000005e96dcc956cdfd60ecfff9fff68fa1440dc3da8befab62521ade41c16362777c469750536696580fc037fa7930670ef538b962c3f5295fceac6baa52964f0d0f", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b02000000000000008f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569008c86470000000000023d000402000000905f0100013100fc302e000000000000fffff00506fe066ff3000018021643554549000000027fff00007b98a000003401010103d486eb51a1a1aa7c36e53deb6f0c7ae713bc43a73c42824b2d607f7b7e65e4720df87946e0703608059ebdca6eab43e876aa075d237388bdeac6a0bb3c40bc845301240c", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b030000000000000014f4653dbf78c434fda6b8d2091ca6f7916ae939e1fc8cbf839af882d3fb0f2500d2496b0000000000023d000403000000905f0100013100fc302e000000000000fffff00506fe09a7ec800018021643554549000000037fff00007b98a0000036010101032e9cea7e1a0f586258bcc72d05cc89fd2d9226e75d67e003c5f74453d1272352d5a6904f2188f1389051ff6ad50f5b54d4e7a55b56b173d5321ed9a5eff3e41a1630910a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0400000000000000a482bf5e2e3dccf5cc456c08748125a5adca80f9ac48eba82692ee1eb6c77bb100180d8f0000000000023d000104000000c0d40100013100fc302e000000000000fffff00506fe0cdfe6000018021643554549000000047fff0000a4cb800000340101010464e2595d3fa46b7d689442bfcdff6aa563e4d9b49ef195034df3d6b5a2d7686a417993f24cfa28f12c648d6c0b63f9e6a477c76a8b6ab4f495a8c503db862576b24c850c", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b05000000000000006a0940dab4808e25ea2bd9e16fcda3b12f43766997972c3fd2b85f5227b58372002634960000000000020a000204000000c0d4010000740cfe383ff008199a0a203a9526d6a1f391bc7791309c760f04d48cbe814ceb98ba9a168d87f60bd2aee0ee7676633aca5d6af2620f1bd476ab75599246710a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b06000000000000002dc6fb562b698175e8c3aa7923a122e71afc98f9665a2565c8e531396b83058a005ed0b20000000000020a00040500000060ea00000054ff32e9d7fd4cc5c6a036e199e49d53243538494d3349a408b682527cfed91fea48434350d659287d2982d5dc080c2eb1080dfabf3a31c8cc55d8cc62068c0c", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0700000000000000232bc44e9ae5f277eb16ece914c88c0b6f504ec1e59440d4992dcfe94e5bd49000ea56fa00000000000242000406000000905f0100013600fc3033000000000000fffff00f05000000067ffffe007b98a0000100000013021143554549000000067fbf0000340101010377d8c3a05a685d7eb5c54a96cd9de95806fdb950895a722affe486d732d878a9837f6857843576361919483530d609aa6026d9183a0873e3f44eb744da453059a81b1903", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0800000000000000e2c80f45453f5911e74600ca73ea0e75d08b391a18e9be5fb286e540bd47af9400bca0650100000000023d000407000000c0d40100013100fc302e000000000000fffff00506fe202fbf000018021643554549000000077fff0000a4cb8000003401010104daa171b3fe16762197d461a4cdc336a8e519216b260f379e1550b2eda6c4828b4fe0f3d76c765551214ec0b855a5a89acb340909de8f5ec28257a699c5893dc3df328004", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0900000000000000be503a87fcfab7ccdaa8a4d32717eb6ceb4c4828e1c030b60ff8ea9e9439289a000264890100000000023b00040800000060ea0000012f00fc302c000000000000fffff00506fe2367b8800016021443554549000000087fff00005265c00000300101e54b0b449fbaa3f858f988e4952dea1d0c2c6494be0bf09c06d9b9d9fdf6cdd8e9dccb5afe015e9687947a8eae8114f17b314eab499f707f0b6d96b6e2eb7bacfcfaff06" + ] + ], + "policy": "5741503101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f7802307500000000000000ffff00210068747470733a2f2f6164732e6368616e6e656c2e6578616d706c652f736761692f260068747470733a2f2f63646e2e6368616e6e656c2e6578616d706c652f6372656174697665732fe891f431a55fe4c12938d97e9d41b8566c5a1ff11c5cf6e220243a86fa13e027eb1f9f426306c865f3b4b350e0a0910f3645a587585a1940fbbfc6b2a9b93105" + }, + "main_other_segmentation_id": { + "descriptor": "5752443101ef57dcb19a2edd125601041956c18e9442b1188c8eddad3df9bffda0c7c7b1c817104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00406cd8e95d060040420f00000000000000100000000400000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020870726f6475636572000101000e0000000000000000000000000000000000000000000000000000000000000007646563696465720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008e2ec6bfee0d3ac5f11a3ca45077e066da2f84d25727df1ecabb651b8281440af6824393ec384a96c4b2c515a5592b26725d6c320a7cc9c89681567e768c6105", + "grants": [ + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a00000000000001010000000000000000000000000000000000000000000000000000000000000000000057455031010e000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000000000b42c6a5b47cd4cb88940a50e2c458a07474950de2c0c7be9b46539ad5c7d0e62ef337994293bfb7a7eff6032e33c2e85ff691975d7ca85a20c62504342f8750e", + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000100000000000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffff0a000000000000004555520000000003a6092c6fa234b83194db16a2abcf8cad23bae40b17d3fae8d22547643a37147c79f08e7965b132d1c085db7d7fa25385993a75e8abe7295f8b6225e8d4b107" + ], + "key_log": null, + "chains": [ + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c717104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027bf6b581a6a9261e664af857758e3172c3a5a7b18b2e4220ece372840b5f0c06ea174c80ddde468fb5caa2c9d8bb8deea77cc0fa583e0a7aa7f8f5659492cadcd48a8c2c6ea9c7a2f0f08f908888b664593f56344f69f89a27cdf164d7d39d8c02" + ], + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b000000000000000000000000000000000000000000000000000000000000000000000000000000000046c3230000000000022f00010100000060ea0000012300fc3020000000000000fffff00f05000000017ffffe005265c000010000000090cb184629de507fed28c91a366fa3c4608305cab08c42b37f079e16f73af315d9e45f43b74badbed60125a6e1ca1036dd75c5b98058710d05ac1095de136c94c57e4d00", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b01000000000000003f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e00cd56270000000000020a00020100000060ea0000005e96dcc956cdfd60ecfff9fff68fa1440dc3da8befab62521ade41c16362777c469750536696580fc037fa7930670ef538b962c3f5295fceac6baa52964f0d0f", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b02000000000000008f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569008c86470000000000023d000402000000905f0100013100fc302e000000000000fffff00506fe066ff3000018021643554549000000027fff00007b98a000003401010103d486eb5029eb3afab357cfcc9a0c9fdb9c95818b291e5671ca9c3df2bd6394e2078885f58ea8899fc957682ba07e48c7a62b5821676899407e18f71a21ec5c4b47fedf04", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0300000000000000b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a525300d2496b0000000000023d000403000000905f0100013100fc302e000000000000fffff00506fe09a7ec800018021643554549000000037fff00007b98a0000036010101032e9cea7e4f4ec7ceac95aca1d9cd9930932f7a2e8608ece49c0035892597e4582d52e39fe3d2df79b5a7ca50fa57d05906c62225c28cd1f2c83fa73b6d4c7a3854cac406", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b04000000000000005ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec6606600180d8f0000000000023d000104000000c0d40100013100fc302e000000000000fffff00506fe0cdfe6000018021643554549000000287fff0000a4cb800000340101010429c1823fc8d1e6a0639295547a856d2dc642761ce26e731b3266dbf2416bee102e66879283ee31795ba21acc855b38847263ff89f5059d847567c67186238a5eeda92108", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b050000000000000098a57e1ede1732841cdc9b3bf15fc8a03ebe61ad1ec102807063277a800a0820002634960000000000020a000204000000c0d4010000b3ae819dd0bcffc9fb9e6a1d3586f042a1591eae485481690873e1e17190ec998d1abaeeb18e7f481035d1f343b032ba3793eb2f86e48f0bb8eecd081c576405", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0600000000000000a59084ce0e05509b94950a7b85515bb54af999f55b618327bfd61a2a52804152005ed0b20000000000020a00040500000060ea0000005801fc507997a668a3600b9b0fec636562e9deb868d3ffe981bebea160869d4fbfc94847cc7353fce10b287c4dcdb93b83e12db82988d1544372875e8523e704", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0700000000000000d7163e7004e8b6ca263c704cbf0c792e13ae5f44d184b60f1da9edf91cbfebb900ea56fa00000000000242000406000000905f0100013600fc3033000000000000fffff00f05000000067ffffe007b98a0000100000013021143554549000000067fbf0000340101010377d8c3a08041a1df8533d6f68579ef8126053cb690fc87f50210cc98565b9f219c5fd4e0fde4b8ea1107a6b1019f49b6c7713255d4b7e5649ac9285bc82fbc1d1c48ce03", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b08000000000000000d7c0103bbf14bdd86514a498533c9f6a48aac49bea208c88690acaa8b0fe87200bca0650100000000023d000407000000c0d40100013100fc302e000000000000fffff00506fe202fbf000018021643554549000000077fff0000a4cb8000003401010104daa171b34e1dcabf443b2dd5e0bd5c33c0c15d9566ce302f0858bf77ceee8956dd89c6cb992e3ce5cc6cc09ec9853e31fa5b46d37c625fbbbda4603082176948263f230a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0900000000000000c8170a2bf22b2d8528db28cc8bc4b11ceb297fcd5d0a4ecdd8729c6bd868c56d000264890100000000023b00040800000060ea0000012f00fc302c000000000000fffff00506fe2367b8800016021443554549000000087fff00005265c00000300101e54b0b44d44b7c4573c6194e8f0a093991f6d8f1ebad226985153e1e295df78a15ff40b9f2172dcf6426445d5973fbba094d1c6d1bcaa15fdfa90bdf42d1986c1bc88604" + ] + ], + "policy": "5741503101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f7802307500000000000000ffff00210068747470733a2f2f6164732e6368616e6e656c2e6578616d706c652f736761692f260068747470733a2f2f63646e2e6368616e6e656c2e6578616d706c652f6372656174697665732fe891f431a55fe4c12938d97e9d41b8566c5a1ff11c5cf6e220243a86fa13e027eb1f9f426306c865f3b4b350e0a0910f3645a587585a1940fbbfc6b2a9b93105" + }, + "main_signature_flipped": { + "descriptor": "5752443101ef57dcb19a2edd125601041956c18e9442b1188c8eddad3df9bffda0c7c7b1c817104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00406cd8e95d060040420f00000000000000100000000400000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020870726f6475636572000101000e0000000000000000000000000000000000000000000000000000000000000007646563696465720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008e2ec6bfee0d3ac5f11a3ca45077e066da2f84d25727df1ecabb651b8281440af6824393ec384a96c4b2c515a5592b26725d6c320a7cc9c89681567e768c6105", + "grants": [ + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a00000000000001010000000000000000000000000000000000000000000000000000000000000000000057455031010e000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000000000b42c6a5b47cd4cb88940a50e2c458a07474950de2c0c7be9b46539ad5c7d0e62ef337994293bfb7a7eff6032e33c2e85ff691975d7ca85a20c62504342f8750e", + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000100000000000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffff0a000000000000004555520000000003a6092c6fa234b83194db16a2abcf8cad23bae40b17d3fae8d22547643a37147c79f08e7965b132d1c085db7d7fa25385993a75e8abe7295f8b6225e8d4b107" + ], + "key_log": null, + "chains": [ + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c717104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027bf6b581a6a9261e664af857758e3172c3a5a7b18b2e4220ece372840b5f0c06ea174c80ddde468fb5caa2c9d8bb8deea77cc0fa583e0a7aa7f8f5659492cadcd48a8c2c6ea9c7a2f0f08f908888b664593f56344f69f89a27cdf164d7d39d8c02" + ], + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b000000000000000000000000000000000000000000000000000000000000000000000000000000000046c3230000000000022f00010100000060ea0000012300fc3020000000000000fffff00f05000000017ffffe005265c000010000000090cb184629de507fed28c91a366fa3c4608305cab08c42b37f079e16f73af315d9e45f43b74badbed60125a6e1ca1036dd75c5b98058710d05ac1095de136c94c57e4d00", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b01000000000000003f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e00cd56270000000000020a00020100000060ea0000005e96dcc956cdfd60ecfff9fff68fa1440dc3da8befab62521ade41c16362777c469750536696580fc037fa7930670ef538b962c3f5295fceac6baa52964f0d0f", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b02000000000000008f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569008c86470000000000023d000402000000905f0100013100fc302e000000000000fffff00506fe066ff3000018021643554549000000027fff00007b98a000003401010103d486eb5029eb3afab357cfcc9a0c9fdb9c95818b291e5671ca9c3df2bd6394e2078885f58ea8899fc957682ba07e48c7a62b5821676899407e18f71a21ec5c4b47fedf05", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0300000000000000b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a525300d2496b0000000000023d000403000000905f0100013100fc302e000000000000fffff00506fe09a7ec800018021643554549000000037fff00007b98a0000036010101032e9cea7e4f4ec7ceac95aca1d9cd9930932f7a2e8608ece49c0035892597e4582d52e39fe3d2df79b5a7ca50fa57d05906c62225c28cd1f2c83fa73b6d4c7a3854cac406", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b04000000000000005ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec6606600180d8f0000000000023d000104000000c0d40100013100fc302e000000000000fffff00506fe0cdfe6000018021643554549000000047fff0000a4cb800000340101010464e2595dd300c68d72677a2381a8097b9d02d56438b6312ffb8dd528a993d1a75151dc641ea114503971f96fc54d78002e4ad1476be62196337297cd190cda75953a5201", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b050000000000000076819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94002634960000000000020a000204000000c0d401000037f639e9f15d2bf8bf540de28b446b1d661b3b019207721bb95991bd189325d6bdc5bc8687edec2ef1aa41d5fc2547cf46aa614d66b7e23be5fe5538ddaa440a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b060000000000000089adab1875539315934024864137ef3354839e876c204027a254eef806417f49005ed0b20000000000020a00040500000060ea00000000b431f191e5dabb21c7b2e7562a39988b7f799dd8670630f5d313d20ed01b3c6e502fca0ce718d1e8d4575170d3323010642f43e91888cd8ca30129f422a60a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b070000000000000055a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a00ea56fa00000000000242000406000000905f0100013600fc3033000000000000fffff00f05000000067ffffe007b98a0000100000013021143554549000000067fbf0000340101010377d8c3a09adb5d92881c6f75aed346bc959072fb3d3b301d6a586fb621dae87fd4d2d6a11d0449f577684328b1c5262b45aae867841f6ff0795e6fefff660691b1cc4901", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0800000000000000384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa04461300bca0650100000000023d000407000000c0d40100013100fc302e000000000000fffff00506fe202fbf000018021643554549000000077fff0000a4cb8000003401010104daa171b358fc78de86459172ef832cfb698b73bd9f68cff517a43e1f1445111290b123ffba5b64c29e4e06582b3769aa6b90764510a9cf8d85905aa8f86fc4a868137e0e", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0900000000000000c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f000264890100000000023b00040800000060ea0000012f00fc302c000000000000fffff00506fe2367b8800016021443554549000000087fff00005265c00000300101e54b0b44c01f66fd9da9a152c3da88d3e8f1f1a16ce02e347180e183d84e8d996ac04b22105cc54c85651a286413f61d1bffc597507e5745c4d5f6cbc198b94066b91c05" + ] + ], + "policy": "5741503101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f7802307500000000000000ffff00210068747470733a2f2f6164732e6368616e6e656c2e6578616d706c652f736761692f260068747470733a2f2f63646e2e6368616e6e656c2e6578616d706c652f6372656174697665732fe891f431a55fe4c12938d97e9d41b8566c5a1ff11c5cf6e220243a86fa13e027eb1f9f426306c865f3b4b350e0a0910f3645a587585a1940fbbfc6b2a9b93105" + }, + "main_with_a_gap": { + "descriptor": "5752443101ef57dcb19a2edd125601041956c18e9442b1188c8eddad3df9bffda0c7c7b1c817104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00406cd8e95d060040420f00000000000000100000000400000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020870726f6475636572000101000e0000000000000000000000000000000000000000000000000000000000000007646563696465720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008e2ec6bfee0d3ac5f11a3ca45077e066da2f84d25727df1ecabb651b8281440af6824393ec384a96c4b2c515a5592b26725d6c320a7cc9c89681567e768c6105", + "grants": [ + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a00000000000001010000000000000000000000000000000000000000000000000000000000000000000057455031010e000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000000000b42c6a5b47cd4cb88940a50e2c458a07474950de2c0c7be9b46539ad5c7d0e62ef337994293bfb7a7eff6032e33c2e85ff691975d7ca85a20c62504342f8750e", + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000100000000000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffff0a000000000000004555520000000003a6092c6fa234b83194db16a2abcf8cad23bae40b17d3fae8d22547643a37147c79f08e7965b132d1c085db7d7fa25385993a75e8abe7295f8b6225e8d4b107" + ], + "key_log": null, + "chains": [ + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c717104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027bf6b581a6a9261e664af857758e3172c3a5a7b18b2e4220ece372840b5f0c06ea174c80ddde468fb5caa2c9d8bb8deea77cc0fa583e0a7aa7f8f5659492cadcd48a8c2c6ea9c7a2f0f08f908888b664593f56344f69f89a27cdf164d7d39d8c02" + ], + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b000000000000000000000000000000000000000000000000000000000000000000000000000000000046c3230000000000022f00010100000060ea0000012300fc3020000000000000fffff00f05000000017ffffe005265c000010000000090cb184629de507fed28c91a366fa3c4608305cab08c42b37f079e16f73af315d9e45f43b74badbed60125a6e1ca1036dd75c5b98058710d05ac1095de136c94c57e4d00", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b01000000000000003f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e00cd56270000000000020a00020100000060ea0000005e96dcc956cdfd60ecfff9fff68fa1440dc3da8befab62521ade41c16362777c469750536696580fc037fa7930670ef538b962c3f5295fceac6baa52964f0d0f", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b02000000000000008f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569008c86470000000000023d000402000000905f0100013100fc302e000000000000fffff00506fe066ff3000018021643554549000000027fff00007b98a000003401010103d486eb5029eb3afab357cfcc9a0c9fdb9c95818b291e5671ca9c3df2bd6394e2078885f58ea8899fc957682ba07e48c7a62b5821676899407e18f71a21ec5c4b47fedf04", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0300000000000000b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a525300d2496b0000000000023d000403000000905f0100013100fc302e000000000000fffff00506fe09a7ec800018021643554549000000037fff00007b98a0000036010101032e9cea7e4f4ec7ceac95aca1d9cd9930932f7a2e8608ece49c0035892597e4582d52e39fe3d2df79b5a7ca50fa57d05906c62225c28cd1f2c83fa73b6d4c7a3854cac406", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b050000000000000076819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94002634960000000000020a000204000000c0d401000037f639e9f15d2bf8bf540de28b446b1d661b3b019207721bb95991bd189325d6bdc5bc8687edec2ef1aa41d5fc2547cf46aa614d66b7e23be5fe5538ddaa440a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b060000000000000089adab1875539315934024864137ef3354839e876c204027a254eef806417f49005ed0b20000000000020a00040500000060ea00000000b431f191e5dabb21c7b2e7562a39988b7f799dd8670630f5d313d20ed01b3c6e502fca0ce718d1e8d4575170d3323010642f43e91888cd8ca30129f422a60a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b070000000000000055a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a00ea56fa00000000000242000406000000905f0100013600fc3033000000000000fffff00f05000000067ffffe007b98a0000100000013021143554549000000067fbf0000340101010377d8c3a09adb5d92881c6f75aed346bc959072fb3d3b301d6a586fb621dae87fd4d2d6a11d0449f577684328b1c5262b45aae867841f6ff0795e6fefff660691b1cc4901", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0800000000000000384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa04461300bca0650100000000023d000407000000c0d40100013100fc302e000000000000fffff00506fe202fbf000018021643554549000000077fff0000a4cb8000003401010104daa171b358fc78de86459172ef832cfb698b73bd9f68cff517a43e1f1445111290b123ffba5b64c29e4e06582b3769aa6b90764510a9cf8d85905aa8f86fc4a868137e0e", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0900000000000000c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f000264890100000000023b00040800000060ea0000012f00fc302c000000000000fffff00506fe2367b8800016021443554549000000087fff00005265c00000300101e54b0b44c01f66fd9da9a152c3da88d3e8f1f1a16ce02e347180e183d84e8d996ac04b22105cc54c85651a286413f61d1bffc597507e5745c4d5f6cbc198b94066b91c05" + ] + ], + "policy": "5741503101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f7802307500000000000000ffff00210068747470733a2f2f6164732e6368616e6e656c2e6578616d706c652f736761692f260068747470733a2f2f63646e2e6368616e6e656c2e6578616d706c652f6372656174697665732fe891f431a55fe4c12938d97e9d41b8566c5a1ff11c5cf6e220243a86fa13e027eb1f9f426306c865f3b4b350e0a0910f3645a587585a1940fbbfc6b2a9b93105" + } + }, + "ledgers": { + "day_16": { + "tariff": "574c543101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fdf6c122609d64eb7e56b606d225680ba7c37b0208d8a0d25c69d22950ef92f45800a0cd18bc5c06000040e2b52b5f060045555200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060e316000000000040420f0000000000010004269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad6df9edfc3f0a93562f8f673d4477995cc5d1514f74d23b9e93ad580cbd4e9330bc32e680fde53b7538d1e975ec5fad259d3ac9a8dd849aab5055d1bc20b0b40f0ac73cc0ce668ef8abb70af1ba9b8634c7804618fafb8986e542ee216a5dfe9eb302f09926bb5287b24dab8b0e45a90582ac669d9c89f2a1ca19afb7cdd38d0c", + "invoice": "574c493101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fdf6c122609d64eb7e56b606d225680ba7c37b0208d8a0d25c69d22950ef92f4584803387f7c351bc151b3204dd698bd15c1f7c43bcc0b93e789e8e59e1332d70c000000000000000000000000000000000000000000000000000000000000000000406cd8e95d060000a043f6fd5d0600455552180000000604134d31b9d29f624d365e358e2097d6efdb2f930f6a93e4a98915fc35112d700616ddc4e6c68d3da1a43503ca852536d8ace2402f3684c4c62c24a7144a78c6dd06198ac7515fd7b2debf4d77d0dd2403bd020c6b8f51267393b92030e67eda91a7061c75bc13d3acca87e05da00abd1e13ad18045feaf40fd2f0d2781e1a94425202064f1c1bfe51cdfcea94fdcfb2f0eb76f4bb1d5f769069ca626abde7b8112d918e06595357c8c069806307a8bb3bd0dfc2701e1733674bb8a7fcb238f989c2da5ccd06613cba8d2d64dd65e11e1fa006afac14614ee984a03c2a2ab92054b1d58276b50667ef5554f0ac52bb1002450d9a83d29af9c1490bd821977826483fec19f2140c0669e90aa501f2660779dd98b7adc4d6445937f862a4db1d1da4c083581f984d0b068bbb3e8c0000bf26f968edd9c65316adc99e002b1c6e8373303ccca9b93c62cc069634524723a7f971c37d2b44b432d26b7686b74e17ac7a3f1430e01136169166069d252f0de5ac607084fde88ac26fe314fe73b606f9f0983fc028fb82552742e906a3eaa5b161cdc3e556d80838b2337e8e7f6a97cfc6a1fe5567c0e11d7220185606abc6d99d5efb8fe1e5fff1309ed722f3781342239afa0478495f328382c2bca706b7c8d8f262a3bc670cabe29a1d0a8dce4b17eeb360dc1545744fda1757334c4206baedce80a11569c1745d3876e804bcea56423f5549489ad56231f5d2fe1e1dfb06c6a7f3fae43fa38e5a8feca87c9d274759bd6e190d1eb83fee01290b6690163506c7f6686db66dc6a0193b54986ff81342264518731d7cfdcb25e882bd3de1d6bf06ce7c5d22351e40f60cb1696bb3eeeb5c524151d730dce0ba35ca3439ac291dc306d6f7e10d5d44c9b85376c1836d09693d65c704e9b1608c4409d8e9b35789423c06d92688885762077975dda5710e679a6ea736dba52d26f2f3422d0f8d6e9c0dc006ee84eed5c3a79ed321c8eba4c65522c8ed25c0f0febdd9142e65ccd82181332a06f3887c5bc6040bf6736bca96a9c6ac13c1a7e5cd3906fa7bc4388f11c2d803e006f9806e584e1975e8aec4ffcdb02acfc0ca990a13c2e776443f9c931b812ac080010004269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad02300000000000000040420f0000000000006cdc0200000000180000002ab893040d40b862945bbcef080d732e793176bff9816927c863b74a8ebad3870000000000000000000000000018000000006cdc0200000000ecd6e119b4c9f327da47c1cebef9bbed27a7d07721a5729443d6b0306bdd085af6eed1ebd9cdfc008cd7def2419251d3c7c9be109db9fc111959bfa5e29e380b", + "previous": null, + "sources": [ + { + "kind": "ads", + "descriptor": "5752443101ef57dcb19a2edd125601041956c18e9442b1188c8eddad3df9bffda0c7c7b1c817104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00406cd8e95d060040420f00000000000000100000000400000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000020870726f6475636572000101000e0000000000000000000000000000000000000000000000000000000000000007646563696465720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008e2ec6bfee0d3ac5f11a3ca45077e066da2f84d25727df1ecabb651b8281440af6824393ec384a96c4b2c515a5592b26725d6c320a7cc9c89681567e768c6105", + "grants": [ + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a00000000000001010000000000000000000000000000000000000000000000000000000000000000000057455031010e000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffffffffffffffffffff00000000000000b42c6a5b47cd4cb88940a50e2c458a07474950de2c0c7be9b46539ad5c7d0e62ef337994293bfb7a7eff6032e33c2e85ff691975d7ca85a20c62504342f8750e", + "5752473101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad00000000000000000000000000000000000000000000000000000000000000000000000000000000f05ad16a0000000080bad26a000000000100000000000000000000000000000000000000000000000000000000000000000000000000574550310100000000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f0000000000ffffffffffffffff0a000000000000004555520000000003a6092c6fa234b83194db16a2abcf8cad23bae40b17d3fae8d22547643a37147c79f08e7965b132d1c085db7d7fa25385993a75e8abe7295f8b6225e8d4b107" + ], + "key_log": null, + "chains": [ + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c717104461217b1ca365f7f120fb8ad6c0f47a96d0b152c3452d91fd7cc939ae3c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104000a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027bf6b581a6a9261e664af857758e3172c3a5a7b18b2e4220ece372840b5f0c06ea174c80ddde468fb5caa2c9d8bb8deea77cc0fa583e0a7aa7f8f5659492cadcd48a8c2c6ea9c7a2f0f08f908888b664593f56344f69f89a27cdf164d7d39d8c02" + ], + [ + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b000000000000000000000000000000000000000000000000000000000000000000000000000000000046c3230000000000022f00010100000060ea0000012300fc3020000000000000fffff00f05000000017ffffe005265c000010000000090cb184629de507fed28c91a366fa3c4608305cab08c42b37f079e16f73af315d9e45f43b74badbed60125a6e1ca1036dd75c5b98058710d05ac1095de136c94c57e4d00", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b01000000000000003f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e00cd56270000000000020a00020100000060ea0000005e96dcc956cdfd60ecfff9fff68fa1440dc3da8befab62521ade41c16362777c469750536696580fc037fa7930670ef538b962c3f5295fceac6baa52964f0d0f", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b02000000000000008f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569008c86470000000000023d000402000000905f0100013100fc302e000000000000fffff00506fe066ff3000018021643554549000000027fff00007b98a000003401010103d486eb5029eb3afab357cfcc9a0c9fdb9c95818b291e5671ca9c3df2bd6394e2078885f58ea8899fc957682ba07e48c7a62b5821676899407e18f71a21ec5c4b47fedf04", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0300000000000000b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a525300d2496b0000000000023d000403000000905f0100013100fc302e000000000000fffff00506fe09a7ec800018021643554549000000037fff00007b98a0000036010101032e9cea7e4f4ec7ceac95aca1d9cd9930932f7a2e8608ece49c0035892597e4582d52e39fe3d2df79b5a7ca50fa57d05906c62225c28cd1f2c83fa73b6d4c7a3854cac406", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b04000000000000005ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec6606600180d8f0000000000023d000104000000c0d40100013100fc302e000000000000fffff00506fe0cdfe6000018021643554549000000047fff0000a4cb800000340101010464e2595dd300c68d72677a2381a8097b9d02d56438b6312ffb8dd528a993d1a75151dc641ea114503971f96fc54d78002e4ad1476be62196337297cd190cda75953a5201", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b050000000000000076819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94002634960000000000020a000204000000c0d401000037f639e9f15d2bf8bf540de28b446b1d661b3b019207721bb95991bd189325d6bdc5bc8687edec2ef1aa41d5fc2547cf46aa614d66b7e23be5fe5538ddaa440a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b060000000000000089adab1875539315934024864137ef3354839e876c204027a254eef806417f49005ed0b20000000000020a00040500000060ea00000000b431f191e5dabb21c7b2e7562a39988b7f799dd8670630f5d313d20ed01b3c6e502fca0ce718d1e8d4575170d3323010642f43e91888cd8ca30129f422a60a", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b070000000000000055a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a00ea56fa00000000000242000406000000905f0100013600fc3033000000000000fffff00f05000000067ffffe007b98a0000100000013021143554549000000067fbf0000340101010377d8c3a09adb5d92881c6f75aed346bc959072fb3d3b301d6a586fb621dae87fd4d2d6a11d0449f577684328b1c5262b45aae867841f6ff0795e6fefff660691b1cc4901", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0800000000000000384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa04461300bca0650100000000023d000407000000c0d40100013100fc302e000000000000fffff00506fe202fbf000018021643554549000000077fff0000a4cb8000003401010104daa171b358fc78de86459172ef832cfb698b73bd9f68cff517a43e1f1445111290b123ffba5b64c29e4e06582b3769aa6b90764510a9cf8d85905aa8f86fc4a868137e0e", + "5745563101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7a6653ade784fbd8bee3dc43ec744f14df88f291d0348b6968f281080787d027b0900000000000000c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f000264890100000000023b00040800000060ea0000012f00fc302c000000000000fffff00506fe2367b8800016021443554549000000087fff00005265c00000300101e54b0b44c01f66fd9da9a152c3da88d3e8f1f1a16ce02e347180e183d84e8d996ac04b22105cc54c85651a286413f61d1bffc597507e5745c4d5f6cbc198b94066b91c05" + ] + ], + "policy": "5741503101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f7802307500000000000000ffff00210068747470733a2f2f6164732e6368616e6e656c2e6578616d706c652f736761692f260068747470733a2f2f63646e2e6368616e6e656c2e6578616d706c652f6372656174697665732fe891f431a55fe4c12938d97e9d41b8566c5a1ff11c5cf6e220243a86fa13e027eb1f9f426306c865f3b4b350e0a0910f3645a587585a1940fbbfc6b2a9b93105", + "pt0_ms": 0, + "receipts": [ + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e", + "session": "8ce32dcf853aed54c99eb0f01aff38e6", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "ccc956f8c11f69cfcaa8df1c20c8bf1b2adefeadf0fe015b36bc78f186c595b4d3fb47bc2e0384273a9fcad8887f396b35a2d17d9ed55a3644af8beabe2bbc02" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e", + "session": "56f26e848bed24e8fc8959bf426f1fcd", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "0ae4ec5162a10b692d334510bc1e4b91ad82fd1248477147bbaf1e64b6ac67c81d1af4dea16d1f6195a4a4e4e15db31006a9c88ae77df67e9a187afed360fe05" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e", + "session": "aee4198ec9dcefe2e8a2cda6a42be9b7", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "3b5d502e8eb365b63b980cf7da87b7554537f84c2736667ed0c87b35314e087859de0bee7ba3796b6b260d575c9103fa5d51e7e806b52e0730a4e565df88be0c" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253", + "session": "8ce32dcf853aed54c99eb0f01aff38e6", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "d4486a5e2d8144d1d28a4ef927f6a6cef6bd38fbf18753dc64dab0cd63c3d006690cdc5f6873a7421326b112f1c3b1eb70ad634e0b323d02020c38a31a901902" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253", + "session": "56f26e848bed24e8fc8959bf426f1fcd", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "e7c91200192d9977a8ef7db4799dd569f2ac0518199be5609b91d0fe63bdd1dda939e68d2b5f65dc95a18d37ba228d2626b8040f84a82e3ec05ae43d05e20408" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253", + "session": "aee4198ec9dcefe2e8a2cda6a42be9b7", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "bc3ac50b09f732d8a480919411dde59f346963e94bed7bbaee946e9530826798fef6821348ec080a08a8273866d4ad51c59ff0906d662b6a867fa0a2e3ed450f" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066", + "session": "8ce32dcf853aed54c99eb0f01aff38e6", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "29b263e3a5dd87dbe7f45ccead410d53ab3aeb9a9641b48137e1074e2b499addfbafc67a34744409441b4ebd19343ebed962c5d17a5e2d40b03ce9403ac7be0a" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066", + "session": "56f26e848bed24e8fc8959bf426f1fcd", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "75191ebd544760f4fcb4d0b0715d4db34d673f9b44eb5579f80161c0c1e2a74e25d7b5e5801d73374aa630982f5ab404e5e1088ab06aa3b4c3e950aca67cbd02" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066", + "session": "aee4198ec9dcefe2e8a2cda6a42be9b7", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "67567451b8257dfa6de086b3ce1e9936a372cdf2eb1d0d7fc64b29936d388b9403833af8cee0090c9ee736884b36262cdd5a147005c19a4442fbb7b0ebf8ab0d" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94", + "session": "8ce32dcf853aed54c99eb0f01aff38e6", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 120000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "2bc23fe0b9ba1192680370d0ebb3b7acaf0a3fd6109b31e7a24fae9989b93ac33634265757dda6c507b52250f22b90d676faca42dc66068bc9dfce325e113209" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94", + "session": "56f26e848bed24e8fc8959bf426f1fcd", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 120000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "0baf3406c1a1e088a8543ed5f8ed845cc445ed0b5004efc88a2fa8b265158926e551f32ec4cdfc2e6a2edbf6cd9befb780a5296383d903b9027ec3b8fb50b40c" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94", + "session": "aee4198ec9dcefe2e8a2cda6a42be9b7", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 120000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "29631bbd93c7c5be762953bba069cf954ccf18fcfc8fac52d16cabf82811ae38db07d331afbeb155d2122345ff61fd52e12b1470b6a25af224854bdc1f60100b" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a", + "session": "8ce32dcf853aed54c99eb0f01aff38e6", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "b012856e230caafb4564024c0f9502879195b39974cab62840cdc5de459d50157b3bb6e5aaeac533fc2c07b11440c8e5b232fde0bf396e94dc8b9c3b47c2b50a" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a", + "session": "56f26e848bed24e8fc8959bf426f1fcd", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "40e832440033c2e08f721e42f811dde30f9fa957093cd468a9a74c30d813a983ed28824b7ce4f9bc82bc7efa62980a77fd8fb41015402f6501f83dacd1955a08" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a", + "session": "aee4198ec9dcefe2e8a2cda6a42be9b7", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "5879507a374910e68a563c6380f52027c619ce8d59ad565a844c0524285daf9e5e47e363dc4473e59ba07b4df5f62afd0195380c4cf320abef4243c12a72d502" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613", + "session": "8ce32dcf853aed54c99eb0f01aff38e6", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "02834c1453bfe0b6c4da1790421dae36e3169ee1733f418e60e4fdf0cb520203b8b8e534ac7f9f9e5c111ad626676c642a15af14dfed375a82e11f927f6ed500" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613", + "session": "56f26e848bed24e8fc8959bf426f1fcd", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "decd0a6aa929c2747eea75c577b24514994651f3fbec6fc60e4b315683a53e997bd9ca5571878d79fe7bbdcf0ed3cbeb511074988c755dd3a2c51b138eb01c0a" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613", + "session": "aee4198ec9dcefe2e8a2cda6a42be9b7", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 90000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "03530b5b239cd717873fb79c5a6baf5991363aaf2d8757e3207bb4d6ec6b218987c6b88b51feb10744a1d589f15892f8e2a99c86c163deccb67118eaa4d4c805" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f", + "session": "8ce32dcf853aed54c99eb0f01aff38e6", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 120000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "62e173717f4962ed29197741d755e7155af68e1344040ddebb7300da0d2da19d8da79b6666b8d26ced7c2f576908a27fae88749cb483837b6951daef8fac0202" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f", + "session": "56f26e848bed24e8fc8959bf426f1fcd", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 120000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "4a5a0948d844a4f39e30cefa3f25310beae3a136a25f3a240d1fd7fa2834b89299d1567a4b4a90bf265b9a4e4d3b3eb8e460191b29ace4c633928e801a77e706" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f", + "session": "aee4198ec9dcefe2e8a2cda6a42be9b7", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 120000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "ca56bafb865e458d16ee65bf0734e2c2e99c1347cbd8c28d20e9d5fd75acdba13620878ecdbd9e0b245fe34f609d8fa32d965d905bb17663b8c9e8ea913a3e06" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99", + "session": "8ce32dcf853aed54c99eb0f01aff38e6", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "75c194af75426417c18685977eb75ebfd054291afb7fde8ae28203e581f8a77bcceb9929023dd9e523b76400ad32fbd53adfd58ea0489deb11baf1cecb88ce00" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99", + "session": "56f26e848bed24e8fc8959bf426f1fcd", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "e63dfd535fcb239027c31af3314be2e1b169a88a0fbbcefead933e4ce92edddf857b96160266c5f2bc844950ad9a68fb336744ec42c00c759a91843d15738507" + }, + { + "form": 2, + "room": "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7", + "cue": "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99", + "session": "aee4198ec9dcefe2e8a2cda6a42be9b7", + "start_offset_ms": 0, + "format": 1, + "asset_list_sha256": "92e1fb19c7595753b16cca19ee4dbf5f455a9b3179fc2c7b7cfbe1d43e64bed9", + "avail_ms": 60000, + "slots": 2, + "funds_remaining_micros": 10000000, + "pod_hash": "4056ca94189993bcb9e1c0445fe08550eb65f41f296f91cdc1eef5dc5d84d723", + "impressions": [ + { + "slot": 0, + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000, + "quartiles_reached": 4 + }, + { + "slot": 1, + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000, + "quartiles_reached": 4 + } + ], + "merkle_root": "b33e38e05d925f1129ab6dfd223d225e42921b9f0202f77e082b813993c88bf0", + "budget_spent_micros": 70000, + "work_decide_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "02af8d6f64213389b0754c147974f81414ab4415238afb5238761a77bff48f78", + "capability": "wai.live.room", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": 10 + }, + "signer_pubkey": "269c84800120bea84113782ff676db00ced0fc028987b9e467d7b5883223d2ad", + "signer_id": "did:key:z6Mkh3yxoZdvvhAtPJ3mh7GY3J3BgTVE6NTGBSzYxDcA5yUY", + "sig": "7bbdf6752f64ad868f9c14540dbe303ffcbb65513812edb488881596f77a8e5c3759afee4054aca4a6357549a818d62262022776c1b448789cb2d30ae2f4c506" + } + ], + "pods": [ + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ], + [ + { + "ad_id": "ad-a", + "creative_hash": "2daffa600d38a7abeff1b8a4065c86e10304917c7119181089452ac1c5ddc2de", + "duration_ms": 15000, + "price_micros": 40000 + }, + { + "ad_id": "ad-b", + "creative_hash": "7163368e28bb41c83868947bbb32c31ced401c8194c4000424f2db79dc65d227", + "duration_ms": 15000, + "price_micros": 30000 + }, + { + "ad_id": "ad-c", + "creative_hash": "ef83d2ea63442846e9048e3dbda12dee5d6f030741e1d43ff87762dfaa1aba6b", + "duration_ms": 20000, + "price_micros": 25000 + } + ] + ], + "claims": [], + "creatives": [], + "issuers": [] + } + ] + } + }, + "reported": { + "a_days": { + "form": "csv", + "text": "scope,key,impressions,revenue\r\nchannel,fast-9,,\r\nstart,1792108800000000,,\r\nend,1792195200000000,,\r\ncurrency,EUR,,\r\nbasis,share,,\r\nperiod,,24,7999992\r\nday,2026-10-16,24,7999992\r\n" + }, + "a_days_header_misspelt": { + "form": "csv", + "text": "scope,key,impression,revenue\r\nchannel,fast-9,,\r\nstart,1792108800000000,,\r\nend,1792195200000000,,\r\ncurrency,EUR,,\r\nbasis,share,,\r\nperiod,,24,7999992\r\nday,2026-10-16,24,7999992\r\n" + }, + "b_a_day_after_the_period": { + "form": "json", + "text": "{\"channel\":\"fast-9\",\"start\":1792108800000000,\"end\":1792195200000000,\"currency\":\"EUR\",\"basis\":\"share\",\"impressions\":14,\"revenue\":8400000,\"programmes\":[{\"asset\":\"b/doc-1\",\"impressions\":12,\"revenue\":7200000},{\"asset\":\"b/doc-2\",\"impressions\":2,\"revenue\":1200000}],\"days\":[{\"day\":\"2026-10-17\",\"impressions\":14,\"revenue\":8400000}]}" + }, + "b_programmes": { + "form": "json", + "text": "{\"channel\":\"fast-9\",\"start\":1792108800000000,\"end\":1792195200000000,\"currency\":\"EUR\",\"basis\":\"share\",\"impressions\":14,\"revenue\":8400000,\"programmes\":[{\"asset\":\"b/doc-1\",\"impressions\":12,\"revenue\":7200000},{\"asset\":\"b/doc-2\",\"impressions\":2,\"revenue\":1200000}]}" + }, + "b_programmes_and_a_programme": { + "form": "json", + "text": "{\"channel\":\"fast-9\",\"start\":1792108800000000,\"end\":1792195200000000,\"currency\":\"EUR\",\"basis\":\"share\",\"impressions\":19,\"revenue\":11400000,\"programmes\":[{\"asset\":\"a/ep-1\",\"impressions\":5,\"revenue\":3000000},{\"asset\":\"b/doc-1\",\"impressions\":12,\"revenue\":7200000},{\"asset\":\"b/doc-2\",\"impressions\":2,\"revenue\":1200000}]}" + }, + "b_programmes_of_another_channel": { + "form": "json", + "text": "{\"channel\":\"fast-8\",\"start\":1792108800000000,\"end\":1792195200000000,\"currency\":\"EUR\",\"basis\":\"share\",\"impressions\":14,\"revenue\":8400000,\"programmes\":[{\"asset\":\"b/doc-1\",\"impressions\":12,\"revenue\":7200000},{\"asset\":\"b/doc-2\",\"impressions\":2,\"revenue\":1200000}]}" + }, + "of_another_form": { + "form": "xml", + "text": "" + } + }, + "vectors": [ + { + "name": "owner_a_day", + "about": "owner A's day: four cued breaks in its episodes, one of them inside the ad slot after its first episode, and the ad slot no cue covers, scheduled and unslotted", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "accept", + "report": "2a0d762f7106ee7f42bc0aa5e0bb0d788da58fecbf797a7bec0bc272c2113092", + "contract": "35f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d", + "issuer": "50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a9", + "party": "50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a9", + "role": "owner", + "start": 1792108800000000, + "end": 1792195200000000, + "rooms": [ + "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7" + ], + "runs": [ + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + "breaks": [ + { + "class": "cued", + "room": 0, + "index": 0, + "taken": null, + "cue": "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e", + "start": 1792109400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "splice_insert", + "id": 1, + "duration": 5400000, + "slots": null + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e" + ], + [ + 12, + "8f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 1, + "taken": null, + "cue": "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253", + "start": 1792110000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 2, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 2, + "taken": null, + "cue": "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066", + "start": 1792110600000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 3, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 1, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 3, + "taken": null, + "cue": "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94", + "start": 1792111200000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 4, + "duration": 10800000, + "slots": 4 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 4, + "taken": null, + "cue": "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a", + "start": 1792111800000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": null, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 5, + "taken": null, + "cue": "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613", + "start": 1792113000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "splice_insert", + "id": 6, + "duration": 8100000, + "slots": 3 + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 0, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 6, + "taken": null, + "cue": "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f", + "start": 1792114800000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 7, + "duration": 10800000, + "slots": 4 + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 2, + "asset": "b/doc-2", + "split": 1, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 7, + "taken": null, + "cue": "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99", + "start": 1792115400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "time_signal", + "id": 8, + "duration": 5400000, + "slots": null + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 2, + "asset": "b/doc-2", + "split": 1, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "scheduled", + "room": null, + "index": null, + "taken": { + "run": 1, + "entry": 1 + }, + "cue": null, + "start": 1792114140000000, + "duration_ms": 60000, + "open": false, + "slots": null, + "slots_from": null, + "section": null, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 1, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "lines": [ + { + "split": 0, + "class": "cued", + "breaks": 4, + "break_ms": 330000, + "slots": 11, + "unslotted": 0, + "ppm": 333333, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613" + ], + [ + 12, + "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e" + ], + [ + 12, + "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066" + ], + [ + 12, + "8f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569" + ], + [ + 12, + "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "split": 0, + "class": "scheduled", + "breaks": 1, + "break_ms": 60000, + "slots": 0, + "unslotted": 1, + "ppm": 333333, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "totals": { + "breaks": 5, + "break_ms": 390000, + "slots": 11 + } + } + }, + { + "name": "owner_b_day", + "about": "owner B's day: four cued breaks in its documentaries, slots from placement opportunities and from the room's policy", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "accept", + "report": "96d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb76", + "contract": "35f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d", + "issuer": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "party": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "role": "owner", + "start": 1792108800000000, + "end": 1792195200000000, + "rooms": [ + "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7" + ], + "runs": [ + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + "breaks": [ + { + "class": "cued", + "room": 0, + "index": 0, + "taken": null, + "cue": "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e", + "start": 1792109400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "splice_insert", + "id": 1, + "duration": 5400000, + "slots": null + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e" + ], + [ + 12, + "8f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 1, + "taken": null, + "cue": "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253", + "start": 1792110000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 2, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 2, + "taken": null, + "cue": "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066", + "start": 1792110600000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 3, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 1, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 3, + "taken": null, + "cue": "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94", + "start": 1792111200000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 4, + "duration": 10800000, + "slots": 4 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 4, + "taken": null, + "cue": "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a", + "start": 1792111800000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": null, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 5, + "taken": null, + "cue": "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613", + "start": 1792113000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "splice_insert", + "id": 6, + "duration": 8100000, + "slots": 3 + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 0, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 6, + "taken": null, + "cue": "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f", + "start": 1792114800000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 7, + "duration": 10800000, + "slots": 4 + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 2, + "asset": "b/doc-2", + "split": 1, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 7, + "taken": null, + "cue": "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99", + "start": 1792115400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "time_signal", + "id": 8, + "duration": 5400000, + "slots": null + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 2, + "asset": "b/doc-2", + "split": 1, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "scheduled", + "room": null, + "index": null, + "taken": { + "run": 1, + "entry": 1 + }, + "cue": null, + "start": 1792114140000000, + "duration_ms": 60000, + "open": false, + "slots": null, + "slots_from": null, + "section": null, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 1, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "lines": [ + { + "split": 1, + "class": "cued", + "breaks": 4, + "break_ms": 360000, + "slots": 12, + "unslotted": 0, + "ppm": 600000, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ], + [ + 12, + "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f" + ], + [ + 12, + "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + } + ], + "totals": { + "breaks": 4, + "break_ms": 360000, + "slots": 12 + } + } + }, + { + "name": "operator_day", + "about": "the operator's day: its share of both owners' splits; no line of the between split, in which it holds no share", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f523101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd35f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2dc877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd00406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0300000001040000000000000010090500000000000b0000000000000000000000000000002b2c0a0008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea000000000000000000000000000001000000000000002b2c0a000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f9650100010400000000000000407e0500000000000c000000000000000000000000000000801a060008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270900000000000000b0710b00000000001700000000000000dced3434a1432c4a57af2946a0f076327a17af591efea15a90e8b200f654049100e7ae893c60b8ceda765e26d3cd8ca5cbf5fde8e7eceaf3a9f7af92a9dd500a", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "accept", + "report": "5337f17a580a1dce6b79292a1e8387f50e41c19ab5772ff51eeffab4926f2747", + "contract": "35f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d", + "issuer": "c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd", + "party": "c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd", + "role": "operator", + "start": 1792108800000000, + "end": 1792195200000000, + "rooms": [ + "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7" + ], + "runs": [ + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + "breaks": [ + { + "class": "cued", + "room": 0, + "index": 0, + "taken": null, + "cue": "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e", + "start": 1792109400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "splice_insert", + "id": 1, + "duration": 5400000, + "slots": null + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e" + ], + [ + 12, + "8f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 1, + "taken": null, + "cue": "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253", + "start": 1792110000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 2, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 2, + "taken": null, + "cue": "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066", + "start": 1792110600000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 3, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 1, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 3, + "taken": null, + "cue": "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94", + "start": 1792111200000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 4, + "duration": 10800000, + "slots": 4 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 4, + "taken": null, + "cue": "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a", + "start": 1792111800000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": null, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 5, + "taken": null, + "cue": "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613", + "start": 1792113000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "splice_insert", + "id": 6, + "duration": 8100000, + "slots": 3 + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 0, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 6, + "taken": null, + "cue": "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f", + "start": 1792114800000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 7, + "duration": 10800000, + "slots": 4 + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 2, + "asset": "b/doc-2", + "split": 1, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 7, + "taken": null, + "cue": "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99", + "start": 1792115400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "time_signal", + "id": 8, + "duration": 5400000, + "slots": null + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 2, + "asset": "b/doc-2", + "split": 1, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "scheduled", + "room": null, + "index": null, + "taken": { + "run": 1, + "entry": 1 + }, + "cue": null, + "start": 1792114140000000, + "duration_ms": 60000, + "open": false, + "slots": null, + "slots_from": null, + "section": null, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 1, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "lines": [ + { + "split": 0, + "class": "cued", + "breaks": 4, + "break_ms": 330000, + "slots": 11, + "unslotted": 0, + "ppm": 666667, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613" + ], + [ + 12, + "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e" + ], + [ + 12, + "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066" + ], + [ + 12, + "8f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569" + ], + [ + 12, + "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "split": 0, + "class": "scheduled", + "breaks": 1, + "break_ms": 60000, + "slots": 0, + "unslotted": 1, + "ppm": 666667, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + }, + { + "split": 1, + "class": "cued", + "breaks": 4, + "break_ms": 360000, + "slots": 12, + "unslotted": 0, + "ppm": 400000, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ], + [ + 12, + "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f" + ], + [ + 12, + "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + } + ], + "totals": { + "breaks": 9, + "break_ms": 750000, + "slots": 23 + } + } + }, + { + "name": "owner_a_runs_alone", + "about": "owner A's day from the runs alone: both ad slots scheduled, no cued break", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310181640fe9c7b63a1babed05713ed2f9958aa011b8d31edcf9c611f3751386873735f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d0600000002000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000000020200000000000000f049020000000000000000000000000002000000000000001516050002000000fe9cf49a9a1062ee661b1e7dc7e128776a5aad764a25073cb2023e5aca7e58d90200000000000000f04902000000000000000000000000009a15672f5f1e82f14c5a4d99e2e9e5832551724404f531d9f1fb61007115555d27a325f3f7db8a4d6ad8b4fb4c10b46dd1736642f6bc7c8b64ce2feb7276ce07", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [] + }, + "outcome": { + "outcome": "accept", + "report": "dbfcf2e08c4f9a03c769847fc9902a68ed74ad62719ebd1471d0e42e5487fee4", + "contract": "35f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d", + "issuer": "81640fe9c7b63a1babed05713ed2f9958aa011b8d31edcf9c611f37513868737", + "party": "50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a9", + "role": "owner", + "start": 1792108800000000, + "end": 1792195200000000, + "rooms": [], + "runs": [ + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + "breaks": [ + { + "class": "scheduled", + "room": null, + "index": null, + "taken": { + "run": 0, + "entry": 1 + }, + "cue": null, + "start": 1792110600000000, + "duration_ms": 90000, + "open": false, + "slots": null, + "slots_from": null, + "section": null, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 1, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ] + ] + }, + { + "class": "scheduled", + "room": null, + "index": null, + "taken": { + "run": 1, + "entry": 1 + }, + "cue": null, + "start": 1792114140000000, + "duration_ms": 60000, + "open": false, + "slots": null, + "slots_from": null, + "section": null, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 1, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "lines": [ + { + "split": 0, + "class": "scheduled", + "breaks": 2, + "break_ms": 150000, + "slots": 0, + "unslotted": 2, + "ppm": 333333, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "totals": { + "breaks": 2, + "break_ms": 150000, + "slots": 0 + } + } + }, + { + "name": "owner_b_two_rooms", + "about": "owner B's day under a contract naming two rooms: a break of the second room in its first documentary, overlapping none", + "kind": "report", + "report": { + "contract": "two_rooms", + "contracts": [], + "report": "574f52310181640fe9c7b63a1babed05713ed2f9958aa011b8d31edcf9c611f375138687376f97aba21b0674c6ef55e6c6468b7a107e638ca072e3da3535340bab6806f73b72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d0600020015bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010500000000000000a0680600000000000e000000000000000000000000000000c02709000a00000066d9fde31098615b73fffae3dc326088e78d3c40125e2f2a33fe3f8f6d7b82590500000000000000a0680600000000000e00000000000000ce02b4bf5c09a7b468dbb20363127ea724cce06cccd5f4cb2a406389001538451a8a5d6d3b7980074c54e7d26b93dd418af26017323c2e23ba4dcefcc0034306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main", + "backup" + ] + }, + "outcome": { + "outcome": "accept", + "report": "a2853d5f10abcb8b845fe04c1854cddc27d852f14a2bc8b31493dad9a2678521", + "contract": "6f97aba21b0674c6ef55e6c6468b7a107e638ca072e3da3535340bab6806f73b", + "issuer": "81640fe9c7b63a1babed05713ed2f9958aa011b8d31edcf9c611f37513868737", + "party": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "role": "owner", + "start": 1792108800000000, + "end": 1792195200000000, + "rooms": [ + "15bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21", + "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7" + ], + "runs": [ + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + "breaks": [ + { + "class": "cued", + "room": 0, + "index": 0, + "taken": null, + "cue": "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e", + "start": 1792109400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "splice_insert", + "id": 1, + "duration": 5400000, + "slots": null + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e" + ], + [ + 12, + "8f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 1, + "taken": null, + "cue": "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253", + "start": 1792110000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 2, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 2, + "taken": null, + "cue": "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066", + "start": 1792110600000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 3, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 1, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 3, + "taken": null, + "cue": "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94", + "start": 1792111200000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 4, + "duration": 10800000, + "slots": 4 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 4, + "taken": null, + "cue": "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a", + "start": 1792111800000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": null, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 5, + "taken": null, + "cue": "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613", + "start": 1792113000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "splice_insert", + "id": 6, + "duration": 8100000, + "slots": 3 + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 0, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "384dc894dcb863a6193f22a31947f10c4c25ea438cdfe66b6e4ed5d5aa044613" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 6, + "taken": null, + "cue": "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f", + "start": 1792114800000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 7, + "duration": 10800000, + "slots": 4 + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 2, + "asset": "b/doc-2", + "split": 1, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 7, + "taken": null, + "cue": "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99", + "start": 1792115400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "time_signal", + "id": 8, + "duration": 5400000, + "slots": null + }, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 2, + "asset": "b/doc-2", + "split": 1, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 1, + "index": 0, + "taken": null, + "cue": "6d8bd6db1e37f994de3520e78e5bdaa13a797b27783d42d231a13b131d3a1d4f", + "start": 1792112100000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": null, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "6d8bd6db1e37f994de3520e78e5bdaa13a797b27783d42d231a13b131d3a1d4f" + ], + [ + 13, + "527dd341c01a01e70d3a9f492f67bd4baba6cedd70ef8a77f018b7a2efacddd1" + ] + ] + }, + { + "class": "scheduled", + "room": null, + "index": null, + "taken": { + "run": 1, + "entry": 1 + }, + "cue": null, + "start": 1792114140000000, + "duration_ms": 60000, + "open": false, + "slots": null, + "slots_from": null, + "section": null, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 1, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "lines": [ + { + "split": 1, + "class": "cued", + "breaks": 5, + "break_ms": 420000, + "slots": 14, + "unslotted": 0, + "ppm": 600000, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 12, + "6d8bd6db1e37f994de3520e78e5bdaa13a797b27783d42d231a13b131d3a1d4f" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ], + [ + 12, + "c218b0f4f63c485384c06d7770579b468c146dc94192cbdea9d1f63bcdb9012f" + ], + [ + 12, + "e12396f9e1f76ee93218891a828c2e5d167e3624ac6d1aa16245225e55f70b99" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ], + [ + 13, + "527dd341c01a01e70d3a9f492f67bd4baba6cedd70ef8a77f018b7a2efacddd1" + ] + ] + } + ], + "totals": { + "breaks": 5, + "break_ms": 420000, + "slots": 14 + } + } + }, + { + "name": "check_owner_b_under_reported", + "about": "owner B's reported statement per programme beside its report and the signed statement: its second documentary far below the day's rate, and the day below the signed impressions and revenue", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010002180000000000000000366e0100000000c027090000badb00000000000f00000060192deecc24c9e81f61982464d2de4bf81efd2964cf02ce2a5b23cafd010c7f00badb0000000000b68fb1554a0007aadd2fe92cbc9b3941c6c035bf1ab5df141da05154982739898637c1d463bfdf56db5dd3f43409ae8264e78c13a0c2994e72ffe82f66b9b30f", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d03000000000000049305d41c8ef6c826473bf7101a5a684ef0962416da7212ca1706a7a4e207a226221efca94a1eb2bad970287ca8323aee7912e62db6218f5e623969016cc61a0c", + "outcome": { + "outcome": "accept", + "check": "6558de6176a975984723daa36feefddbb73d44b029a680bd7250f423386835f4", + "issuer": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "report": "96d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb76", + "party": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "start": 1792108800000000, + "end": 1792195200000000, + "reported": "54438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe7328", + "form": "json", + "basis": "share", + "statement": "092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d6", + "floor_ppm": 500000, + "ceiling_ppm": 2000000, + "lines": [ + { + "scope": "period", + "key": "", + "breaks": 4, + "break_ms": 360000, + "slots": 12, + "unslotted": 0, + "impressions": 14, + "revenue": 8400000, + "fill_ppm": 1166666, + "revenue_per_slot": 700000, + "signed": { + "impressions": 24, + "revenue": 14400000 + }, + "flags": [ + "impressions_below_signed", + "revenue_below_signed" + ] + }, + { + "scope": "programme", + "key": "b/doc-1", + "breaks": 2, + "break_ms": 180000, + "slots": 6, + "unslotted": 0, + "impressions": 12, + "revenue": 7200000, + "fill_ppm": 2000000, + "revenue_per_slot": 1200000, + "signed": null, + "flags": [] + }, + { + "scope": "programme", + "key": "b/doc-2", + "breaks": 2, + "break_ms": 180000, + "slots": 6, + "unslotted": 0, + "impressions": 2, + "revenue": 1200000, + "fill_ppm": 333333, + "revenue_per_slot": 200000, + "signed": null, + "flags": [ + "below" + ] + } + ] + } + }, + { + "name": "check_owner_a_csv", + "about": "owner A's reported statement in CSV, per day, beside its report and the signed statement: nothing flagged", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "a_days", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100000002180000000000000000366e010000000015160500f8117a00000000000f000000fe9351db786bb0c6894a01160903dba38f93f6fe161d44d481b04f8ad1ba7e10f8117a0000000000480c733c3fcdcfe7f0fdd07e5e4ee5109a95691661583da49e9b144864463b478477cd301cd2e3f0ebf848a71dc3b1b1917c902c2b9526119a32e475d03d0a09", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a92a0d762f7106ee7f42bc0aa5e0bb0d788da58fecbf797a7bec0bc272c21130925dc1d8fcbbc51373d396bd319a469ef8db64adfa0157509d125a9fd9dcbf53a8024e976ea60293e6de76e4fe844d7105bbbddc31eb87c7b24385ad8ccfbd18d45220a1070080841e000200010000050000000000000070f30500000000000b0000000000000001000000000000001800000000000000f8117a0000000000ba4a210000000000e8180b0000000000011800000000000000f8117a000000000000020a00323032362d31302d3136050000000000000070f30500000000000b0000000000000001000000000000001800000000000000f8117a0000000000ba4a210000000000e8180b00000000000000ca64448b9558f9e3c640d45422221c8f0d75b5b8eb052f971c79c88c845c870302d27bf7317068a6497e9f81868ceedf6e6d8bd8991379a24a6a533fbddda209", + "outcome": { + "outcome": "accept", + "check": "8eddf0abe925c91b57e442fc92cd503063d5cd8f2fdcf7a5293dc15f40d1a8c4", + "issuer": "50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a9", + "report": "2a0d762f7106ee7f42bc0aa5e0bb0d788da58fecbf797a7bec0bc272c2113092", + "party": "50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a9", + "start": 1792108800000000, + "end": 1792195200000000, + "reported": "5dc1d8fcbbc51373d396bd319a469ef8db64adfa0157509d125a9fd9dcbf53a8", + "form": "csv", + "basis": "share", + "statement": "4e976ea60293e6de76e4fe844d7105bbbddc31eb87c7b24385ad8ccfbd18d452", + "floor_ppm": 500000, + "ceiling_ppm": 2000000, + "lines": [ + { + "scope": "period", + "key": "", + "breaks": 5, + "break_ms": 390000, + "slots": 11, + "unslotted": 1, + "impressions": 24, + "revenue": 7999992, + "fill_ppm": 2181818, + "revenue_per_slot": 727272, + "signed": { + "impressions": 24, + "revenue": 7999992 + }, + "flags": [] + }, + { + "scope": "day", + "key": "2026-10-16", + "breaks": 5, + "break_ms": 390000, + "slots": 11, + "unslotted": 1, + "impressions": 24, + "revenue": 7999992, + "fill_ppm": 2181818, + "revenue_per_slot": 727272, + "signed": null, + "flags": [] + } + ] + } + }, + { + "name": "check_owner_b_without_statement", + "about": "owner B's reported statement beside its report alone", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": null, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801000000000000000000000000000000000000000000000000000000000000000020a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a00000000000000030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d030000000000000403ccc9cdae29c90c6589cd524a01545ffdaa257287a861de783bb35c21ecc1fcd2d427ab8d46299b3bec07c95b7a413f70443e654ea9aec68049260b505cdf0e", + "outcome": { + "outcome": "accept", + "check": "d4e4d98e69dc8d02de0b2969b57a0f3179b5941432af6b0636a3ef26ed637b50", + "issuer": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "report": "96d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb76", + "party": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "start": 1792108800000000, + "end": 1792195200000000, + "reported": "54438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe7328", + "form": "json", + "basis": "share", + "statement": null, + "floor_ppm": 500000, + "ceiling_ppm": 2000000, + "lines": [ + { + "scope": "period", + "key": "", + "breaks": 4, + "break_ms": 360000, + "slots": 12, + "unslotted": 0, + "impressions": 14, + "revenue": 8400000, + "fill_ppm": 1166666, + "revenue_per_slot": 700000, + "signed": null, + "flags": [] + }, + { + "scope": "programme", + "key": "b/doc-1", + "breaks": 2, + "break_ms": 180000, + "slots": 6, + "unslotted": 0, + "impressions": 12, + "revenue": 7200000, + "fill_ppm": 2000000, + "revenue_per_slot": 1200000, + "signed": null, + "flags": [] + }, + { + "scope": "programme", + "key": "b/doc-2", + "breaks": 2, + "break_ms": 180000, + "slots": 6, + "unslotted": 0, + "impressions": 2, + "revenue": 1200000, + "fill_ppm": 333333, + "revenue_per_slot": 200000, + "signed": null, + "flags": [ + "below" + ] + } + ] + } + }, + { + "name": "check_owner_b_reports_a_programme", + "about": "owner B's reported statement listing owner A's episode: impressions where no break of B's aired", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes_and_a_programme", + "statement": null, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb76ae896e182e3b2f3de0f9b130270911ee5f3c7f863b44f0bc0be6a9662ddcb41701000000000000000000000000000000000000000000000000000000000000000020a1070080841e0004000100000400000000000000407e0500000000000c000000000000000000000000000000130000000000000040f3ad0000000000e528180000000000f07e0e00000000000000030600612f65702d3100000000000000000000000000000000000000000000000000000000000000000500000000000000c0c62d0000000000000000000000000000000000000000000001030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d0300000000000004afefe0726e6d79145b6ee21dac055696675a3f9ce975d62ea3efb1eb19fec4545c0d2cfdbb2d0d68a1b06f148d06fd08d1d26afabf4510a74aa84fd3c9c5ba0a", + "outcome": { + "outcome": "accept", + "check": "35db333eb19f7d6347c6cd6d98815a30c3b1f6fcdacc5bed2a19dd67ac6d7456", + "issuer": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "report": "96d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb76", + "party": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "start": 1792108800000000, + "end": 1792195200000000, + "reported": "ae896e182e3b2f3de0f9b130270911ee5f3c7f863b44f0bc0be6a9662ddcb417", + "form": "json", + "basis": "share", + "statement": null, + "floor_ppm": 500000, + "ceiling_ppm": 2000000, + "lines": [ + { + "scope": "period", + "key": "", + "breaks": 4, + "break_ms": 360000, + "slots": 12, + "unslotted": 0, + "impressions": 19, + "revenue": 11400000, + "fill_ppm": 1583333, + "revenue_per_slot": 950000, + "signed": null, + "flags": [] + }, + { + "scope": "programme", + "key": "a/ep-1", + "breaks": 0, + "break_ms": 0, + "slots": 0, + "unslotted": 0, + "impressions": 5, + "revenue": 3000000, + "fill_ppm": 0, + "revenue_per_slot": 0, + "signed": null, + "flags": [ + "unproved" + ] + }, + { + "scope": "programme", + "key": "b/doc-1", + "breaks": 2, + "break_ms": 180000, + "slots": 6, + "unslotted": 0, + "impressions": 12, + "revenue": 7200000, + "fill_ppm": 2000000, + "revenue_per_slot": 1200000, + "signed": null, + "flags": [] + }, + { + "scope": "programme", + "key": "b/doc-2", + "breaks": 2, + "break_ms": 180000, + "slots": 6, + "unslotted": 0, + "impressions": 2, + "revenue": 1200000, + "fill_ppm": 333333, + "revenue_per_slot": 200000, + "signed": null, + "flags": [ + "below" + ] + } + ] + } + }, + { + "name": "document_malformed", + "about": "a report's document whose runs are not a list", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": null, + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "malformed", + "at": [] + } + }, + { + "name": "contract_offered", + "about": "a contract owner B has not countersigned", + "kind": "report", + "report": { + "contract": "offered", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "not_countersigned", + "at": [ + "contract", + "owner", + 1 + ] + } + }, + { + "name": "report_truncated", + "about": "a report of ten bytes", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "truncated", + "at": [ + "report" + ] + } + }, + { + "name": "report_magic", + "about": "a report whose magic is WOR2", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52320150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "malformed", + "at": [ + "report" + ] + } + }, + { + "name": "report_version_2", + "about": "a report of version 2", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310250e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "unknown_version", + "at": [ + "report" + ] + } + }, + { + "name": "report_lines_unordered", + "about": "a report whose lines are not in ascending order", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0050000000000000070f30500000000000b00000000000000989a6dd792f2d9cf001d70ae6bcf0120b123d69143cb1e7a9a639c053d2ea03c5c589f67030fc24f4c69d4578a20b0125a40526559eca7953897b7630649ed07", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "opportunity_report_invalid", + "at": [ + "report" + ] + } + }, + { + "name": "report_of_another_contract", + "about": "a report naming the contract of two rooms", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a96f97aba21b0674c6ef55e6c6468b7a107e638ca072e3da3535340bab6806f73b50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000406801b3b2c2126925007ac16f2099c2b74aba7478847c7b1dd7344c2c3c62d941c4c28bdbae1c83b4ebb592c657a64f9af3579c2ff7c83de1bc113ebd335f0f", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "contract_mismatch", + "at": [ + "report" + ] + } + }, + { + "name": "report_past_the_contract", + "about": "a report whose period ends a day after the contract's", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a0b9d33f5f06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b0000000000000030ae948dda23e105f18f563d12833fe4bc68b1d704d92c2d778884decf83d87e084d9afc8dc1b73bcc583a46741631e87e31b59844fa9b75dc62ba12a9e0bc0a", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "outside_contract", + "at": [ + "report" + ] + } + }, + { + "name": "report_signature", + "about": "a report whose signature has a byte flipped", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c300ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "bad_signature", + "at": [ + "report" + ] + } + }, + { + "name": "report_issuer_small_order", + "about": "a report whose issuer key is of small order", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f523101010000000000000000000000000000000000000000000000000000000000000035f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "bad_key", + "at": [ + "report" + ] + } + }, + { + "name": "run_aired_misstated", + "about": "a run whose aired set the window does not give, signed again", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1_aired_misstated", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "run_invalid", + "at": [ + "run", + 0 + ] + } + }, + { + "name": "room_event_signature", + "about": "a room whose producer's third event has a byte of its signature flipped", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main_signature_flipped" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "bad_signature", + "at": [ + "room", + 0, + "room", + "record", + 1, + 2 + ] + } + }, + { + "name": "room_with_a_gap", + "about": "a room whose producer's chain lacks its fifth event", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main_with_a_gap" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "delivery_gap", + "at": [ + "room", + 0 + ] + } + }, + { + "name": "room_foreign", + "about": "a room the contract does not name", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "backup" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "room_foreign", + "at": [ + "room", + 0 + ] + } + }, + { + "name": "room_repeated", + "about": "the contract's room given twice", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main", + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "room_repeated", + "at": [ + "room", + 1 + ] + } + }, + { + "name": "cue_section_crc", + "about": "a room whose second break's section has its CRC damaged", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main_crc_flipped" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "cue_section_invalid", + "at": [ + "room", + 0, + "break", + 1 + ] + } + }, + { + "name": "cue_section_other_id", + "about": "a room whose fourth break's section names segmentation event 40 for cue 4", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main_other_segmentation_id" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "cue_section_mismatch", + "at": [ + "room", + 0, + "break", + 3 + ] + } + }, + { + "name": "two_rooms_overlapping", + "about": "a second room of the contract's whose break falls inside the first room's second break", + "kind": "report", + "report": { + "contract": "two_rooms", + "contracts": [], + "report": "574f52310181640fe9c7b63a1babed05713ed2f9958aa011b8d31edcf9c611f375138687376f97aba21b0674c6ef55e6c6468b7a107e638ca072e3da3535340bab6806f73b72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d0600020015bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c21ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010500000000000000a0680600000000000e000000000000000000000000000000c02709000a00000066d9fde31098615b73fffae3dc326088e78d3c40125e2f2a33fe3f8f6d7b82590500000000000000a0680600000000000e00000000000000ce02b4bf5c09a7b468dbb20363127ea724cce06cccd5f4cb2a406389001538451a8a5d6d3b7980074c54e7d26b93dd418af26017323c2e23ba4dcefcc0034306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main", + "backup_overlapping" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "break_overlap", + "at": [ + "room", + 1, + "break", + 0 + ] + } + }, + { + "name": "break_outside_every_window", + "about": "the run of the first hour alone: the breaks of the second aired in no window held", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "outside_window", + "at": [ + "room", + 0, + "break", + 5 + ] + } + }, + { + "name": "asset_unmapped", + "about": "a run of the second hour whose last programme the contract does not map", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2_unmapped" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "asset_unmapped", + "at": [ + "room", + 0, + "break", + 6 + ] + } + }, + { + "name": "line_of_another_split", + "about": "owner A's report with a line of owner B's split", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0300000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f9650100010400000000000000407e0500000000000c0000000000000000000000000000000100000008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba2527050000000000000070f30500000000000b000000000000004d6742f338ec4bc81938671118cccdaf8decef70ead0349e5da34dd9e801a72c18ff368e451d7ab19aaefc77306613c8295fc7f0932a4c4e24996df6cd55d30b", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "line_unsourced", + "at": [ + "report" + ] + } + }, + { + "name": "line_citations_changed", + "about": "a line whose citation digest has a byte changed", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000198f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b0000000000000036bab081990ef5355a2fb6ddd47f6a6c37d1e79fd391838fd1511903cc48614945778968bd7aec9bd6f0f54f37f268d11cef8b95462dd4bc8f981e6ff2c4ed05", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "citation_mismatch", + "at": [ + "report" + ] + } + }, + { + "name": "line_seconds_raised", + "about": "a line stating a second more of breaks, and the total with it", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b02000000010400000000000000f80c0500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000058f70500000000000b000000000000001b06683c8f5b31e2660bd138743ed81762983e7f1621ef6a7a2435ab1bbb667c5c1a9fa79e0176cae7ddb57ef97944cdd0cc1eb338de8026ac287a9b52e03806", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "opportunity_report_mismatch", + "at": [ + "report" + ] + } + }, + { + "name": "runs_listed_short", + "about": "a report listing one of the two runs it is given", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c701000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b0000000000000055efbce6537590c6761056d15ace35260f18d069a45a98f9d91bccbd414b6b3e07907850468f1c74fee838b7b3eb85fd5326571f752bf409159f41944364e20b", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "opportunity_report_mismatch", + "at": [ + "report" + ] + } + }, + { + "name": "check_document_malformed", + "about": "a check's document whose reported statement is of a form this text does not define", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "of_another_form", + "statement": null, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d03000000000000049305d41c8ef6c826473bf7101a5a684ef0962416da7212ca1706a7a4e207a226221efca94a1eb2bad970287ca8323aee7912e62db6218f5e623969016cc61a0c", + "outcome": { + "outcome": "refuse", + "reason": "malformed", + "at": [] + } + }, + { + "name": "check_report_refused", + "about": "a check whose report has a byte of its signature flipped, refused at the report's place", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6d7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010002180000000000000000366e0100000000c027090000badb00000000000f00000060192deecc24c9e81f61982464d2de4bf81efd2964cf02ce2a5b23cafd010c7f00badb0000000000b68fb1554a0007aadd2fe92cbc9b3941c6c035bf1ab5df141da05154982739898637c1d463bfdf56db5dd3f43409ae8264e78c13a0c2994e72ffe82f66b9b30f", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d03000000000000049305d41c8ef6c826473bf7101a5a684ef0962416da7212ca1706a7a4e207a226221efca94a1eb2bad970287ca8323aee7912e62db6218f5e623969016cc61a0c", + "outcome": { + "outcome": "refuse", + "reason": "bad_signature", + "at": [ + "report", + "report" + ] + } + }, + { + "name": "reported_day_outside_the_period", + "about": "a reported statement listing a day after its period", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_a_day_after_the_period", + "statement": null, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d03000000000000049305d41c8ef6c826473bf7101a5a684ef0962416da7212ca1706a7a4e207a226221efca94a1eb2bad970287ca8323aee7912e62db6218f5e623969016cc61a0c", + "outcome": { + "outcome": "refuse", + "reason": "reported_invalid", + "at": [ + "reported" + ] + } + }, + { + "name": "reported_csv_header", + "about": "a reported statement in CSV whose header misspells a column", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "a_days_header_misspelt", + "statement": null, + "check": "57434b310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a92a0d762f7106ee7f42bc0aa5e0bb0d788da58fecbf797a7bec0bc272c21130925dc1d8fcbbc51373d396bd319a469ef8db64adfa0157509d125a9fd9dcbf53a8024e976ea60293e6de76e4fe844d7105bbbddc31eb87c7b24385ad8ccfbd18d45220a1070080841e000200010000050000000000000070f30500000000000b0000000000000001000000000000001800000000000000f8117a0000000000ba4a210000000000e8180b0000000000011800000000000000f8117a000000000000020a00323032362d31302d3136050000000000000070f30500000000000b0000000000000001000000000000001800000000000000f8117a0000000000ba4a210000000000e8180b00000000000000ca64448b9558f9e3c640d45422221c8f0d75b5b8eb052f971c79c88c845c870302d27bf7317068a6497e9f81868ceedf6e6d8bd8991379a24a6a533fbddda209", + "outcome": { + "outcome": "refuse", + "reason": "reported_invalid", + "at": [ + "reported" + ] + } + }, + { + "name": "reported_of_another_channel", + "about": "a reported statement of another channel", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes_of_another_channel", + "statement": null, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d03000000000000049305d41c8ef6c826473bf7101a5a684ef0962416da7212ca1706a7a4e207a226221efca94a1eb2bad970287ca8323aee7912e62db6218f5e623969016cc61a0c", + "outcome": { + "outcome": "refuse", + "reason": "reported_foreign", + "at": [ + "reported" + ] + } + }, + { + "name": "check_statement_refused", + "about": "a signed statement with a byte of its signature flipped, refused at the statement's place", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010002180000000000000000366e0100000000c027090000badb00000000000f00000060192deecc24c9e81f61982464d2de4bf81efd2964cf02ce2a5b23cafd010c7f00badb0000000000b68fb1554a0007aadd2fe92cbc9b3941c6c035bf1ab5df141da05154982739898637c1d463bfdf56db5dd3f43409ae8264e78c13a0c2984e72ffe82f66b9b30f", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d03000000000000049305d41c8ef6c826473bf7101a5a684ef0962416da7212ca1706a7a4e207a226221efca94a1eb2bad970287ca8323aee7912e62db6218f5e623969016cc61a0c", + "outcome": { + "outcome": "refuse", + "reason": "bad_signature", + "at": [ + "statement", + "statement" + ] + } + }, + { + "name": "statement_of_another_owner", + "about": "owner B's report beside owner A's signed statement", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100000002180000000000000000366e010000000015160500f8117a00000000000f000000fe9351db786bb0c6894a01160903dba38f93f6fe161d44d481b04f8ad1ba7e10f8117a0000000000480c733c3fcdcfe7f0fdd07e5e4ee5109a95691661583da49e9b144864463b478477cd301cd2e3f0ebf848a71dc3b1b1917c902c2b9526119a32e475d03d0a09", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d03000000000000049305d41c8ef6c826473bf7101a5a684ef0962416da7212ca1706a7a4e207a226221efca94a1eb2bad970287ca8323aee7912e62db6218f5e623969016cc61a0c", + "outcome": { + "outcome": "refuse", + "reason": "statement_foreign", + "at": [ + "statement" + ] + } + }, + { + "name": "check_truncated", + "about": "a check of ten bytes", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010002180000000000000000366e0100000000c027090000badb00000000000f00000060192deecc24c9e81f61982464d2de4bf81efd2964cf02ce2a5b23cafd010c7f00badb0000000000b68fb1554a0007aadd2fe92cbc9b3941c6c035bf1ab5df141da05154982739898637c1d463bfdf56db5dd3f43409ae8264e78c13a0c2994e72ffe82f66b9b30f", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bb", + "outcome": { + "outcome": "refuse", + "reason": "truncated", + "at": [ + "check" + ] + } + }, + { + "name": "check_floor_above_the_rate", + "about": "a check whose floor is above the whole", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010002180000000000000000366e0100000000c027090000badb00000000000f00000060192deecc24c9e81f61982464d2de4bf81efd2964cf02ce2a5b23cafd010c7f00badb0000000000b68fb1554a0007aadd2fe92cbc9b3941c6c035bf1ab5df141da05154982739898637c1d463bfdf56db5dd3f43409ae8264e78c13a0c2994e72ffe82f66b9b30f", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d660e3160080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d030000000000000467517a4d55144b2a8cefb25f350b8501ac02ae2d52d289e4026dfa0f23092056523a6f24afb7ba9004bccb483357368c861a97697d38b5bf1a0219e21e7fb70d", + "outcome": { + "outcome": "refuse", + "reason": "statement_check_invalid", + "at": [ + "check" + ] + } + }, + { + "name": "check_flags_changed", + "about": "a check whose flags leave out the programme's below", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010002180000000000000000366e0100000000c027090000badb00000000000f00000060192deecc24c9e81f61982464d2de4bf81efd2964cf02ce2a5b23cafd010c7f00badb0000000000b68fb1554a0007aadd2fe92cbc9b3941c6c035bf1ab5df141da05154982739898637c1d463bfdf56db5dd3f43409ae8264e78c13a0c2994e72ffe82f66b9b30f", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d0300000000000000f6f9b9a6c61951ca37052118a678777577d0ed4a91d02a49eb41295e8d331c0887f3f67ea4737c0afe9d15164c5281aa842d065f353cfedefda5d34284fe9c03", + "outcome": { + "outcome": "refuse", + "reason": "statement_check_mismatch", + "at": [ + "check" + ] + } + }, + { + "name": "check_of_another_reported_statement", + "about": "a check naming the bytes of another reported statement", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010002180000000000000000366e0100000000c027090000badb00000000000f00000060192deecc24c9e81f61982464d2de4bf81efd2964cf02ce2a5b23cafd010c7f00badb0000000000b68fb1554a0007aadd2fe92cbc9b3941c6c035bf1ab5df141da05154982739898637c1d463bfdf56db5dd3f43409ae8264e78c13a0c2994e72ffe82f66b9b30f", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb76ae448ac86c4e8e4dec645729708ef41873ae79c6dff84eff73360989487f08e501092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d030000000000000467dc813c971da2729392dcbf1c0f3144c438a706f4951320ca3e9c32fcbc58f35b0a43720919da7604b431ee1e03c1a3359d9fd01be66ac36615eac265516d04", + "outcome": { + "outcome": "refuse", + "reason": "statement_check_mismatch", + "at": [ + "check" + ] + } + }, + { + "name": "check_signature", + "about": "a check whose signature has a byte flipped", + "kind": "check", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000100010400000000000000407e0500000000000c000000000000000000000000000000c027090008000000d0fa303784106844e4b374990ee85598ffdd1d31c37aa9887e3cd6c16aba25270400000000000000407e0500000000000c00000000000000d03187f7b55d6ba8968befe5bec7a3812855dbde2462454dcebfb952a8b613fcb1fb71150f9c3a34f46dae0c5362f0a63c796c26f11a6c7a0944cc420a0dc306", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "reported": "b_programmes", + "statement": { + "contract": "main", + "statement": "575253310135f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000a043f6fd5d06004555520100be859400907614fc0f845febd42190b7286268cbe66ab76a20c61a3e27dffb3e02000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010002180000000000000000366e0100000000c027090000badb00000000000f00000060192deecc24c9e81f61982464d2de4bf81efd2964cf02ce2a5b23cafd010c7f00badb0000000000b68fb1554a0007aadd2fe92cbc9b3941c6c035bf1ab5df141da05154982739898637c1d463bfdf56db5dd3f43409ae8264e78c13a0c2994e72ffe82f66b9b30f", + "runs": [ + "hour_1", + "hour_2" + ], + "ledgers": [ + "day_16" + ] + }, + "check": "57434b310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7696d33c89eb4958f751363e3494ae57a42087e758ef56fcad8a549265b903fb7654438b8dc5d20b776871b4e014dd58bbe62d56e4f68d43b228970986bfbe732801092c5f62ff6cad8ed44e04253a5defcbd88f969bfab7ed42f03c777e1309d5d620a1070080841e0003000100000400000000000000407e0500000000000c0000000000000000000000000000000e00000000000000802c8000000000004acd11000000000060ae0a000000000001180000000000000000badb000000000050030700622f646f632d31020000000000000020bf020000000000060000000000000000000000000000000c0000000000000000dd6d000000000080841e0000000000804f1200000000000000030700622f646f632d32020000000000000020bf020000000000060000000000000000000000000000000200000000000000804f1200000000001516050000000000400d03000000000000049305d41c8ef6c826473bf7101a5a684ef0962416da7212ca1606a7a4e207a226221efca94a1eb2bad970287ca8323aee7912e62db6218f5e623969016cc61a0c", + "outcome": { + "outcome": "refuse", + "reason": "bad_signature", + "at": [ + "check" + ] + } + } + ] +} diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index d4400e981..a4db01b41 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -513,6 +513,14 @@ receipt_ledger = ["live_metering", "settlement", "live_ads", "video_package", "s # revenue divided by the largest-remainder rule. SHA-256, Ed25519 and the sources' own readers; no # new dependency. revenue_share = ["receipt_ledger", "video_channel"] +# Opportunity report (extensions/opportunity-report): `wai.delivery.opportunity` — the ad breaks a +# channel's own signed records prove, per owner and period (WOR1): the cues of a room a share +# contract names (live_ads, the SCTE 35 sections they carry read for their slots) and the ad +# entries of the channel's program-as-run, each attributed to the programme on air; and the check +# (WCK1) of a platform's reported figures, in a documented JSON or CSV form, against the report and +# the operator's signed revenue-share statement. SHA-256, Ed25519 and the sources' own readers; no +# new dependency. +opportunity_report = ["revenue_share"] # Stream audit (extensions/stream-audit): `wai.delivery.audit` — what a content owner gathers from the # players of a stream another service delivers over HTTP: settlement acknowledgements of every object # by plain SHA-256 (settlement §14), one chain per origin, under a fresh session key; presentation @@ -1313,6 +1321,11 @@ name = "wai_revenue_share_vectors" path = "src/bin/wai_revenue_share_vectors.rs" required-features = ["revenue_share"] +[[bin]] +name = "wai_opportunity_vectors" +path = "src/bin/wai_opportunity_vectors.rs" +required-features = ["opportunity_report"] + [[bin]] name = "wai_fleet_vectors" path = "src/bin/wai_fleet_vectors.rs" @@ -2329,6 +2342,11 @@ name = "revenue_share" path = "tests/revenue_share.rs" required-features = ["revenue_share"] +[[test]] +name = "opportunity_report" +path = "tests/opportunity_report.rs" +required-features = ["opportunity_report"] + [[test]] name = "fleet" path = "tests/fleet.rs" diff --git a/wai/wai-rs/src/bin/wai_opportunity_vectors.rs b/wai/wai-rs/src/bin/wai_opportunity_vectors.rs new file mode 100644 index 000000000..32caeadbd --- /dev/null +++ b/wai/wai-rs/src/bin/wai_opportunity_vectors.rs @@ -0,0 +1,61 @@ +//! Conformance corpus for the opportunity report (`live-conformance/opportunity/`, +//! extensions/opportunity-report.md §14). +//! +//! wai_opportunity_vectors gen write vectors.json, from fixed seeds +//! wai_opportunity_vectors verify vectors.json regenerates byte for byte, and +//! every vector gets the outcome it states +//! +//! The generator and this verifier are one implementation. The corpus's second implementation is +//! `live-conformance/opportunity_report_verify.py`, written from the extension's text and sharing +//! no code with this binary. + +use std::path::Path; +use wai::opportunity_report::corpus; + +fn main() { + let a: Vec = std::env::args().collect(); + match (a.get(1).map(String::as_str), a.get(2)) { + (Some("gen"), Some(d)) => { + let p = Path::new(d).join("vectors.json"); + let text = match corpus::generate() { + Ok(t) => t, + Err(e) => { + eprintln!("generate: {e}"); + std::process::exit(1); + } + }; + if let Err(e) = std::fs::create_dir_all(d).and_then(|_| std::fs::write(&p, text)) { + eprintln!("write {}: {e}", p.display()); + std::process::exit(1); + } + println!("wrote {}", p.display()); + } + (Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))), + _ => { + eprintln!("usage: wai_opportunity_vectors gen|verify "); + std::process::exit(2); + } + } +} + +fn verify(dir: &Path) -> i32 { + let p = dir.join("vectors.json"); + let doc = match std::fs::read_to_string(&p) { + Ok(s) => s, + Err(e) => { + eprintln!("read {}: {e}", p.display()); + return 1; + } + }; + let mut fail = 0; + let same = corpus::generate().is_ok_and(|g| g == doc); + println!("{} vectors.json regenerates byte for byte from its seeds", if same { "PASS" } else { "FAIL" }); + fail += (!same) as i32; + let fails = corpus::verify(&doc); + for f in &fails { + println!("FAIL {f}"); + } + println!("{} every vector gets the outcome the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" }); + fail += (!fails.is_empty()) as i32; + (fail != 0) as i32 +} diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 5e7ed726b..c045749ec 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -741,6 +741,14 @@ pub mod receipt_ledger; /// cbindgen:ignore #[cfg(feature = "revenue_share")] pub mod revenue_share; +// Opportunity report (extensions/opportunity-report.md): `wai.delivery.opportunity` — the ad breaks +// a channel's own signed records prove for one party and one period (a room's cues and their SCTE 35 +// sections, the program-as-run's ad entries), attributed by the share contract, and the check of a +// platform's reported figures against them and the signed revenue-share statement. Rust-only, like +// the other record modules: its constants must not become macros in include/wai.h. +/// cbindgen:ignore +#[cfg(feature = "opportunity_report")] +pub mod opportunity_report; // Stream audit (extensions/stream-audit.md): `wai.delivery.audit` — a player's signed evidence of a // stream another service delivers over HTTP (acknowledgements by plain SHA-256, presentation claims for // marked ads, the session record), the bundle a collector verifies, and the channel report it signs diff --git a/wai/wai-rs/src/opportunity_report.rs b/wai/wai-rs/src/opportunity_report.rs new file mode 100644 index 000000000..3371fcfa8 --- /dev/null +++ b/wai/wai-rs/src/opportunity_report.rs @@ -0,0 +1,1548 @@ +//! Opportunity report — `wai.delivery.opportunity` ([extensions/opportunity-report.md]). +//! +//! 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 [`crate::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 (made into breaks by [`crate::live_ads::verify`], the SCTE 35 sections +//! they carry read by [`section`]) or from an ad entry of the channel's signed program-as-run, +//! 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** ([`reported`]) is what a platform says it counted. A +//! **statement check** (`WCK1`) sets the report beside it and, where given, beside the +//! operator's signed revenue-share statement. +//! +//! What this module implements, by section of the extension: +//! +//! - §3: the evidence: the contract, the runs and the rooms; +//! - §4, §6: breaks ([`Break`]), cued and scheduled, their slots, and their attribution; +//! - §5: the section a cue carries ([`section`]); +//! - §7, §8: the report ([`Report`]), its lines ([`compute`]), its verification ([`verify`]) and +//! [`create`]; +//! - §9: the reported statement ([`reported`]); +//! - §10, §11: the check ([`Check`]), its lines ([`check_lines`]), [`verify_check`] and +//! [`create_check`]. +//! +//! The corpus `live-conformance/opportunity/` is written by [`corpus::generate`] and checked by +//! [`corpus::verify`], and by `live-conformance/opportunity_report_verify.py`, which shares no +//! code with this module. +//! +//! [extensions/opportunity-report.md]: ../../extensions/opportunity-report.md + +use std::collections::{BTreeMap, BTreeSet}; + +use ed25519_dalek::{Signer, SigningKey}; +use serde_json::{Map, Value, json}; +use sha2::{Digest, Sha256}; + +use crate::live_ads as ads; +use crate::live_events::{self as events, Event, Payload, kind}; +use crate::live_room as room; +use crate::revenue_share::{self as share, Contract, hex, unhex}; +use crate::wai_video_channel::{ChannelReceipt, EntryKind}; + +pub mod corpus; +pub mod reported; +pub mod section; + +use reported::{Reported, basis, form}; + +pub const CAPABILITY: &str = "wai.delivery.opportunity"; +pub const REPORT_MAGIC: [u8; 4] = *b"WOR1"; +pub const CHECK_MAGIC: [u8; 4] = *b"WCK1"; +pub const VERSION: u8 = 1; +pub const REPORT_DOMAIN: &[u8] = b"wai:opportunity-report\x01"; +pub const CHECK_DOMAIN: &[u8] = b"wai:statement-check\x01"; +pub const CITES_DOMAIN: &[u8] = b"wai:opportunity-cites\x01"; +pub const WHOLE: u32 = 1_000_000; +/// A report's line, in bytes (§7.2). +pub const LINE_LEN: usize = 75; +/// The bounds `statement.check` uses when none is given (§12). +pub const FLOOR_PPM: u32 = 500_000; +pub const CEILING_PPM: u32 = 2_000_000; + +/// The classes of a break (§4.3). +pub mod class { + pub const CUED: u8 = 1; + pub const SCHEDULED: u8 = 2; + + pub fn name(c: u8) -> &'static str { + match c { + CUED => "cued", + SCHEDULED => "scheduled", + _ => "?", + } + } +} + +/// The kinds of a cited record (§7.3). +pub mod record_kind { + pub const RUN: u8 = 10; + pub const CUE: u8 = 12; + pub const POLICY: u8 = 13; +} + +/// The scopes of a check's line (§10.2). +pub mod scope { + pub const PERIOD: u8 = 1; + pub const DAY: u8 = 2; + pub const PROGRAMME: u8 = 3; + + pub fn name(s: u8) -> &'static str { + match s { + PERIOD => "period", + DAY => "day", + PROGRAMME => "programme", + _ => "?", + } + } +} + +/// The flags of a check's line (§10.3), by bit. +pub const FLAGS: [&str; 7] = ["unproved", "above", "below", "impressions_above_signed", "impressions_below_signed", "revenue_above_signed", "revenue_below_signed"]; + +// --------------------------------------------------------------------------------------- +// Reasons +// --------------------------------------------------------------------------------------- + +/// The codes this extension adds (§13). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Reason { + RoomForeign, + RoomRepeated, + CueSectionInvalid, + CueSectionMismatch, + OpportunityReportInvalid, + OpportunityReportMismatch, + ReportedInvalid, + ReportedForeign, + StatementForeign, + StatementCheckInvalid, + StatementCheckMismatch, +} + +impl Reason { + /// §13's order. + pub const ALL: [Reason; 11] = [ + Reason::RoomForeign, + Reason::RoomRepeated, + Reason::CueSectionInvalid, + Reason::CueSectionMismatch, + Reason::OpportunityReportInvalid, + Reason::OpportunityReportMismatch, + Reason::ReportedInvalid, + Reason::ReportedForeign, + Reason::StatementForeign, + Reason::StatementCheckInvalid, + Reason::StatementCheckMismatch, + ]; + + /// The codes of other extensions this one raises itself (§13). + pub const REUSED: [&'static str; 13] = [ + "truncated", + "malformed", + "unknown_version", + "bad_key", + "bad_signature", + "contract_mismatch", + "outside_contract", + "delivery_gap", + "break_overlap", + "outside_window", + "asset_unmapped", + "line_unsourced", + "citation_mismatch", + ]; + + pub fn code(self) -> &'static str { + match self { + Reason::RoomForeign => "room_foreign", + Reason::RoomRepeated => "room_repeated", + Reason::CueSectionInvalid => "cue_section_invalid", + Reason::CueSectionMismatch => "cue_section_mismatch", + Reason::OpportunityReportInvalid => "opportunity_report_invalid", + Reason::OpportunityReportMismatch => "opportunity_report_mismatch", + Reason::ReportedInvalid => "reported_invalid", + Reason::ReportedForeign => "reported_foreign", + Reason::StatementForeign => "statement_foreign", + Reason::StatementCheckInvalid => "statement_check_invalid", + Reason::StatementCheckMismatch => "statement_check_mismatch", + } + } +} + +impl std::fmt::Display for Reason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.code()) + } +} + +/// A refusal: the reason's code, and where, as a JSON array. +pub type Refusal = (&'static str, Value); + +const TRUNCATED: &str = "truncated"; +const MALFORMED: &str = "malformed"; +const UNKNOWN_VERSION: &str = "unknown_version"; +const CONTRACT_MISMATCH: &str = "contract_mismatch"; +const OUTSIDE_CONTRACT: &str = "outside_contract"; +const DELIVERY_GAP: &str = "delivery_gap"; +const BREAK_OVERLAP: &str = "break_overlap"; +const OUTSIDE_WINDOW: &str = "outside_window"; +const ASSET_UNMAPPED: &str = "asset_unmapped"; +const LINE_UNSOURCED: &str = "line_unsourced"; +const CITATION_MISMATCH: &str = "citation_mismatch"; + +// --------------------------------------------------------------------------------------- +// Bytes +// --------------------------------------------------------------------------------------- + +fn sha256(parts: &[&[u8]]) -> [u8; 32] { + let mut h = Sha256::new(); + for p in parts { + h.update(p); + } + h.finalize().into() +} + +fn ascending(v: &[T]) -> bool { + v.windows(2).all(|w| w[0] < w[1]) +} + +fn sign(key: &SigningKey, domain: &[u8], body: &[u8]) -> [u8; 64] { + let mut m = domain.to_vec(); + m.extend_from_slice(body); + key.sign(&m).to_bytes() +} + +fn check_sig(key: &[u8; 32], domain: &[u8], body: &[u8], sig: &[u8; 64]) -> Result<(), &'static str> { + let mut m = domain.to_vec(); + m.extend_from_slice(body); + crate::sig::verify_strict(key, &m, sig).map_err(|e| match e { + crate::sig::SigError::BadKey => "bad_key", + crate::sig::SigError::BadSignature => "bad_signature", + }) +} + +/// A cursor; running out is `truncated`. +struct Rd<'a> { + b: &'a [u8], + p: usize, +} + +impl<'a> Rd<'a> { + fn take(&mut self, n: usize) -> Result<&'a [u8], &'static str> { + let e = self.p.checked_add(n).filter(|&e| e <= self.b.len()).ok_or(TRUNCATED)?; + let s = &self.b[self.p..e]; + self.p = e; + Ok(s) + } + fn arr(&mut self) -> Result<[u8; N], &'static str> { + Ok(self.take(N)?.try_into().expect("N bytes")) + } + fn u8(&mut self) -> Result { + Ok(self.take(1)?[0]) + } + fn u16(&mut self) -> Result { + Ok(u16::from_le_bytes(self.arr()?)) + } + fn u32(&mut self) -> Result { + Ok(u32::from_le_bytes(self.arr()?)) + } + fn u64(&mut self) -> Result { + Ok(u64::from_le_bytes(self.arr()?)) + } + fn text(&mut self) -> Result, &'static str> { + let n = self.u16()? as usize; + Ok(self.take(n)?.to_vec()) + } + fn digests(&mut self) -> Result, &'static str> { + let n = self.u16()? as usize; + (0..n).map(|_| self.arr()).collect() + } + fn done(&self) -> bool { + self.p == self.b.len() + } +} + +/// The first steps of reading (§7.5, §10.6): the magic and the version. +fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<(), &'static str> { + if b.len() < 4 { + return Err(TRUNCATED); + } + if &b[..4] != magic { + return Err(MALFORMED); + } + match b.get(4) { + None => Err(TRUNCATED), + Some(&VERSION) => Ok(()), + Some(_) => Err(UNKNOWN_VERSION), + } +} + +fn put_digests(o: &mut Vec, l: &[[u8; 32]]) { + o.extend_from_slice(&u16::try_from(l.len()).expect("at most 65535 entries").to_le_bytes()); + for d in l { + o.extend_from_slice(d); + } +} + +/// `prefix` followed by the elements of `rest`. +fn place(prefix: Value, rest: Value) -> Value { + let mut v = prefix.as_array().cloned().unwrap_or_default(); + v.extend(rest.as_array().cloned().unwrap_or_default()); + Value::Array(v) +} + +// --------------------------------------------------------------------------------------- +// §7 The opportunity report +// --------------------------------------------------------------------------------------- + +/// A report's line (§7.2). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Line { + pub split: u16, + pub class: u8, + pub breaks: u64, + pub break_ms: u64, + pub slots: u64, + pub unslotted: u64, + pub ppm: u32, + pub cited: u32, + pub cites: [u8; 32], +} + +impl Line { + fn key(&self) -> (u16, u8) { + (self.split, self.class) + } +} + +/// An opportunity report (§7.1). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Report { + pub issuer: [u8; 32], + pub contract: [u8; 32], + pub party: [u8; 32], + pub start: u64, + pub end: u64, + pub rooms: Vec<[u8; 32]>, + pub runs: Vec<[u8; 32]>, + pub lines: Vec, + pub breaks: u64, + pub break_ms: u64, + pub slots: u64, + pub sig: [u8; 64], +} + +impl Report { + /// The report without `sig`. + pub fn body(&self) -> Vec { + let mut o = Vec::with_capacity(200 + 32 * (self.rooms.len() + self.runs.len()) + LINE_LEN * self.lines.len()); + o.extend_from_slice(&REPORT_MAGIC); + o.push(VERSION); + o.extend_from_slice(&self.issuer); + o.extend_from_slice(&self.contract); + o.extend_from_slice(&self.party); + o.extend_from_slice(&self.start.to_le_bytes()); + o.extend_from_slice(&self.end.to_le_bytes()); + put_digests(&mut o, &self.rooms); + put_digests(&mut o, &self.runs); + o.extend_from_slice(&u16::try_from(self.lines.len()).expect("at most 65535 lines").to_le_bytes()); + for l in &self.lines { + o.extend_from_slice(&l.split.to_le_bytes()); + o.push(l.class); + for v in [l.breaks, l.break_ms, l.slots, l.unslotted] { + o.extend_from_slice(&v.to_le_bytes()); + } + o.extend_from_slice(&l.ppm.to_le_bytes()); + o.extend_from_slice(&l.cited.to_le_bytes()); + o.extend_from_slice(&l.cites); + } + for v in [self.breaks, self.break_ms, self.slots] { + o.extend_from_slice(&v.to_le_bytes()); + } + o + } + + pub fn to_bytes(&self) -> Vec { + let mut o = self.body(); + o.extend_from_slice(&self.sig); + o + } + + /// Its digest (§7.4). + pub fn digest(&self) -> [u8; 32] { + sha256(&[&self.body()]) + } + + /// Signed by its issuer (§7.4), whose key it names. + pub fn sign(mut self, issuer: &SigningKey) -> Report { + self.issuer = issuer.verifying_key().to_bytes(); + self.sig = sign(issuer, REPORT_DOMAIN, &self.body()); + self + } + + /// Read a report and hold it to its rules (§7.5); its signature is checked in §8.2 step 3. + pub fn read(b: &[u8]) -> Result { + read_head(b, &REPORT_MAGIC)?; + let mut r = Rd { b, p: 5 }; + let (issuer, contract, party) = (r.arr()?, r.arr()?, r.arr()?); + let (start, end) = (r.u64()?, r.u64()?); + let rooms = r.digests()?; + let runs = r.digests()?; + let n = r.u16()? as usize; + let mut lines = Vec::with_capacity(n); + for _ in 0..n { + lines.push(Line { + split: r.u16()?, + class: r.u8()?, + breaks: r.u64()?, + break_ms: r.u64()?, + slots: r.u64()?, + unslotted: r.u64()?, + ppm: r.u32()?, + cited: r.u32()?, + cites: r.arr()?, + }); + } + let (breaks, break_ms, slots) = (r.u64()?, r.u64()?, r.u64()?); + let sig = r.arr()?; + if !r.done() { + return Err(MALFORMED); + } + let rep = Report { issuer, contract, party, start, end, rooms, runs, lines, breaks, break_ms, slots, sig }; + let keys: Vec<(u16, u8)> = rep.lines.iter().map(Line::key).collect(); + let rules = rep.start < rep.end + && rep.issuer != [0; 32] + && rep.party != [0; 32] + && ascending(&rep.rooms) + && ascending(&rep.runs) + && ascending(&keys) + && rep.lines.iter().all(|l| (1..=2).contains(&l.class) && l.breaks > 0 && l.cited > 0 && l.unslotted <= l.breaks && (1..=WHOLE).contains(&l.ppm)); + if !rules { + return Err(Reason::OpportunityReportInvalid.code()); + } + Ok(rep) + } +} + +/// A line's citation digest (§7.3) over its cited records, in ascending order. +pub fn cites_digest(cited: &Cited) -> [u8; 32] { + let mut h = Sha256::new(); + h.update(CITES_DOMAIN); + for (k, id) in cited { + h.update([*k]); + h.update(id); + } + h.finalize().into() +} + +/// Records by kind and identity, in ascending order. +pub type Cited = BTreeSet<(u8, [u8; 32])>; + +// --------------------------------------------------------------------------------------- +// §3 The evidence +// --------------------------------------------------------------------------------------- + +/// A room as a verification document holds it (§8.1). +#[derive(Clone, Debug)] +pub struct RoomInput { + pub descriptor: Vec, + pub grants: Vec>, + pub key_log: Option, + pub chains: Vec>>, + pub policy: Vec, +} + +fn hexes(v: &Value) -> Option>> { + v.as_array()?.iter().map(|x| unhex(x.as_str()?)).collect() +} + +fn has_exactly(o: &Map, keys: &[&str]) -> bool { + o.len() == keys.len() && keys.iter().all(|k| o.contains_key(*k)) +} + +impl RoomInput { + /// A room's object (§8.1), or `None` when it does not read. + pub fn from_json(v: &Value) -> Option { + let o = v.as_object()?; + if !has_exactly(o, &["descriptor", "grants", "key_log", "chains", "policy"]) { + return None; + } + let key_log = match &o["key_log"] { + Value::Null => None, + Value::String(s) => Some(s.clone()), + _ => return None, + }; + Some(RoomInput { + descriptor: unhex(o["descriptor"].as_str()?)?, + grants: hexes(&o["grants"])?, + key_log, + chains: o["chains"].as_array()?.iter().map(hexes).collect::>()?, + policy: unhex(o["policy"].as_str()?)?, + }) + } + + pub fn to_json(&self) -> Value { + json!({ + "descriptor": hex(&self.descriptor), + "grants": self.grants.iter().map(|g| hex(g)).collect::>(), + "key_log": self.key_log, + "chains": self.chains.iter().map(|c| c.iter().map(|e| hex(e)).collect::>()).collect::>(), + "policy": hex(&self.policy), + }) + } +} + +/// A room verified (§3.3): its id, `t0`, policy and schedule, and its events by digest. +#[derive(Clone, Debug)] +pub struct Room { + pub id: [u8; 32], + pub t0_us: u64, + pub policy: ads::AdPolicy, + pub schedule: ads::Schedule, + pub events: BTreeMap<[u8; 32], Event>, +} + +/// The place of a live-ads refusal, as the receipt ledger writes it. +fn ads_place(at: &ads::At) -> Value { + use room::At as R; + match at { + ads::At::Room(R::Descriptor) => json!(["room", "descriptor"]), + ads::At::Room(R::KeyLog) => json!(["room", "key_log"]), + ads::At::Room(R::Grant(i)) => json!(["room", "grant", i]), + ads::At::Room(R::Record { chain, record }) => json!(["room", "record", chain, record]), + ads::At::Policy => json!(["policy"]), + ads::At::Record { chain, record } => json!(["record", chain, record]), + ads::At::Receipt(k) => json!(["receipt", k]), + } +} + +/// §8.2 step 5 for one room: verified as live-ads §9.2 steps 1 to 3 verify one, no receipts. +pub fn verify_room(r: &RoomInput) -> Result { + let run = ads::verify(&ads::Input { + descriptor: &r.descriptor, + grants: &r.grants, + head: r.key_log.as_deref(), + chains: &r.chains, + policy: &r.policy, + pt0_ms: 0, + receipts: &[], + pods: &[], + claims: &[], + creatives: &[], + issuers: &[], + }); + match run.outcome() { + ads::Outcome::Accept => {} + ads::Outcome::Gap(_) => return Err((DELIVERY_GAP, json!([]))), + ads::Outcome::Refuse { at, reason } => return Err((reason.code(), ads_place(at))), + } + let mut ev = BTreeMap::new(); + for c in &r.chains { + for b in c { + let e = Event::parse(b).expect("an event the room accepted reads"); + ev.insert(e.digest(), e); + } + } + Ok(Room { id: run.room.expect("an accepted run names its room"), t0_us: run.t0_us, policy: run.policy.clone().expect("an accepted run holds its policy"), schedule: run.schedule.clone(), events: ev }) +} + +/// §8.2 steps 1 and 2: the contract in force, and none of the others overlapping it. +fn the_contract(contract: &[u8], contracts: &[Vec]) -> Result { + let c = Contract::parse(contract).map_err(|(r, at)| (r, place(json!(["contract"]), at)))?; + let d = c.digest(); + for (k, b) in contracts.iter().enumerate() { + let o = Contract::parse(b).map_err(|(r, at)| (r, place(json!(["contracts", k]), at)))?; + if o.digest() != d && o.overlaps(&c) { + return Err(("contract_overlap", json!(["contracts", k]))); + } + } + Ok(c) +} + +// --------------------------------------------------------------------------------------- +// §4, §5, §6 Breaks +// --------------------------------------------------------------------------------------- + +/// A break of a report (§4), attributed (§6). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Break { + pub class: u8, + /// A cued break: its room's index in the document, and its index in that room's schedule. + pub room: Option<(usize, usize)>, + /// A scheduled break: its run's index in the document, and its entry's. + pub taken: Option<(usize, usize)>, + pub opening: Option<[u8; 32]>, + pub closing: Option<[u8; 32]>, + pub start: u64, + pub duration_ms: u64, + pub open: bool, + pub slots: Option, + /// `section` or `policy`, when it has slots. + pub slot_source: Option<&'static str>, + /// The policy's digest, when its slots are the policy's. + pub policy: Option<[u8; 32]>, + pub section: Option, + /// Its run's identity, and its entry's index in that run's schedule. + pub run: [u8; 32], + pub entry: u64, + pub asset: Option, + pub split: u16, +} + +impl Break { + /// Every record a line citing it cites (§7.3). + pub fn cited(&self) -> Vec<(u8, [u8; 32])> { + let mut v: Vec<(u8, [u8; 32])> = self.opening.iter().chain(self.closing.iter()).map(|d| (record_kind::CUE, *d)).collect(); + v.extend(self.policy.map(|p| (record_kind::POLICY, p))); + v.push((record_kind::RUN, self.run)); + v + } +} + +/// What a report rests on (§8.2 steps 4 to 7). +#[derive(Clone, Debug)] +pub struct Evidence { + pub runs: Vec, + /// The runs' identities, in the document's order. + pub run_ids: Vec<[u8; 32]>, + pub rooms: Vec, + /// The breaks in the period, in the order of steps 6 and 7. + pub breaks: Vec, +} + +fn us(ms: u64) -> u128 { + ms as u128 * 1000 +} + +/// §8.2 step 5: the rooms. +fn the_rooms(c: &Contract, rooms: &[Value]) -> Result, Refusal> { + let mut out: Vec = Vec::with_capacity(rooms.len()); + for (k, v) in rooms.iter().enumerate() { + let at = || json!(["room", k]); + let input = RoomInput::from_json(v).ok_or((MALFORMED, at()))?; + let r = verify_room(&input).map_err(|(reason, w)| (reason, place(at(), w)))?; + if !c.carries(share::carrier::ROOM, &r.id) { + return Err((Reason::RoomForeign.code(), at())); + } + if out.iter().any(|o| o.id == r.id) { + return Err((Reason::RoomRepeated.code(), at())); + } + out.push(r); + } + Ok(out) +} + +/// §5.3: a section against its opening cue's summary. +fn agrees(st: §ion::States, cue: &events::Cue) -> bool { + if let Some((cancel, out)) = st.insert + && (cancel || !out) + { + return false; + } + if st.id.is_some_and(|id| id != cue.cue_id) { + return false; + } + st.duration.is_none_or(|d| d / 90 == cue.duration_ms as u64) +} + +/// §6: the run and entry of an instant, the asset and the split; `None` is `outside_window`. +fn placed(c: &Contract, runs: &[ChannelReceipt], ids: &[[u8; 32]], t: u64) -> Option, u16), &'static str>> { + let r = runs.iter().position(|r| us(r.window_start_ms) <= t as u128 && (t as u128) < us(r.window_end_ms))?; + let sched = &runs[r].schedule; + let e = sched.iter().position(|x| us(x.start_ms) <= t as u128 && (t as u128) < us(x.start_ms) + us(x.duration_ms))?; + Some(attributed(c, &runs[r], ids[r], e)) +} + +/// §6: the asset an entry gives, and its split. +fn attributed(c: &Contract, run: &ChannelReceipt, id: [u8; 32], e: usize) -> Result<([u8; 32], u64, Option, u16), &'static str> { + let asset = share::asset_at(&run.schedule, e, c.rule).map(str::to_string); + let split = match &asset { + Some(a) => c.split_of(a.as_bytes()).ok_or(ASSET_UNMAPPED)?, + None => c.between, + }; + Ok((id, e as u64, asset, split)) +} + +/// §8.2 steps 6 and 7: the breaks in the period, placed and attributed. +fn the_breaks(c: &Contract, start: u64, end: u64, runs: &[ChannelReceipt], ids: &[[u8; 32]], rooms: &[Room]) -> Result, Refusal> { + let mut out: Vec = Vec::new(); + // Every cued break's start, in the period or not, for §4.2's cued test. + let mut starts: Vec = Vec::new(); + for (k, room) in rooms.iter().enumerate() { + let policy_digest = room.policy.digest(); + let mut mine: Vec = Vec::new(); + for (i, b) in room.schedule.breaks.iter().enumerate() { + let at = || json!(["room", k, "break", i]); + let Some(s) = room.t0_us.checked_add(b.t) else { continue }; + starts.push(s); + if !(start <= s && s < end) { + continue; + } + let cue = match room.events.get(&b.digest).map(|e| events::parse_payload(kind::CUE, &e.payload, u16::MAX)) { + Some(Ok(Payload::Cue(q))) => q, + _ => unreachable!("a break's opening cue is a cue the room accepted"), + }; + let st = match &cue.scte { + Some((events::scte_form::SPLICE_INFO_SECTION, bytes)) => { + let st = section::read(bytes).map_err(|_| (Reason::CueSectionInvalid.code(), at()))?; + if !agrees(&st, &cue) { + return Err((Reason::CueSectionMismatch.code(), at())); + } + Some(st) + } + _ => None, + }; + let duration_ms = b.duration_ms.unwrap_or(b.avail_ms); + let span = |x: &Break| (x.start as u128, x.start as u128 + us(x.duration_ms)); + let mine_span = (s as u128, s as u128 + us(duration_ms)); + if out.iter().any(|o| { + let (a, z) = span(o); + a < mine_span.1 && mine_span.0 < z + }) { + return Err((BREAK_OVERLAP, at())); + } + let (run, entry, asset, split) = placed(c, runs, ids, s).ok_or((OUTSIDE_WINDOW, at()))?.map_err(|r| (r, at()))?; + let (slots, slot_source, policy) = match st.as_ref().and_then(|x| x.slots) { + Some(n) => (Some(n), Some("section"), None), + None => (Some(room.policy.slots as u64), Some("policy"), Some(policy_digest)), + }; + mine.push(Break { + class: class::CUED, + room: Some((k, i)), + taken: None, + opening: Some(b.digest), + closing: b.closed_by, + start: s, + duration_ms, + open: b.is_open(), + slots, + slot_source, + policy, + section: st, + run, + entry, + asset, + split, + }); + } + out.extend(mine); + } + // The scheduled breaks, the runs by their windows' starts. + let mut order: Vec = (0..runs.len()).collect(); + order.sort_by_key(|&k| runs[k].window_start_ms); + let mut seen: BTreeSet<(String, u64, u64)> = BTreeSet::new(); + for k in order { + let r = &runs[k]; + for &e in &r.aired { + let e = e as usize; + let x = &r.schedule[e]; + if x.kind != EntryKind::Ad || !seen.insert((x.asset_id.clone(), x.start_ms, x.duration_ms)) { + continue; + } + let s = us(x.start_ms); + let z = s + us(x.duration_ms); + if starts.iter().any(|&t| s <= t as u128 && (t as u128) < z) || !(start as u128 <= s && s < end as u128) { + continue; + } + let (run, entry, asset, split) = attributed(c, r, ids[k], e).map_err(|reason| (reason, json!(["run", k, "entry", e])))?; + out.push(Break { + class: class::SCHEDULED, + room: None, + taken: Some((k, e)), + opening: None, + closing: None, + start: s as u64, + duration_ms: x.duration_ms, + open: false, + slots: None, + slot_source: None, + policy: None, + section: None, + run, + entry, + asset, + split, + }); + } + } + Ok(out) +} + +/// §8.2 steps 4 to 7 for a contract and a period. +fn evidence(c: &Contract, start: u64, end: u64, runs: &[Value], rooms: &[Value]) -> Result { + let runs = share::the_runs(c, runs)?; + let run_ids: Vec<[u8; 32]> = runs.iter().map(ChannelReceipt::receipt_hash).collect(); + let rooms = the_rooms(c, rooms)?; + let breaks = the_breaks(c, start, end, &runs, &run_ids, &rooms)?; + Ok(Evidence { runs, run_ids, rooms, breaks }) +} + +// --------------------------------------------------------------------------------------- +// §7.2, §7.3 The lines +// --------------------------------------------------------------------------------------- + +#[derive(Clone, Debug, Default)] +struct Group { + breaks: u64, + break_ms: u128, + slots: u128, + unslotted: u64, + cited: Cited, +} + +fn groups(bs: &[Break]) -> BTreeMap<(u16, u8), Group> { + let mut g: BTreeMap<(u16, u8), Group> = BTreeMap::new(); + for b in bs { + let e = g.entry((b.split, b.class)).or_default(); + e.breaks += 1; + e.break_ms += b.duration_ms as u128; + match b.slots { + Some(n) => e.slots += n as u128, + None => e.unslotted += 1, + } + e.cited.extend(b.cited()); + } + g +} + +/// A figure that cannot be held in its field. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Overflow; + +/// A party's lines, each with its cited records, and the totals (§7.2, §7.3). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Computed { + pub lines: Vec<(Line, Cited)>, + pub breaks: u64, + pub break_ms: u64, + pub slots: u64, +} + +/// The party's share of a split, when it holds one. +fn share_of(c: &Contract, party: &[u8; 32], split: u16) -> Option { + c.parties(split).into_iter().find(|(k, _)| k == party).map(|(_, p)| p) +} + +/// The lines of `party`'s report over the breaks (§7.2, §7.3). +pub fn compute(c: &Contract, party: &[u8; 32], bs: &[Break]) -> Result { + let mut lines = Vec::new(); + let (mut breaks, mut break_ms, mut slots) = (0u64, 0u64, 0u64); + for ((split, cls), g) in groups(bs) { + let Some(ppm) = share_of(c, party, split) else { continue }; + let line = Line { + split, + class: cls, + breaks: g.breaks, + break_ms: u64::try_from(g.break_ms).map_err(|_| Overflow)?, + slots: u64::try_from(g.slots).map_err(|_| Overflow)?, + unslotted: g.unslotted, + ppm, + cited: u32::try_from(g.cited.len()).map_err(|_| Overflow)?, + cites: cites_digest(&g.cited), + }; + breaks = breaks.checked_add(line.breaks).ok_or(Overflow)?; + break_ms = break_ms.checked_add(line.break_ms).ok_or(Overflow)?; + slots = slots.checked_add(line.slots).ok_or(Overflow)?; + lines.push((line, g.cited)); + } + Ok(Computed { lines, breaks, break_ms, slots }) +} + +/// §8.2 step 8: the report's figures against those recomputed. +fn check_figures(rep: &Report, c: &Contract, ev: &Evidence) -> Result { + let g = groups(&ev.breaks); + for l in &rep.lines { + let holds = share_of(c, &rep.party, l.split).is_some(); + match g.get(&l.key()) { + Some(x) if holds && l.breaks <= x.breaks => {} + _ => return Err(LINE_UNSOURCED), + } + } + let mismatch = Reason::OpportunityReportMismatch.code(); + let computed = compute(c, &rep.party, &ev.breaks).map_err(|_| mismatch)?; + for l in &rep.lines { + let Some((x, _)) = computed.lines.iter().find(|(x, _)| x.key() == l.key()) else { + return Err(LINE_UNSOURCED); + }; + if l.cited != x.cited || l.cites != x.cites { + return Err(CITATION_MISMATCH); + } + } + let mut rooms: Vec<[u8; 32]> = ev.rooms.iter().map(|r| r.id).collect(); + rooms.sort(); + let mut runs = ev.run_ids.clone(); + runs.sort(); + let lines: Vec = computed.lines.iter().map(|(l, _)| *l).collect(); + if rep.rooms != rooms || rep.runs != runs || rep.lines != lines || (rep.breaks, rep.break_ms, rep.slots) != (computed.breaks, computed.break_ms, computed.slots) { + return Err(mismatch); + } + Ok(computed) +} + +// --------------------------------------------------------------------------------------- +// §8 Verifying a report +// --------------------------------------------------------------------------------------- + +/// What a report's verification document holds (§8.1), its members read. +struct Parts<'a> { + contract: Vec, + contracts: Vec>, + report: Option>, + runs: &'a [Value], + rooms: &'a [Value], +} + +fn parts(doc: &Value, with_report: bool) -> Result, Refusal> { + let bad = || (MALFORMED, json!([])); + let o = doc.as_object().ok_or_else(bad)?; + let want: &[&str] = if with_report { &["contract", "contracts", "report", "runs", "rooms"] } else { &["contract", "contracts", "runs", "rooms"] }; + if !has_exactly(o, want) { + return Err(bad()); + } + let bytes = |v: &Value| v.as_str().and_then(unhex); + Ok(Parts { + contract: bytes(&o["contract"]).ok_or_else(bad)?, + contracts: o["contracts"].as_array().ok_or_else(bad)?.iter().map(bytes).collect::>>().ok_or_else(bad)?, + report: if with_report { Some(bytes(&o["report"]).ok_or_else(bad)?) } else { None }, + runs: o["runs"].as_array().ok_or_else(bad)?, + rooms: o["rooms"].as_array().ok_or_else(bad)?, + }) +} + +/// An accepted report (§8.3). +#[derive(Clone, Debug)] +pub struct Accepted { + pub digest: [u8; 32], + pub contract: Contract, + pub report: Report, + pub evidence: Evidence, + pub computed: Computed, +} + +impl Accepted { + /// The breaks counted in a line of the party's (§10.3). + pub fn counted(&self) -> impl Iterator { + self.evidence.breaks.iter().filter(|b| share_of(&self.contract, &self.report.party, b.split).is_some()) + } +} + +/// §8.2: verify a report's verification document (§8.1). +pub fn verify(doc: &Value) -> Result { + let p = parts(doc, true)?; + let c = the_contract(&p.contract, &p.contracts)?; + let at = || json!(["report"]); + let rep = Report::read(p.report.as_deref().expect("with its report")).map_err(|r| (r, at()))?; + if rep.contract != c.digest() || !c.is_party(&rep.party) { + return Err((CONTRACT_MISMATCH, at())); + } + if !(c.start <= rep.start && rep.end <= c.end) { + return Err((OUTSIDE_CONTRACT, at())); + } + check_sig(&rep.issuer, REPORT_DOMAIN, &rep.body(), &rep.sig).map_err(|r| (r, at()))?; + let ev = evidence(&c, rep.start, rep.end, p.runs, p.rooms)?; + let computed = check_figures(&rep, &c, &ev).map_err(|r| (r, at()))?; + Ok(Accepted { digest: rep.digest(), contract: c, report: rep, evidence: ev, computed }) +} + +/// Compute and sign the report of `party` for `[start, end)` over a verification document +/// without its report (§8.1), and return it with the full document, verified. +pub fn create(inputs: &Value, issuer: &SigningKey, party: [u8; 32], start: u64, end: u64) -> Result<(Report, Value), Refusal> { + let p = parts(inputs, false)?; + let c = the_contract(&p.contract, &p.contracts)?; + let at = || json!(["report"]); + if !c.is_party(&party) { + return Err((CONTRACT_MISMATCH, at())); + } + if start >= end { + return Err((Reason::OpportunityReportInvalid.code(), at())); + } + if !(c.start <= start && end <= c.end) { + return Err((OUTSIDE_CONTRACT, at())); + } + let ev = evidence(&c, start, end, p.runs, p.rooms)?; + let computed = compute(&c, &party, &ev.breaks).map_err(|_| (Reason::OpportunityReportMismatch.code(), at()))?; + let mut rooms: Vec<[u8; 32]> = ev.rooms.iter().map(|r| r.id).collect(); + rooms.sort(); + let mut runs = ev.run_ids.clone(); + runs.sort(); + let rep = Report { + issuer: [0; 32], + contract: c.digest(), + party, + start, + end, + rooms, + runs, + lines: computed.lines.iter().map(|(l, _)| *l).collect(), + breaks: computed.breaks, + break_ms: computed.break_ms, + slots: computed.slots, + sig: [0; 64], + } + .sign(issuer); + let mut doc = inputs.clone(); + doc["report"] = json!(hex(&rep.to_bytes())); + verify(&doc)?; + Ok((rep, doc)) +} + +fn records_json(r: &[(u8, [u8; 32])]) -> Value { + Value::Array(r.iter().map(|(k, id)| json!([k, hex(id)])).collect()) +} + +fn section_json(s: &Option) -> Value { + match s { + None => Value::Null, + Some(s) => json!({"command": s.command_name(), "id": s.id, "duration": s.duration, "slots": s.slots}), + } +} + +/// What §8.3 states of an accepted report. +pub fn accepted_json(a: &Accepted) -> Value { + let r = &a.report; + let breaks: Vec = a + .evidence + .breaks + .iter() + .map(|b| { + json!({ + "class": class::name(b.class), + "room": b.room.map(|x| x.0), "index": b.room.map(|x| x.1), + "taken": b.taken.map(|(k, e)| json!({"run": k, "entry": e})), + "cue": b.opening.map(|d| hex(&d)), + "start": b.start, "duration_ms": b.duration_ms, "open": b.open, + "slots": b.slots, "slots_from": b.slot_source, + "section": section_json(&b.section), + "run": hex(&b.run), "entry": b.entry, "asset": b.asset, "split": b.split, + "cites": records_json(&b.cited().into_iter().collect::>().into_iter().collect::>()), + }) + }) + .collect(); + let lines: Vec = a + .computed + .lines + .iter() + .map(|(l, cited)| { + json!({ + "split": l.split, "class": class::name(l.class), "breaks": l.breaks, "break_ms": l.break_ms, "slots": l.slots, + "unslotted": l.unslotted, "ppm": l.ppm, "cites": records_json(&cited.iter().copied().collect::>()), + }) + }) + .collect(); + json!({ + "outcome": "accept", + "report": hex(&a.digest), + "contract": hex(&r.contract), + "issuer": hex(&r.issuer), + "party": hex(&r.party), + "role": if r.party == a.contract.operator { "operator" } else { "owner" }, + "start": r.start, + "end": r.end, + "rooms": r.rooms.iter().map(|d| hex(d)).collect::>(), + "runs": r.runs.iter().map(|d| hex(d)).collect::>(), + "breaks": breaks, + "lines": lines, + "totals": {"breaks": r.breaks, "break_ms": r.break_ms, "slots": r.slots}, + }) +} + +// --------------------------------------------------------------------------------------- +// §10 The statement check +// --------------------------------------------------------------------------------------- + +/// A check's line (§10.2). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CheckLine { + pub scope: u8, + pub key: Vec, + pub breaks: u64, + pub break_ms: u64, + pub slots: u64, + pub unslotted: u64, + pub impressions: u64, + pub revenue: u64, + pub fill_ppm: u64, + pub revenue_per_slot: u64, + /// (`signed_impressions`, `signed_revenue`). + pub signed: Option<(u64, u64)>, + pub flags: u8, +} + +/// A statement check (§10.1). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Check { + pub issuer: [u8; 32], + pub report: [u8; 32], + pub reported: [u8; 32], + pub form: u8, + pub statement: [u8; 32], + pub floor_ppm: u32, + pub ceiling_ppm: u32, + pub lines: Vec, + pub sig: [u8; 64], +} + +impl Check { + pub fn body(&self) -> Vec { + let mut o = Vec::with_capacity(200 + 90 * self.lines.len()); + o.extend_from_slice(&CHECK_MAGIC); + o.push(VERSION); + o.extend_from_slice(&self.issuer); + o.extend_from_slice(&self.report); + o.extend_from_slice(&self.reported); + o.push(self.form); + o.extend_from_slice(&self.statement); + o.extend_from_slice(&self.floor_ppm.to_le_bytes()); + o.extend_from_slice(&self.ceiling_ppm.to_le_bytes()); + o.extend_from_slice(&u16::try_from(self.lines.len()).expect("at most 65535 lines").to_le_bytes()); + for l in &self.lines { + o.push(l.scope); + o.extend_from_slice(&u16::try_from(l.key.len()).expect("a key of at most 65535 bytes").to_le_bytes()); + o.extend_from_slice(&l.key); + for v in [l.breaks, l.break_ms, l.slots, l.unslotted, l.impressions, l.revenue, l.fill_ppm, l.revenue_per_slot] { + o.extend_from_slice(&v.to_le_bytes()); + } + match l.signed { + Some((i, r)) => { + o.push(1); + o.extend_from_slice(&i.to_le_bytes()); + o.extend_from_slice(&r.to_le_bytes()); + } + None => o.push(0), + } + o.push(l.flags); + } + o + } + + pub fn to_bytes(&self) -> Vec { + let mut o = self.body(); + o.extend_from_slice(&self.sig); + o + } + + /// Its digest (§10.5). + pub fn digest(&self) -> [u8; 32] { + sha256(&[&self.body()]) + } + + /// Signed by its issuer (§10.5), whose key it names. + pub fn sign(mut self, issuer: &SigningKey) -> Check { + self.issuer = issuer.verifying_key().to_bytes(); + self.sig = sign(issuer, CHECK_DOMAIN, &self.body()); + self + } + + /// Read a check and hold it to its rules (§10.6). + pub fn read(b: &[u8]) -> Result { + read_head(b, &CHECK_MAGIC)?; + let mut r = Rd { b, p: 5 }; + let (issuer, report, reported) = (r.arr()?, r.arr()?, r.arr()?); + let form_ = r.u8()?; + let statement = r.arr()?; + let (floor_ppm, ceiling_ppm) = (r.u32()?, r.u32()?); + let n = r.u16()? as usize; + let mut lines = Vec::with_capacity(n); + for _ in 0..n { + let scope = r.u8()?; + let key = r.text()?; + let mut f = [0u64; 8]; + for x in &mut f { + *x = r.u64()?; + } + let signed = match r.u8()? { + 0 => None, + 1 => Some((r.u64()?, r.u64()?)), + _ => return Err(Reason::StatementCheckInvalid.code()), + }; + let flags = r.u8()?; + let [breaks, break_ms, slots, unslotted, impressions, revenue, fill_ppm, revenue_per_slot] = f; + lines.push(CheckLine { scope, key, breaks, break_ms, slots, unslotted, impressions, revenue, fill_ppm, revenue_per_slot, signed, flags }); + } + let sig = r.arr()?; + if !r.done() { + return Err(MALFORMED); + } + let ch = Check { issuer, report, reported, form: form_, statement, floor_ppm, ceiling_ppm, lines, sig }; + if !ch.rules_hold() { + return Err(Reason::StatementCheckInvalid.code()); + } + Ok(ch) + } + + fn rules_hold(&self) -> bool { + let keys: Vec<(u8, &[u8])> = self.lines.iter().map(|l| (l.scope, l.key.as_slice())).collect(); + let key_ok = |l: &CheckLine| match l.scope { + scope::PERIOD => l.key.is_empty(), + scope::DAY => std::str::from_utf8(&l.key).ok().and_then(reported::day_start).is_some(), + scope::PROGRAMME => (1..=1024).contains(&l.key.len()) && std::str::from_utf8(&l.key).is_ok(), + _ => false, + }; + self.issuer != [0; 32] + && (form::JSON..=form::CSV).contains(&self.form) + && self.floor_ppm <= WHOLE + && self.ceiling_ppm >= WHOLE + && self.lines.first().is_some_and(|l| l.scope == scope::PERIOD) + && self.lines.iter().filter(|l| l.scope == scope::PERIOD).count() == 1 + && ascending(&keys) + && self.lines.iter().all(|l| key_ok(l) && (l.signed.is_none() || l.scope == scope::PERIOD) && l.flags < 0x80) + } +} + +/// ⌊a × m / b⌋ in integers wide enough, held at 2^64 − 1; 0 when `b` is 0 (§10.3). +fn ratio(a: u64, m: u128, b: u64) -> u64 { + if b == 0 { + return 0; + } + u64::try_from(a as u128 * m / b as u128).unwrap_or(u64::MAX) +} + +/// The opportunities of a scope: count, `break_ms`, slots, unslotted (§10.3). +fn tally<'a>(bs: impl Iterator) -> (u64, u64, u64, u64) { + let (mut n, mut ms, mut slots, mut un) = (0u64, 0u128, 0u128, 0u64); + for b in bs { + n += 1; + ms += b.duration_ms as u128; + match b.slots { + Some(s) => slots += s as u128, + None => un += 1, + } + } + let hold = |x: u128| u64::try_from(x).unwrap_or(u64::MAX); + (n, hold(ms), hold(slots), un) +} + +/// The signed figures beside a check (§10.4): (`signed_impressions`, `signed_revenue`). +pub fn signed_figures(s: &share::Accepted, basis_: u8) -> (u64, u64) { + let sum = |f: fn(&share::Line) -> u64| s.statement.lines.iter().fold(0u64, |a, l| a.saturating_add(f(l))); + let impressions = sum(|l| l.impressions); + let revenue = if basis_ == basis::GROSS { sum(|l| l.revenue) } else { s.statement.total }; + (impressions, revenue) +} + +/// A check's lines (§10.2 to §10.4) for a report, a reported statement, the signed figures when +/// given, and the bounds. +pub fn check_lines(a: &Accepted, r: &Reported, signed: Option<(u64, u64)>, floor_ppm: u32, ceiling_ppm: u32) -> Vec { + let counted: Vec<&Break> = a.counted().collect(); + let line = |scope_: u8, key: Vec, t: (u64, u64, u64, u64), impressions: u64, revenue: u64, signed: Option<(u64, u64)>| { + let (breaks, break_ms, slots, unslotted) = t; + CheckLine { + scope: scope_, + key, + breaks, + break_ms, + slots, + unslotted, + impressions, + revenue, + fill_ppm: ratio(impressions, WHOLE as u128, slots), + revenue_per_slot: ratio(revenue, 1, slots), + signed, + flags: 0, + } + }; + let mut lines = vec![line(scope::PERIOD, Vec::new(), tally(counted.iter().copied()), r.impressions, r.revenue, signed)]; + for d in &r.days { + let end = d.start + reported::DAY_US; + lines.push(line(scope::DAY, d.day.as_bytes().to_vec(), tally(counted.iter().copied().filter(|b| d.start <= b.start && b.start < end)), d.impressions, d.revenue, None)); + } + for p in &r.programmes { + lines.push(line(scope::PROGRAMME, p.asset.clone(), tally(counted.iter().copied().filter(|b| b.asset.as_deref().map(str::as_bytes) == Some(p.asset.as_slice()))), p.impressions, p.revenue, None)); + } + let (rate, period_slots) = (lines[0].fill_ppm as u128, lines[0].slots); + for l in &mut lines { + let mut f = 0u8; + if l.impressions > 0 && l.breaks == 0 { + f |= 1 << 0; + } + if l.scope != scope::PERIOD && l.slots > 0 && period_slots > 0 { + let fill = l.fill_ppm as u128 * WHOLE as u128; + if fill > rate * ceiling_ppm as u128 { + f |= 1 << 1; + } + if fill < rate * floor_ppm as u128 { + f |= 1 << 2; + } + } + if let Some((si, sr)) = l.signed { + if l.impressions > si { + f |= 1 << 3; + } + if l.impressions < si { + f |= 1 << 4; + } + if l.revenue > sr { + f |= 1 << 5; + } + if l.revenue < sr { + f |= 1 << 6; + } + } + l.flags = f; + } + lines +} + +// --------------------------------------------------------------------------------------- +// §11 Verifying a check +// --------------------------------------------------------------------------------------- + +/// An accepted check (§11.3). +#[derive(Clone, Debug)] +pub struct CheckAccepted { + pub digest: [u8; 32], + pub check: Check, + pub report: Accepted, + pub reported: Reported, + pub statement: Option, +} + +/// What a check's document holds after steps 1 to 3 (§11.2). +struct Held { + report: Accepted, + reported: Reported, + reported_digest: [u8; 32], + form: u8, + statement: Option, +} + +fn held(doc: &Value, with_check: bool) -> Result<(Held, Option>), Refusal> { + let bad = || (MALFORMED, json!([])); + let o = doc.as_object().ok_or_else(bad)?; + let want: &[&str] = if with_check { &["report", "reported", "statement", "check"] } else { &["report", "reported", "statement"] }; + if !has_exactly(o, want) { + return Err(bad()); + } + let rp = o["reported"].as_object().filter(|r| has_exactly(r, &["form", "text"])).ok_or_else(bad)?; + let form_ = rp["form"].as_str().and_then(form::code).ok_or_else(bad)?; + let text = rp["text"].as_str().ok_or_else(bad)?; + let check = if with_check { Some(o["check"].as_str().and_then(unhex).ok_or_else(bad)?) } else { None }; + if !(o["statement"].is_null() || o["statement"].is_object()) { + return Err(bad()); + } + // Step 1. + let report = verify(&o["report"]).map_err(|(r, at)| (r, place(json!(["report"]), at)))?; + // Step 2. + let at = || json!(["reported"]); + let reported = reported::read(form_, text).ok_or((Reason::ReportedInvalid.code(), at()))?; + let c = &report.contract; + if reported.channel != c.channel || reported.currency != c.currency || (reported.start, reported.end) != (report.report.start, report.report.end) { + return Err((Reason::ReportedForeign.code(), at())); + } + // Step 3. + let statement = match &o["statement"] { + Value::Null => None, + s => { + let a = share::verify(s).map_err(|(r, at)| (r, place(json!(["statement"]), at)))?; + let st = &a.statement; + if st.contract != report.report.contract || st.party != report.report.party || (st.start, st.end) != (report.report.start, report.report.end) { + return Err((Reason::StatementForeign.code(), json!(["statement"]))); + } + Some(a) + } + }; + Ok((Held { report, reported, reported_digest: sha256(&[text.as_bytes()]), form: form_, statement }, check)) +} + +/// §11.2: verify a check's verification document (§11.1). +pub fn verify_check(doc: &Value) -> Result { + let (h, check) = held(doc, true)?; + let at = || json!(["check"]); + let ch = Check::read(&check.expect("with its check")).map_err(|r| (r, at()))?; + check_sig(&ch.issuer, CHECK_DOMAIN, &ch.body(), &ch.sig).map_err(|r| (r, at()))?; + let signed = h.statement.as_ref().map(|s| signed_figures(s, h.reported.basis)); + let statement = h.statement.as_ref().map_or([0; 32], |s| s.digest); + let lines = check_lines(&h.report, &h.reported, signed, ch.floor_ppm, ch.ceiling_ppm); + if ch.report != h.report.digest || ch.reported != h.reported_digest || ch.form != h.form || ch.statement != statement || ch.lines != lines { + return Err((Reason::StatementCheckMismatch.code(), at())); + } + Ok(CheckAccepted { digest: ch.digest(), check: ch, report: h.report, reported: h.reported, statement: h.statement }) +} + +/// Compute and sign the check of a document without its check (§11.1), at the bounds given, and +/// return it with the full document, verified. +pub fn create_check(inputs: &Value, issuer: &SigningKey, floor_ppm: u32, ceiling_ppm: u32) -> Result<(Check, Value), Refusal> { + let (h, _) = held(inputs, false)?; + if floor_ppm > WHOLE || ceiling_ppm < WHOLE { + return Err((Reason::StatementCheckInvalid.code(), json!(["check"]))); + } + let signed = h.statement.as_ref().map(|s| signed_figures(s, h.reported.basis)); + let ch = Check { + issuer: [0; 32], + report: h.report.digest, + reported: h.reported_digest, + form: h.form, + statement: h.statement.as_ref().map_or([0; 32], |s| s.digest), + floor_ppm, + ceiling_ppm, + lines: check_lines(&h.report, &h.reported, signed, floor_ppm, ceiling_ppm), + sig: [0; 64], + } + .sign(issuer); + let mut doc = inputs.clone(); + doc["check"] = json!(hex(&ch.to_bytes())); + verify_check(&doc)?; + Ok((ch, doc)) +} + +/// The names of a line's flags. +pub fn flag_names(f: u8) -> Vec<&'static str> { + FLAGS.iter().enumerate().filter(|(i, _)| f & (1 << i) != 0).map(|(_, n)| *n).collect() +} + +/// What §11.3 states of an accepted check. +pub fn check_json(a: &CheckAccepted) -> Value { + let c = &a.check; + let lines: Vec = c + .lines + .iter() + .map(|l| { + json!({ + "scope": scope::name(l.scope), "key": String::from_utf8_lossy(&l.key), + "breaks": l.breaks, "break_ms": l.break_ms, "slots": l.slots, "unslotted": l.unslotted, + "impressions": l.impressions, "revenue": l.revenue, "fill_ppm": l.fill_ppm, "revenue_per_slot": l.revenue_per_slot, + "signed": l.signed.map(|(i, r)| json!({"impressions": i, "revenue": r})), + "flags": flag_names(l.flags), + }) + }) + .collect(); + json!({ + "outcome": "accept", + "check": hex(&a.digest), + "issuer": hex(&c.issuer), + "report": hex(&c.report), + "party": hex(&a.report.report.party), + "start": a.report.report.start, + "end": a.report.report.end, + "reported": hex(&c.reported), + "form": form::name(c.form), + "basis": basis::name(a.reported.basis), + "statement": a.statement.as_ref().map(|s| hex(&s.digest)), + "floor_ppm": c.floor_ppm, + "ceiling_ppm": c.ceiling_ppm, + "lines": lines, + }) +} + +/// A report's verification's outcome as the corpus states it. +pub fn outcome_json(r: &Result) -> Value { + match r { + Ok(a) => accepted_json(a), + Err((reason, at)) => json!({"outcome": "refuse", "reason": reason, "at": at}), + } +} + +/// A check's verification's outcome as the corpus states it. +pub fn check_outcome_json(r: &Result) -> Value { + match r { + Ok(a) => check_json(a), + Err((reason, at)) => json!({"outcome": "refuse", "reason": reason, "at": at}), + } +} + +/// Whether a document is a check's (§11.1) rather than a report's (§8.1): it has `reported`. +pub fn is_check_document(doc: &Value) -> bool { + doc.get("reported").is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ratios_round_down_and_hold() { + assert_eq!(ratio(14, WHOLE as u128, 12), 1_166_666); + assert_eq!(ratio(5, WHOLE as u128, 0), 0); + assert_eq!(ratio(u64::MAX, WHOLE as u128, 1), u64::MAX); + assert_eq!(ratio(7, 1, 2), 3); + } + + #[test] + fn flags_by_name() { + assert_eq!(flag_names(0b101_0101), vec!["unproved", "below", "impressions_below_signed", "revenue_below_signed"]); + assert!(flag_names(0).is_empty()); + } + + fn report() -> Report { + Report { + issuer: [1; 32], + contract: [2; 32], + party: [3; 32], + start: 10, + end: 20, + rooms: vec![[4; 32]], + runs: vec![[5; 32], [6; 32]], + lines: vec![Line { split: 0, class: 1, breaks: 2, break_ms: 9, slots: 4, unslotted: 0, ppm: 500_000, cited: 3, cites: [7; 32] }], + breaks: 2, + break_ms: 9, + slots: 4, + sig: [8; 64], + } + } + + #[test] + fn a_report_reads_back_and_keeps_its_rules() { + let r = report(); + assert_eq!(Report::read(&r.to_bytes()), Ok(r.clone())); + let b = r.to_bytes(); + assert_eq!(Report::read(&b[..3]), Err(TRUNCATED)); + assert_eq!(Report::read(&[b"WOR2".as_slice(), &b[4..]].concat()), Err(MALFORMED)); + assert_eq!(Report::read(&[&b[..4], &[2u8][..], &b[5..]].concat()), Err(UNKNOWN_VERSION)); + assert_eq!(Report::read(&[b.as_slice(), &[0u8][..]].concat()), Err(MALFORMED)); + for bad in [ + Report { end: 10, ..r.clone() }, + Report { party: [0; 32], ..r.clone() }, + Report { runs: vec![[6; 32], [5; 32]], ..r.clone() }, + Report { lines: vec![Line { class: 3, ..r.lines[0] }], ..r.clone() }, + Report { lines: vec![Line { unslotted: 3, ..r.lines[0] }], ..r.clone() }, + Report { lines: vec![Line { ppm: 0, ..r.lines[0] }], ..r.clone() }, + ] { + assert_eq!(Report::read(&bad.to_bytes()), Err(Reason::OpportunityReportInvalid.code())); + } + } + + #[test] + fn a_check_reads_back_and_keeps_its_rules() { + let line = |scope_: u8, key: &str| CheckLine { + scope: scope_, + key: key.as_bytes().to_vec(), + breaks: 1, + break_ms: 2, + slots: 3, + unslotted: 0, + impressions: 4, + revenue: 5, + fill_ppm: 6, + revenue_per_slot: 7, + signed: None, + flags: 0, + }; + let c = Check { + issuer: [1; 32], + report: [2; 32], + reported: [3; 32], + form: form::CSV, + statement: [0; 32], + floor_ppm: FLOOR_PPM, + ceiling_ppm: CEILING_PPM, + lines: vec![CheckLine { signed: Some((1, 2)), ..line(scope::PERIOD, "") }, line(scope::DAY, "2026-10-16"), line(scope::PROGRAMME, "a/ep-1")], + sig: [9; 64], + }; + assert_eq!(Check::read(&c.to_bytes()), Ok(c.clone())); + let invalid = Reason::StatementCheckInvalid.code(); + for bad in [ + Check { form: 3, ..c.clone() }, + Check { floor_ppm: WHOLE + 1, ..c.clone() }, + Check { ceiling_ppm: WHOLE - 1, ..c.clone() }, + Check { lines: c.lines[1..].to_vec(), ..c.clone() }, + Check { lines: vec![c.lines[0].clone(), c.lines[2].clone(), c.lines[1].clone()], ..c.clone() }, + Check { lines: vec![c.lines[0].clone(), line(scope::DAY, "2026-02-30")], ..c.clone() }, + Check { lines: vec![c.lines[0].clone(), CheckLine { signed: Some((0, 0)), ..line(scope::DAY, "2026-10-16") }], ..c.clone() }, + Check { lines: vec![CheckLine { flags: 0x80, ..c.lines[0].clone() }], ..c.clone() }, + ] { + assert_eq!(Check::read(&bad.to_bytes()), Err(invalid)); + } + } +} diff --git a/wai/wai-rs/src/opportunity_report/corpus.rs b/wai/wai-rs/src/opportunity_report/corpus.rs new file mode 100644 index 000000000..37fde2c55 --- /dev/null +++ b/wai/wai-rs/src/opportunity_report/corpus.rs @@ -0,0 +1,835 @@ +//! The opportunity-report corpus (`live-conformance/opportunity/`, +//! extensions/opportunity-report.md §14). +//! +//! Everything is written here from fixed seeds: every key is SHA-256 of this corpus's label, every +//! time a constant, and Ed25519 signing is deterministic (RFC 8032), so the file is the same bytes +//! on every machine. The world is a channel's day, 2026-10-16, under a contract of two owners: two +//! windows of its program-as-run, a room whose producer cues breaks with SCTE 35 sections inside +//! the owners' programmes, a ledger document billing the breaks' impressions and the operator's +//! signed statements of the day. [`generate`] writes the corpus; [`verify`] checks every vector +//! against the outcome it states. + +use std::collections::BTreeMap; + +use ed25519_dalek::{Signer, SigningKey}; +use serde_json::{Map, Value, json}; + +use super::section::{Command, SegmentationSpec, write as section}; +use super::*; +use crate::live_ads::{AdPolicy, NO_SKIP_CONTROL}; +use crate::live_events::{Cue, Payload, Policy as EventPolicy, cue_type, scte_form}; +use crate::live_room::{Caps, Descriptor, Detail, Grant, Limits, NO_CEILING, Recording, Review, Role}; +use crate::quantum_ops::GrantRef; +use crate::receipt_ledger::{self as ledger, Tariff}; +use crate::revenue_share::{Split, Statement, rule}; +use crate::wai_video_channel::ScheduleEntry; +use crate::wai_video_ssai::{AdCandidate, BreakBinding, SsaiBreakReceipt, asset_format, decide, pod_hash}; + +/// 2026-10-01 and 2026-11-01 (00:00:00Z): the contract's period. +const OCT: u64 = 1_790_812_800_000_000; +const NOV: u64 = 1_793_491_200_000_000; +/// 2026-10-16T00:00:00Z: the room's `t0` and the day every report states. +pub const DAY16: u64 = 1_792_108_800_000_000; +pub const DAY_US: u64 = 86_400_000_000; +pub const DAY17: u64 = DAY16 + DAY_US; +const T0_S: u64 = DAY16 / 1_000_000; +const S: u64 = 1_000_000; +pub const CHANNEL: &str = "fast-9"; +/// Per presented and per billable impression: 1.5 and 1 minor unit, in millionths. +const PRICES: [u64; 2] = [1_500_000, 1_000_000]; +/// The decider grant's ceiling, in minor units of EUR. +const BUDGET_MINOR: u64 = 10; +/// Sessions per break. +const SESSIONS: u8 = 3; + +/// A key of the corpus, from its fixed seed: `operator`, `owner-a`, `owner-b`, `channel`, `buyer`, +/// `auditor`, `stranger`, and the room's `authority`, `clock`, `producer` and `decider`. Test +/// keys, never a deployment's. +pub fn key(label: &str) -> SigningKey { + SigningKey::from_bytes(&sha256(&[b"wai:opportunity-corpus\x01", label.as_bytes()])) +} + +pub fn pk(label: &str) -> [u8; 32] { + key(label).verifying_key().to_bytes() +} + +// ---- the room ---- + +fn bits(ks: &[u8]) -> [u8; 32] { + let mut b = [0u8; 32]; + for &k in ks { + b[(k / 8) as usize] |= 1 << (k % 8); + } + b +} + +fn role(name: &str, subscribe: bool, events: &[u8]) -> Role { + Role { name: name.into(), caps: Caps { publish: 0, subscribe, max_layer: subscribe as u8, powers: 0 }, events: bits(events) } +} + +/// A principal's chain being written. +#[derive(Clone)] +struct Chain { + label: &'static str, + seq: u64, + prev: [u8; 32], + records: Vec>, +} + +/// A room being written: the clock's chain, then the producer's. +#[derive(Clone)] +pub struct RoomBuild { + pub descriptor: Vec, + pub id: [u8; 32], + pub grants: Vec>, + decider: Grant, + chains: Vec, + pub policy: AdPolicy, +} + +impl RoomBuild { + /// A room whose descriptor admits `max_present` (so that two rooms have two ids): a producer + /// that cues breaks, and a decider with the room's ad budget that never joins. + fn new(max_present: u16) -> RoomBuild { + let d = Descriptor { + authority: pk("authority"), + clock: pk("clock"), + t0_us: DAY16, + tick_us: S, + origin: String::new(), + limits: Limits { max_present: max_present.into(), max_publishers: 4, reval_s: 0, ceiling: EventPolicy::default() }, + roles: vec![role("producer", true, &[kind::META, kind::CUE, kind::CHAT]), role("decider", false, &[])], + slots: vec![], + profiles: vec![], + review: Review { kinds: [0; 32], deadline_ms: 0, fallback: 0 }, + recording: Recording { mode: 0, retention_days: 0, commit_interval_ms: 0, jitter_ms: 0, regions: vec![] }, + sig: [0; 64], + } + .sign(&key("authority")); + let id = d.room_id(); + let grant = |who: &str, r: usize, funds: Option| { + let role = &d.roles[r]; + let kinds: Vec = (0..=255u8).filter(|&k| role.events[(k / 8) as usize] & (1 << (k % 8)) != 0).collect(); + Grant { + room: id, + principal: pk(who), + prev: [0; 32], + effective: 0, + nbf: T0_S - 3_600, + exp: T0_S + 86_400, + role: r as u8, + caps: role.caps, + precedence: 0, + once: [0; 16], + viewer: [0; 16], + policy: EventPolicy::with_kinds(&kinds), + joules_ceiling: NO_CEILING, + funds_ceiling: funds.unwrap_or(NO_CEILING), + currency: if funds.is_some() { *b"EUR" } else { [0; 3] }, + profiles: vec![], + origins: vec![], + attrs: vec![], + sig: [0; 64], + } + .sign(&key("authority")) + }; + let producer = grant("producer", 0, None); + let decider = grant("decider", 1, Some(BUDGET_MINOR)); + let policy = AdPolicy { + room: id, + decider_grant: decider.digest(), + slots: 2, + open_avail_ms: 30_000, + preload_ms: 0, + restrict: 0, + skip_after_s: NO_SKIP_CONTROL, + snap: 0, + asset_base: "https://ads.channel.example/sgai/".into(), + creative_base: "https://cdn.channel.example/creatives/".into(), + presentation: None, + sig: [0; 64], + } + .sign(&key("authority")); + let mut room = RoomBuild { + descriptor: d.to_bytes(), + id, + grants: vec![producer.to_bytes(), decider.to_bytes()], + decider, + chains: vec![Chain { label: "clock", seq: 0, prev: [0; 32], records: vec![] }, Chain { label: "producer", seq: 0, prev: [0; 32], records: vec![] }], + policy, + }; + let j = Detail::Join { grant: producer.digest() }; + room.raw(0, 0, j.kind(), Payload::Lifecycle { kind: j.kind(), subject: pk("producer"), detail: j.encode() }.encode()); + room + } + + fn raw(&mut self, chain: usize, t: u64, kind_: u8, payload: Vec) -> [u8; 32] { + let c = &mut self.chains[chain]; + let e = Event::sign(&key(c.label), self.id, c.seq, c.prev, t, None, kind_, payload); + c.seq += 1; + c.prev = e.digest(); + c.records.push(e.to_bytes()); + c.prev + } + + /// The producer's cue at `t` seconds of the room clock, with its section when it has one. + fn cue(&mut self, t_s: u64, ctype: u8, cue_id: u32, duration_ms: u32, scte: Option>) -> [u8; 32] { + let c = Payload::Cue(Cue { cue_type: ctype, cue_id, duration_ms, scte: scte.map(|s| (scte_form::SPLICE_INFO_SECTION, s)) }); + self.raw(1, t_s * S, kind::CUE, c.encode()) + } + + pub fn chains(&self) -> Vec>> { + self.chains.iter().map(|c| c.records.clone()).collect() + } + + pub fn input(&self) -> RoomInput { + RoomInput { descriptor: self.descriptor.clone(), grants: self.grants.clone(), key_log: None, chains: self.chains(), policy: self.policy.to_bytes() } + } +} + +/// The program clock's `pts_time` of an instant of `t` seconds (cue-injection §5.2). +fn pts(t_s: u64) -> u64 { + (t_s * S * 9 / 100) % (1 << 33) +} + +/// A placement opportunity, or another segmentation type, of `cue_id`, `seconds` long. +fn seg(cue_id: u32, seconds: u64, type_id: u8, sub: Option<(u8, u8)>) -> SegmentationSpec { + SegmentationSpec { event_id: cue_id, cancelled: false, duration: Some(seconds * 90_000), type_id, sub } +} + +/// How the main room's cues are written. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Cues { + Main, + /// Cue 2's section with its CRC's last byte flipped. + BadCrc, + /// Cue 4's section naming segmentation event 40. + OtherId, +} + +/// The main room's breaks: (cue id, start s, type, duration ms, the in's start s), in order. +pub const BREAKS: [(u32, u64, u8, u32, Option); 8] = [ + (1, 600, cue_type::OUT, 60_000, Some(660)), + (2, 1_200, cue_type::BREAK, 90_000, None), + (3, 1_800, cue_type::BREAK, 90_000, None), + (4, 2_400, cue_type::OUT, 120_000, Some(2_520)), + (5, 3_000, cue_type::BREAK, 60_000, None), + (6, 4_200, cue_type::BREAK, 90_000, None), + (7, 6_000, cue_type::BREAK, 120_000, None), + (8, 6_600, cue_type::BREAK, 60_000, None), +]; + +/// The main room: the producer cues eight breaks of the day, each kind of section §5 reads +/// among them, and returns the room with each break's opening cue and avail. +fn main_room(cues: Cues) -> (RoomBuild, Vec<([u8; 32], u64)>) { + let mut r = RoomBuild::new(16); + let mut opened = Vec::new(); + for (id, t, ctype, dur, in_at) in BREAKS { + let secs = dur as u64 / 1000; + let s = match id { + // A splice_insert() with its break duration: slots from the policy. + 1 => Some(section(&Command::Insert { id, cancel: false, out: true, duration: Some(secs * 90_000) }, &[])), + // Provider and distributor placement opportunities stating three and four slots. + 2 | 3 | 4 | 7 => { + let ty = if id == 3 { 0x36 } else { 0x34 }; + let n = if dur >= 120_000 { 4 } else { 3 }; + let event = if id == 4 && cues == Cues::OtherId { 40 } else { id }; + let mut s = section(&Command::TimeSignal { pts: pts(t) }, &[seg(event, secs, ty, Some((1, n)))]); + if id == 2 && cues == Cues::BadCrc { + let n = s.len(); + s[n - 1] ^= 0x01; + } + Some(s) + } + // A splice_insert() carrying a placement opportunity of three slots. + 6 => Some(section(&Command::Insert { id, cancel: false, out: true, duration: Some(secs * 90_000) }, &[SegmentationSpec { duration: None, ..seg(id, 0, 0x34, Some((1, 3))) }])), + // A provider advertisement start, a type that states no slots. + 8 => Some(section(&Command::TimeSignal { pts: pts(t) }, &[seg(id, secs, 0x30, None)])), + // A cue of form 0. + _ => None, + }; + opened.push((r.cue(t, ctype, id, dur, s), dur as u64)); + if let Some(t_in) = in_at { + r.cue(t_in, cue_type::IN, id, dur, None); + } + } + (r, opened) +} + +/// A second room of the channel's: one break, at 55 minutes, or at 20½ minutes, inside cue 2's. +fn backup_room(overlapping: bool) -> RoomBuild { + let mut r = RoomBuild::new(15); + let t = if overlapping { 1_230 } else { 3_300 }; + r.cue(t, cue_type::BREAK, 9, 60_000, None); + r +} + +// ---- the ledger ---- + +fn ad(id: &str, creative: u8, duration_ms: u64, price_micros: u64) -> AdCandidate { + AdCandidate { ad_id: id.into(), creative_hash: sha256(&[b"wai:opportunity-corpus/creative\x01", &[creative]]), duration_ms, price_micros } +} + +/// Three ads: two of 15 s and one of 20 s. +fn pod() -> Vec { + vec![ad("ad-a", 1, 15_000, 40_000), ad("ad-b", 2, 15_000, 30_000), ad("ad-c", 3, 20_000, 25_000)] +} + +fn session(i: u8) -> [u8; 16] { + sha256(&[b"wai:opportunity-corpus/session\x01", &[i]])[..16].try_into().expect("16 bytes") +} + +/// A viewer's receipt for a break: the fill of the pod, every ad played to its end. +fn receipt(r: &RoomBuild, cue: [u8; 32], avail_ms: u64, s: u8) -> SsaiBreakReceipt { + let funds = BUDGET_MINOR * 1_000_000; + let fill = decide(&pod(), r.policy.slots as u64, avail_ms, funds); + let mut imps = fill.clone(); + for i in &mut imps { + i.quartiles_reached = 4; + } + let list = crate::live_ads::asset_list(&r.policy, asset_format::HLS, &fill); + let binding = BreakBinding { + room: r.id, + cue, + session: session(s), + start_offset_ms: 0, + format: asset_format::HLS, + asset_list_sha256: sha256(&[&list]), + avail_ms, + slots: r.policy.slots as u64, + funds_remaining_micros: funds, + }; + let (grant_hash, capability, joule_ceiling_micro, funds_ceiling) = r.decider.grant_ref(); + let grant = GrantRef { grant_hash, capability: capability.into(), joule_ceiling_micro, funds_ceiling }; + SsaiBreakReceipt::seal(&key("decider"), crate::signer_id::did_key(&pk("decider")), binding, pod_hash(&pod()), imps, pod().len() as u64, 0, None, grant) +} + +/// The room as a source of the receipt ledger, with every viewer's receipt for every break. +fn ads_source(r: &RoomBuild, opened: &[([u8; 32], u64)]) -> Value { + let mut receipts = Vec::new(); + let mut pods = Vec::new(); + let pod_json: Vec = pod().iter().map(|c| json!({"ad_id": c.ad_id, "creative_hash": hex(&c.creative_hash), "duration_ms": c.duration_ms, "price_micros": c.price_micros})).collect(); + for (cue, avail) in opened { + for s in 1..=SESSIONS { + receipts.push(receipt(r, *cue, *avail, s).to_json()); + pods.push(Value::Array(pod_json.clone())); + } + } + let i = r.input(); + json!({ + "kind": "ads", "descriptor": hex(&i.descriptor), "grants": i.grants.iter().map(|g| hex(g)).collect::>(), "key_log": null, + "chains": i.chains.iter().map(|c| c.iter().map(|e| hex(e)).collect::>()).collect::>(), "policy": hex(&i.policy), "pt0_ms": 0, + "receipts": receipts, "pods": pods, "claims": [], "creatives": [], "issuers": [], + }) +} + +// ---- the channel ---- + +fn entry(asset: &str, start_s: u64, duration_s: u64, kind_: EntryKind) -> ScheduleEntry { + ScheduleEntry { asset_id: asset.into(), start_ms: DAY16 / 1000 + start_s * 1000, duration_ms: duration_s * 1000, kind: kind_ } +} + +fn run(schedule: Vec, window_s: (u64, u64), parent: Option<[u8; 32]>) -> ChannelReceipt { + let ms = |s: u64| DAY16 / 1000 + s * 1000; + ChannelReceipt::seal(&key("channel"), "did:key:channel-signer", CHANNEL, schedule, ms(window_s.0), ms(window_s.1), 0, None, GrantRef::unbounded("video.channel"), parent) +} + +/// The day's first two hours, one window each: owner A's episode, an ad slot, owner B's +/// documentary; then A's next episode, an ad slot no cue covers, and B's next documentary. +fn runs(last: &str) -> (ChannelReceipt, ChannelReceipt) { + let r1 = run(vec![entry("a/ep-1", 0, 1_800, EntryKind::Program), entry("slate-90", 1_800, 90, EntryKind::Ad), entry("b/doc-1", 1_890, 1_710, EntryKind::Program)], (0, 3_600), None); + let r2 = run(vec![entry("a/ep-2", 3_600, 1_740, EntryKind::Program), entry("mid-60", 5_340, 60, EntryKind::Ad), entry(last, 5_400, 1_800, EntryKind::Program)], (3_600, 7_200), Some(r1.receipt_hash())); + (r1, r2) +} + +/// The contract: two owners, a split each and a between split, under the rule preceding. +fn contract(rooms: &[[u8; 32]]) -> Contract { + let mut owners = vec![pk("owner-a"), pk("owner-b")]; + owners.sort(); + let ia = owners.iter().position(|k| *k == pk("owner-a")).expect("listed") as u8; + let ib = 1 - ia; + let mut carriers: Vec<(u8, [u8; 32])> = rooms.iter().map(|r| (share::carrier::ROOM, *r)).collect(); + carriers.sort(); + let mut halves = vec![(ia, 500_000), (ib, 500_000)]; + halves.sort(); + Contract { + operator: pk("operator"), + start: OCT, + end: NOV, + currency: *b"EUR", + channel: CHANNEL.as_bytes().to_vec(), + channel_signer: pk("channel"), + rule: rule::PRECEDING, + between: 2, + carriers, + owners, + splits: vec![ + Split { operator_ppm: 666_667, shares: vec![(ia, 333_333)] }, + Split { operator_ppm: 400_000, shares: vec![(ib, 600_000)] }, + Split { operator_ppm: 0, shares: halves }, + ], + assets: ["a/ep-1", "a/ep-2", "b/doc-1", "b/doc-2"].iter().map(|a| (a.as_bytes().to_vec(), if a.starts_with('a') { 0 } else { 1 })).collect(), + operator_sig: [0; 64], + countersigs: Vec::new(), + } +} + +fn in_force(c: Contract) -> Contract { + c.offer(&key("operator")).countersign(&key("owner-a")).expect("an owner").countersign(&key("owner-b")).expect("an owner") +} + +// ---- the corpus ---- + +/// The named parts every vector draws on. +pub struct Pools { + pub contracts: BTreeMap, + pub runs: BTreeMap, + pub rooms: BTreeMap, + pub ledgers: BTreeMap, + pub reported: BTreeMap, +} + +/// A report's verification document by names (§8.1). +#[derive(Clone)] +pub struct ReportParts { + pub contract: &'static str, + pub contracts: Vec<&'static str>, + pub report: Vec, + pub runs: Option>, + pub rooms: Vec<&'static str>, +} + +/// A revenue-share verification document by names. +#[derive(Clone)] +pub struct StatementParts { + pub contract: &'static str, + pub statement: Vec, + pub runs: Vec<&'static str>, + pub ledgers: Vec<&'static str>, +} + +impl Pools { + fn named(&self, pool: &BTreeMap, n: &str) -> Value { + pool.get(n).unwrap_or_else(|| panic!("no part {n}")).clone() + } + + /// A report's document, with its report or without one. + pub fn report_document(&self, p: &ReportParts, with_report: bool) -> Value { + let mut d = Map::new(); + d.insert("contract".into(), self.named(&self.contracts, p.contract)); + d.insert("contracts".into(), Value::Array(p.contracts.iter().map(|n| self.named(&self.contracts, n)).collect())); + if with_report { + d.insert("report".into(), json!(hex(&p.report))); + } + d.insert("runs".into(), p.runs.as_ref().map_or(Value::Null, |r| Value::Array(r.iter().map(|n| self.named(&self.runs, n)).collect()))); + d.insert("rooms".into(), Value::Array(p.rooms.iter().map(|n| self.named(&self.rooms, n)).collect())); + Value::Object(d) + } + + pub fn statement_document(&self, p: &StatementParts, with_statement: bool) -> Value { + let mut d = Map::new(); + d.insert("contract".into(), self.named(&self.contracts, p.contract)); + d.insert("contracts".into(), json!([])); + if with_statement { + d.insert("statement".into(), json!(hex(&p.statement))); + } + d.insert("runs".into(), Value::Array(p.runs.iter().map(|n| self.named(&self.runs, n)).collect())); + d.insert("ledgers".into(), Value::Array(p.ledgers.iter().map(|n| self.named(&self.ledgers, n)).collect())); + Value::Object(d) + } + + /// A check's document, with its check or without one. + pub fn check_document(&self, report: &ReportParts, reported: &str, statement: Option<&StatementParts>, check: Option<&[u8]>) -> Value { + let mut d = Map::new(); + d.insert("report".into(), self.report_document(report, true)); + d.insert("reported".into(), self.named(&self.reported, reported)); + d.insert("statement".into(), statement.map_or(Value::Null, |s| self.statement_document(s, true))); + if let Some(c) = check { + d.insert("check".into(), json!(hex(c))); + } + Value::Object(d) + } +} + +/// The world of the corpus: its pools, and the parts its accepted vectors name. +pub struct World { + pub pools: Pools, + pub reports: BTreeMap<&'static str, ReportParts>, + pub statements: BTreeMap<&'static str, StatementParts>, +} + +fn reported_json(r: Value) -> Value { + json!({"form": "json", "text": serde_json::to_string(&r).expect("JSON")}) +} + +/// B's reported statement of the day, per programme: its second documentary's impressions all +/// but missing. +fn b_programmes(channel: &str, extra: Option) -> Value { + let mut programmes = vec![json!({"asset": "b/doc-1", "impressions": 12, "revenue": 7_200_000}), json!({"asset": "b/doc-2", "impressions": 2, "revenue": 1_200_000})]; + if let Some(e) = extra { + programmes.insert(0, e); + } + let (imps, rev) = programmes.iter().fold((0u64, 0u64), |(i, r), p| (i + p["impressions"].as_u64().unwrap(), r + p["revenue"].as_u64().unwrap())); + json!({"channel": channel, "start": DAY16, "end": DAY17, "currency": "EUR", "basis": "share", "impressions": imps, "revenue": rev, "programmes": programmes}) +} + +/// A's reported statement of the day in CSV, per day: what the signed statement states. +fn a_days(header: &str) -> String { + format!("{header}\r\nchannel,{CHANNEL},,\r\nstart,{DAY16},,\r\nend,{DAY17},,\r\ncurrency,EUR,,\r\nbasis,share,,\r\nperiod,,24,7999992\r\nday,2026-10-16,24,7999992\r\n") +} + +/// Build the world: pools, the day's reports and statements. +pub fn world() -> Result { + let (main, opened) = main_room(Cues::Main); + let backup = backup_room(false); + let mut rooms = BTreeMap::new(); + let mut put_room = |n: &str, r: &RoomInput| { + rooms.insert(n.to_string(), r.to_json()); + }; + put_room("main", &main.input()); + put_room("main_crc_flipped", &main_room(Cues::BadCrc).0.input()); + put_room("main_other_segmentation_id", &main_room(Cues::OtherId).0.input()); + put_room("backup", &backup.input()); + put_room("backup_overlapping", &backup_room(true).input()); + let mut gap = main.input(); + gap.chains[1].remove(4); + put_room("main_with_a_gap", &gap); + let mut flipped = main.input(); + let last = flipped.chains[1][2].len() - 1; + flipped.chains[1][2][last] ^= 0x01; + put_room("main_signature_flipped", &flipped); + + let (r1, r2) = runs("b/doc-2"); + let (_, r2_unmapped) = runs("c/film-9"); + let mut runs_ = BTreeMap::new(); + runs_.insert("hour_1".to_string(), r1.to_json()); + runs_.insert("hour_2".to_string(), r2.to_json()); + runs_.insert("hour_2_unmapped".to_string(), r2_unmapped.to_json()); + let mut tampered = r1.clone(); + tampered.aired = vec![0]; + tampered.sig = key("channel").sign(&tampered.payload()).to_bytes(); + runs_.insert("hour_1_aired_misstated".to_string(), tampered.to_json()); + + let c = in_force(contract(&[main.id])); + let two = in_force(contract(&[main.id, backup.id])); + let offered = contract(&[main.id]).offer(&key("operator")).countersign(&key("owner-a")).expect("an owner"); + let mut contracts = BTreeMap::new(); + contracts.insert("main".to_string(), json!(hex(&c.to_bytes()))); + contracts.insert("two_rooms".to_string(), json!(hex(&two.to_bytes()))); + contracts.insert("offered".to_string(), json!(hex(&offered.to_bytes()))); + + // The ledger: the room's receipts for every break, billed for the day. + let source = ads_source(&main, &opened); + let t = Tariff { + seller: pk("operator"), + buyer: pk("buyer"), + start: OCT, + end: NOV, + currency: *b"EUR", + room: [0; ledger::ROOM_PRICES], + delivery: [0; 4], + package: [0; 3], + ads: PRICES, + scope: vec![(4, pk("decider"))], + seller_sig: [0; 64], + buyer_sig: [0; 64], + } + .sign(&key("operator"), &key("buyer")); + let invoice = ledger::make_invoice(&key("operator"), &t, None, DAY16, DAY17, std::slice::from_ref(&source)).map_err(|e| format!("the day's invoice: {e:?}"))?; + let mut ledgers = BTreeMap::new(); + ledgers.insert("day_16".to_string(), json!({"tariff": hex(&t.to_bytes()), "invoice": hex(&invoice.to_bytes()), "previous": null, "sources": [source]})); + + let mut reported = BTreeMap::new(); + reported.insert("b_programmes".to_string(), reported_json(b_programmes(CHANNEL, None))); + reported.insert("b_programmes_and_a_programme".to_string(), reported_json(b_programmes(CHANNEL, Some(json!({"asset": "a/ep-1", "impressions": 5, "revenue": 3_000_000}))))); + reported.insert("b_programmes_of_another_channel".to_string(), reported_json(b_programmes("fast-8", None))); + let mut outside = b_programmes(CHANNEL, None); + outside["days"] = json!([{"day": "2026-10-17", "impressions": 14, "revenue": 8_400_000}]); + reported.insert("b_a_day_after_the_period".to_string(), reported_json(outside)); + reported.insert("a_days".to_string(), json!({"form": "csv", "text": a_days("scope,key,impressions,revenue")})); + reported.insert("a_days_header_misspelt".to_string(), json!({"form": "csv", "text": a_days("scope,key,impression,revenue")})); + reported.insert("of_another_form".to_string(), json!({"form": "xml", "text": ""})); + + let pools = Pools { contracts, runs: runs_, rooms, ledgers, reported }; + + // The operator's signed statements of the day. + let mut statements = BTreeMap::new(); + for (name, who) in [("owner_a", "owner-a"), ("owner_b", "owner-b"), ("operator", "operator")] { + let mut sp = StatementParts { contract: "main", statement: Vec::new(), runs: vec!["hour_1", "hour_2"], ledgers: vec!["day_16"] }; + let (s, _): (Statement, Value) = share::create(&pools.statement_document(&sp, false), &key("operator"), pk(who), DAY16, DAY17).map_err(|e| format!("the statement of {who}: {e:?}"))?; + sp.statement = s.to_bytes(); + statements.insert(name, sp); + } + + // The reports of the day. + let mut reports = BTreeMap::new(); + let both = || Some(vec!["hour_1", "hour_2"]); + for (name, contract_, rooms_, who, issuer) in [ + ("owner_a", "main", vec!["main"], "owner-a", "owner-a"), + ("owner_b", "main", vec!["main"], "owner-b", "owner-b"), + ("operator", "main", vec!["main"], "operator", "operator"), + ("owner_a_runs_alone", "main", vec![], "owner-a", "auditor"), + ("owner_b_two_rooms", "two_rooms", vec!["main", "backup"], "owner-b", "auditor"), + ] { + let mut rp = ReportParts { contract: contract_, contracts: vec![], report: Vec::new(), runs: both(), rooms: rooms_ }; + let (r, _) = super::create(&pools.report_document(&rp, false), &key(issuer), pk(who), DAY16, DAY17).map_err(|e| format!("the report {name}: {e:?}"))?; + rp.report = r.to_bytes(); + reports.insert(name, rp); + } + Ok(World { pools, reports, statements }) +} + +/// A report changed and signed again by its issuer. +fn resealed(r: &[u8], issuer: &str, f: impl FnOnce(&mut Report)) -> Vec { + let mut r = Report::read(r).expect("a corpus report reads"); + f(&mut r); + r.sign(&key(issuer)).to_bytes() +} + +/// A check changed and signed again by its issuer. +fn check_resealed(c: &[u8], issuer: &str, f: impl FnOnce(&mut Check)) -> Vec { + let mut c = Check::read(c).expect("a corpus check reads"); + f(&mut c); + c.sign(&key(issuer)).to_bytes() +} + +fn flip(b: &[u8], at: usize) -> Vec { + let mut b = b.to_vec(); + b[at] ^= 0x01; + b +} + +/// One vector: a report's document or a check's, by names. +enum Vector { + Report { name: &'static str, about: &'static str, parts: ReportParts }, + Check { name: &'static str, about: &'static str, report: ReportParts, reported: &'static str, statement: Option, check: Vec }, +} + +fn report_parts_json(p: &ReportParts) -> Value { + json!({"contract": p.contract, "contracts": p.contracts, "report": hex(&p.report), "runs": p.runs, "rooms": p.rooms}) +} + +fn statement_parts_json(p: &StatementParts) -> Value { + json!({"contract": p.contract, "statement": hex(&p.statement), "runs": p.runs, "ledgers": p.ledgers}) +} + +/// Write the corpus, from fixed seeds. +pub fn generate() -> Result { + let w = world()?; + let p = &w.pools; + let rep = |n: &str| w.reports[n].clone(); + let st = |n: &str| w.statements[n].clone(); + let a = rep("owner_a"); + let ra = a.report.clone(); + let b_lines = Report::read(&rep("owner_b").report).expect("reads").lines; + let with = |name: &'static str, about: &'static str, f: &dyn Fn(&mut ReportParts)| { + let mut parts = a.clone(); + f(&mut parts); + Vector::Report { name, about, parts } + }; + let report = |name: &'static str, about: &'static str, report: Vec| with(name, about, &|x: &mut ReportParts| x.report = report.clone()); + // The checks of the day, signed by the owner each is for. + let check_of = |report: &str, reported: &str, statement: Option<&str>, issuer: &str| -> Result, String> { + let s = statement.map(|n| w.statements[n].clone()); + let inputs = p.check_document(&w.reports[report], reported, s.as_ref(), None); + create_check(&inputs, &key(issuer), FLOOR_PPM, CEILING_PPM).map(|(c, _)| c.to_bytes()).map_err(|e| format!("the check of {report} and {reported}: {e:?}")) + }; + let cb = check_of("owner_b", "b_programmes", Some("owner_b"), "owner-b")?; + let ca = check_of("owner_a", "a_days", Some("owner_a"), "owner-a")?; + let cb_alone = check_of("owner_b", "b_programmes", None, "owner-b")?; + let cb_unproved = check_of("owner_b", "b_programmes_and_a_programme", None, "owner-b")?; + let check = |name: &'static str, about: &'static str, report: ReportParts, reported: &'static str, statement: Option, check: Vec| Vector::Check { name, about, report, reported, statement, check }; + let b = rep("owner_b"); + let b_check = |name: &'static str, about: &'static str, check_: Vec| check(name, about, b.clone(), "b_programmes", Some(st("owner_b")), check_); + let mut b_report_flipped = b.clone(); + let n = b_report_flipped.report.len(); + b_report_flipped.report = flip(&b_report_flipped.report, n - 10); + let mut b_statement_flipped = st("owner_b"); + let n = b_statement_flipped.statement.len(); + b_statement_flipped.statement = flip(&b_statement_flipped.statement, n - 10); + + let vectors = vec![ + // Accepted reports. + Vector::Report { name: "owner_a_day", about: "owner A's day: four cued breaks in its episodes, one of them inside the ad slot after its first episode, and the ad slot no cue covers, scheduled and unslotted", parts: rep("owner_a") }, + Vector::Report { name: "owner_b_day", about: "owner B's day: four cued breaks in its documentaries, slots from placement opportunities and from the room's policy", parts: rep("owner_b") }, + Vector::Report { name: "operator_day", about: "the operator's day: its share of both owners' splits; no line of the between split, in which it holds no share", parts: rep("operator") }, + Vector::Report { name: "owner_a_runs_alone", about: "owner A's day from the runs alone: both ad slots scheduled, no cued break", parts: rep("owner_a_runs_alone") }, + Vector::Report { name: "owner_b_two_rooms", about: "owner B's day under a contract naming two rooms: a break of the second room in its first documentary, overlapping none", parts: rep("owner_b_two_rooms") }, + // Accepted checks. + check("check_owner_b_under_reported", "owner B's reported statement per programme beside its report and the signed statement: its second documentary far below the day's rate, and the day below the signed impressions and revenue", b.clone(), "b_programmes", Some(st("owner_b")), cb.clone()), + check("check_owner_a_csv", "owner A's reported statement in CSV, per day, beside its report and the signed statement: nothing flagged", a.clone(), "a_days", Some(st("owner_a")), ca.clone()), + check("check_owner_b_without_statement", "owner B's reported statement beside its report alone", b.clone(), "b_programmes", None, cb_alone), + check("check_owner_b_reports_a_programme", "owner B's reported statement listing owner A's episode: impressions where no break of B's aired", b.clone(), "b_programmes_and_a_programme", None, cb_unproved), + // The report's document and contract. + with("document_malformed", "a report's document whose runs are not a list", &|x| x.runs = None), + with("contract_offered", "a contract owner B has not countersigned", &|x| x.contract = "offered"), + // The report. + report("report_truncated", "a report of ten bytes", ra[..10].to_vec()), + report("report_magic", "a report whose magic is WOR2", [b"WOR2".as_slice(), &ra[4..]].concat()), + report("report_version_2", "a report of version 2", [&ra[..4], &[2u8][..], &ra[5..]].concat()), + report("report_lines_unordered", "a report whose lines are not in ascending order", resealed(&ra, "owner-a", |r| r.lines.reverse())), + report("report_of_another_contract", "a report naming the contract of two rooms", resealed(&ra, "owner-a", |r| r.contract = Contract::read(&unhex(p.contracts["two_rooms"].as_str().unwrap()).unwrap()).unwrap().digest())), + report("report_past_the_contract", "a report whose period ends a day after the contract's", resealed(&ra, "owner-a", |r| r.end = NOV + DAY_US)), + report("report_signature", "a report whose signature has a byte flipped", flip(&ra, ra.len() - 40)), + report("report_issuer_small_order", "a report whose issuer key is of small order", { + let mut b = ra.clone(); + b[5..37].copy_from_slice(&crate::sig::IDENTITY); + b + }), + // The runs and the rooms. + with("run_aired_misstated", "a run whose aired set the window does not give, signed again", &|x| x.runs = Some(vec!["hour_1_aired_misstated", "hour_2"])), + with("room_event_signature", "a room whose producer's third event has a byte of its signature flipped", &|x| x.rooms = vec!["main_signature_flipped"]), + with("room_with_a_gap", "a room whose producer's chain lacks its fifth event", &|x| x.rooms = vec!["main_with_a_gap"]), + with("room_foreign", "a room the contract does not name", &|x| x.rooms = vec!["backup"]), + with("room_repeated", "the contract's room given twice", &|x| x.rooms = vec!["main", "main"]), + // The breaks. + with("cue_section_crc", "a room whose second break's section has its CRC damaged", &|x| x.rooms = vec!["main_crc_flipped"]), + with("cue_section_other_id", "a room whose fourth break's section names segmentation event 40 for cue 4", &|x| x.rooms = vec!["main_other_segmentation_id"]), + Vector::Report { + name: "two_rooms_overlapping", + about: "a second room of the contract's whose break falls inside the first room's second break", + parts: ReportParts { rooms: vec!["main", "backup_overlapping"], ..rep("owner_b_two_rooms") }, + }, + with("break_outside_every_window", "the run of the first hour alone: the breaks of the second aired in no window held", &|x| x.runs = Some(vec!["hour_1"])), + with("asset_unmapped", "a run of the second hour whose last programme the contract does not map", &|x| x.runs = Some(vec!["hour_1", "hour_2_unmapped"])), + // The figures. + report("line_of_another_split", "owner A's report with a line of owner B's split", resealed(&ra, "owner-a", |r| r.lines.push(Line { ppm: 1, ..b_lines[0] }))), + report("line_citations_changed", "a line whose citation digest has a byte changed", resealed(&ra, "owner-a", |r| r.lines[0].cites[0] ^= 0x01)), + report("line_seconds_raised", "a line stating a second more of breaks, and the total with it", resealed(&ra, "owner-a", |r| { + r.lines[0].break_ms += 1_000; + r.break_ms += 1_000; + })), + report("runs_listed_short", "a report listing one of the two runs it is given", resealed(&ra, "owner-a", |r| r.runs.truncate(1))), + // The check's document, the report and the reported statement. + check("check_document_malformed", "a check's document whose reported statement is of a form this text does not define", b.clone(), "of_another_form", None, cb.clone()), + check("check_report_refused", "a check whose report has a byte of its signature flipped, refused at the report's place", b_report_flipped, "b_programmes", Some(st("owner_b")), cb.clone()), + check("reported_day_outside_the_period", "a reported statement listing a day after its period", b.clone(), "b_a_day_after_the_period", None, cb.clone()), + check("reported_csv_header", "a reported statement in CSV whose header misspells a column", a.clone(), "a_days_header_misspelt", None, ca.clone()), + check("reported_of_another_channel", "a reported statement of another channel", b.clone(), "b_programmes_of_another_channel", None, cb.clone()), + // The signed statement. + check("check_statement_refused", "a signed statement with a byte of its signature flipped, refused at the statement's place", b.clone(), "b_programmes", Some(b_statement_flipped), cb.clone()), + check("statement_of_another_owner", "owner B's report beside owner A's signed statement", b.clone(), "b_programmes", Some(st("owner_a")), cb.clone()), + // The check. + b_check("check_truncated", "a check of ten bytes", cb[..10].to_vec()), + b_check("check_floor_above_the_rate", "a check whose floor is above the whole", check_resealed(&cb, "owner-b", |c| c.floor_ppm = 1_500_000)), + b_check("check_flags_changed", "a check whose flags leave out the programme's below", check_resealed(&cb, "owner-b", |c| { + for l in &mut c.lines { + l.flags &= !(1 << 2); + } + })), + b_check("check_of_another_reported_statement", "a check naming the bytes of another reported statement", check_resealed(&cb, "owner-b", |c| c.reported = sha256(&[b"another"]))), + b_check("check_signature", "a check whose signature has a byte flipped", flip(&cb, cb.len() - 40)), + ]; + + let mut out = Vec::with_capacity(vectors.len()); + for v in &vectors { + out.push(match v { + Vector::Report { name, about, parts } => { + let outcome = outcome_json(&super::verify(&p.report_document(parts, true))); + json!({"name": name, "about": about, "kind": "report", "report": report_parts_json(parts), "outcome": outcome}) + } + Vector::Check { name, about, report, reported, statement, check } => { + let outcome = check_outcome_json(&verify_check(&p.check_document(report, reported, statement.as_ref(), Some(check)))); + json!({ + "name": name, "about": about, "kind": "check", "report": report_parts_json(report), "reported": reported, + "statement": statement.as_ref().map(statement_parts_json), "check": hex(check), "outcome": outcome, + }) + } + }); + } + let keys = json!({ + "operator": hex(&pk("operator")), "owner_a": hex(&pk("owner-a")), "owner_b": hex(&pk("owner-b")), "channel_signer": hex(&pk("channel")), + "auditor": hex(&pk("auditor")), "producer": hex(&pk("producer")), "decider": hex(&pk("decider")), + }); + let to_map = |m: &BTreeMap| Value::Object(m.clone().into_iter().collect()); + let doc = json!({ + "corpus": CAPABILITY, + "extension": "extensions/opportunity-report.md", + "keys": keys, + "contracts": to_map(&p.contracts), + "runs": to_map(&p.runs), + "rooms": to_map(&p.rooms), + "ledgers": to_map(&p.ledgers), + "reported": to_map(&p.reported), + "vectors": out, + }); + Ok(serde_json::to_string_pretty(&doc).expect("JSON") + "\n") +} + +/// The parts of the committed corpus, read back into pools. +fn pools_of(doc: &Value) -> Option { + let map = |k: &str| -> Option> { Some(doc[k].as_object()?.iter().map(|(n, v)| (n.clone(), v.clone())).collect()) }; + Some(Pools { contracts: map("contracts")?, runs: map("runs")?, rooms: map("rooms")?, ledgers: map("ledgers")?, reported: map("reported")? }) +} + +/// A name read back from the corpus, held for the life of the process (the corpus's parts name +/// their pools by `&'static str`). +fn leak(s: &str) -> &'static str { + Box::leak(s.to_string().into_boxed_str()) +} + +fn names(v: &Value) -> Option> { + v.as_array()?.iter().map(|n| n.as_str().map(leak)).collect() +} + +fn report_parts(v: &Value) -> Option { + Some(ReportParts { + contract: leak(v["contract"].as_str()?), + contracts: names(&v["contracts"])?, + report: unhex(v["report"].as_str()?)?, + runs: if v["runs"].is_null() { None } else { Some(names(&v["runs"])?) }, + rooms: names(&v["rooms"])?, + }) +} + +fn statement_parts(v: &Value) -> Option { + Some(StatementParts { contract: leak(v["contract"].as_str()?), statement: unhex(v["statement"].as_str()?)?, runs: names(&v["runs"])?, ledgers: names(&v["ledgers"])? }) +} + +/// A vector of the corpus as its verification document, its parts resolved by name. +pub fn document(doc: &Value, v: &Value) -> Option { + let p = pools_of(doc)?; + let has = |pool: &BTreeMap, ns: &[&str]| ns.iter().all(|n| pool.contains_key(*n)); + let rp = report_parts(&v["report"])?; + if !has(&p.contracts, &[rp.contract]) || !has(&p.contracts, &rp.contracts) || !has(&p.runs, rp.runs.as_deref().unwrap_or(&[])) || !has(&p.rooms, &rp.rooms) { + return None; + } + match v["kind"].as_str()? { + "report" => Some(p.report_document(&rp, true)), + "check" => { + let reported = v["reported"].as_str()?; + let sp = if v["statement"].is_null() { None } else { Some(statement_parts(&v["statement"])?) }; + if !has(&p.reported, &[reported]) || sp.as_ref().is_some_and(|s| !has(&p.contracts, &[s.contract]) || !has(&p.runs, &s.runs) || !has(&p.ledgers, &s.ledgers)) { + return None; + } + Some(p.check_document(&rp, reported, sp.as_ref(), Some(&unhex(v["check"].as_str()?)?))) + } + _ => None, + } +} + +/// The outcome a vector's document gets. +pub fn outcome_of(v: &Value, d: &Value) -> Value { + if v["kind"] == "check" { check_outcome_json(&verify_check(d)) } else { outcome_json(&super::verify(d)) } +} + +/// The failures: every vector whose stated outcome is not the one this module gives. +pub fn verify(text: &str) -> Vec { + let doc: Value = match serde_json::from_str(text) { + Ok(d) => d, + Err(e) => return vec![format!("vectors.json: {e}")], + }; + let mut fails = Vec::new(); + for v in doc["vectors"].as_array().into_iter().flatten() { + let name = v["name"].as_str().unwrap_or("?"); + let Some(d) = document(&doc, v) else { + fails.push(format!("{name}: the vector does not read")); + continue; + }; + let got = outcome_of(v, &d); + if got != v["outcome"] { + fails.push(format!("{name}: the outcome is {got}, the corpus states {}", v["outcome"])); + } + } + fails +} diff --git a/wai/wai-rs/src/opportunity_report/reported.rs b/wai/wai-rs/src/opportunity_report/reported.rs new file mode 100644 index 000000000..b9fe460b7 --- /dev/null +++ b/wai/wai-rs/src/opportunity_report/reported.rs @@ -0,0 +1,341 @@ +//! The reported statement (extensions/opportunity-report.md §9): what a platform reports it +//! counted, in a documented JSON form (§9.1) or CSV form (§9.2), held to §9.3's rules. It is an +//! input and never evidence: a check names it by the digest of its bytes. + +use serde_json::{Map, Value}; + +/// The forms (§10.1 `form`). +pub mod form { + pub const JSON: u8 = 1; + pub const CSV: u8 = 2; + + pub fn name(f: u8) -> &'static str { + match f { + JSON => "json", + CSV => "csv", + _ => "?", + } + } + + pub fn code(s: &str) -> Option { + match s { + "json" => Some(JSON), + "csv" => Some(CSV), + _ => None, + } + } +} + +/// The bases of the reported revenue (§9.1). +pub mod basis { + pub const GROSS: u8 = 1; + pub const SHARE: u8 = 2; + + pub fn name(b: u8) -> &'static str { + match b { + GROSS => "gross", + SHARE => "share", + _ => "?", + } + } + + pub fn code(s: &str) -> Option { + match s { + "gross" => Some(GROSS), + "share" => Some(SHARE), + _ => None, + } + } +} + +/// A day's figures: the day as written, the start of its span, and its figures. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Day { + pub day: String, + pub start: u64, + pub impressions: u64, + pub revenue: u64, +} + +/// A programme's figures. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Programme { + pub asset: Vec, + pub impressions: u64, + pub revenue: u64, +} + +/// A reported statement, read. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Reported { + pub channel: Vec, + pub start: u64, + pub end: u64, + pub currency: [u8; 3], + pub basis: u8, + pub impressions: u64, + pub revenue: u64, + pub days: Vec, + pub programmes: Vec, +} + +/// A day's span, in microseconds. +pub const DAY_US: u64 = 86_400_000_000; + +fn days_from_civil(y: i64, m: i64, d: i64) -> i64 { + let y = if m <= 2 { y - 1 } else { y }; + let era = y.div_euclid(400); + let yoe = y - era * 400; + let mp = (m + 9) % 12; + let doy = (153 * mp + 2) / 5 + d - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + era * 146_097 + doe - 719_468 +} + +/// The start of a day written `YYYY-MM-DD` (§2), a date of the Gregorian calendar from 1970 to +/// 9999, in microseconds since 1970. +pub fn day_start(s: &str) -> Option { + let b = s.as_bytes(); + let digits = |r: std::ops::Range| -> Option { b[r].iter().try_fold(0i64, |a, c| c.is_ascii_digit().then(|| a * 10 + (c - b'0') as i64)) }; + if b.len() != 10 || b[4] != b'-' || b[7] != b'-' { + return None; + } + let (y, m, d) = (digits(0..4)?, digits(5..7)?, digits(8..10)?); + let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; + let month_days = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + if !(1970..=9999).contains(&y) || !(1..=12).contains(&m) || d < 1 || d > month_days[(m - 1) as usize] { + return None; + } + Some(days_from_civil(y, m, d) as u64 * DAY_US) +} + +/// A JSON integer from 0 to 2^64 − 1. +fn int(v: &Value) -> Option { + v.as_u64() +} + +fn members(o: &Map, required: &[&str], optional: &[&str]) -> bool { + required.iter().all(|k| o.contains_key(*k)) && o.keys().all(|k| required.contains(&k.as_str()) || optional.contains(&k.as_str())) +} + +fn from_json(v: &Value) -> Option { + let o = v.as_object()?; + if !members(o, &["channel", "start", "end", "currency", "basis", "impressions", "revenue"], &["days", "programmes"]) { + return None; + } + let mut days = Vec::new(); + let list = |k: &str| -> Option<&[Value]> { o.get(k).map_or(Some(&[][..]), |v| v.as_array().map(Vec::as_slice)) }; + for d in list("days")? { + let d = d.as_object()?; + if !members(d, &["day", "impressions", "revenue"], &[]) { + return None; + } + let day = d["day"].as_str()?.to_string(); + days.push(Day { start: day_start(&day)?, day, impressions: int(&d["impressions"])?, revenue: int(&d["revenue"])? }); + } + let mut programmes = Vec::new(); + for p in list("programmes")? { + let p = p.as_object()?; + if !members(p, &["asset", "impressions", "revenue"], &[]) { + return None; + } + programmes.push(Programme { asset: p["asset"].as_str()?.as_bytes().to_vec(), impressions: int(&p["impressions"])?, revenue: int(&p["revenue"])? }); + } + Some(Reported { + channel: o["channel"].as_str()?.as_bytes().to_vec(), + start: int(&o["start"])?, + end: int(&o["end"])?, + currency: o["currency"].as_str()?.as_bytes().try_into().ok()?, + basis: basis::code(o["basis"].as_str()?)?, + impressions: int(&o["impressions"])?, + revenue: int(&o["revenue"])?, + days, + programmes, + }) +} + +/// RFC 4180 records (§9.2): each ended by CRLF or LF, the last line break optional. +fn records(text: &str) -> Option>> { + let b = text.as_bytes(); + let n = b.len(); + let mut out = Vec::new(); + let mut p = 0; + if n == 0 { + return None; + } + loop { + let mut rec = Vec::new(); + loop { + let mut f = Vec::new(); + if p < n && b[p] == b'"' { + p += 1; + loop { + if p >= n { + return None; + } + if b[p] == b'"' { + if p + 1 < n && b[p + 1] == b'"' { + f.push(b'"'); + p += 2; + } else { + p += 1; + break; + } + } else { + f.push(b[p]); + p += 1; + } + } + } else { + while p < n && !matches!(b[p], b',' | b'\r' | b'\n') { + if b[p] == b'"' { + return None; + } + f.push(b[p]); + p += 1; + } + } + rec.push(String::from_utf8(f).ok()?); + if p < n && b[p] == b',' { + p += 1; + continue; + } + break; + } + out.push(rec); + if p == n { + break; + } + match b[p] { + b'\r' if p + 1 < n && b[p + 1] == b'\n' => p += 2, + b'\n' => p += 1, + _ => return None, + } + if p == n { + break; + } + } + Some(out) +} + +/// An integer of §9.2: `0`, or a digit 1 to 9 and digits, at most 2^64 − 1. +fn csv_int(s: &str) -> Option { + let b = s.as_bytes(); + if b.is_empty() || !b.iter().all(u8::is_ascii_digit) || (b.len() > 1 && b[0] == b'0') { + return None; + } + s.parse().ok() +} + +fn from_csv(text: &str) -> Option { + let recs = records(text)?; + if recs.iter().any(|r| r.len() != 4) { + return None; + } + let mut it = recs.iter(); + if it.next()? != &["scope", "key", "impressions", "revenue"] { + return None; + } + let mut meta = |name: &str| -> Option { + let r = it.next()?; + (r[0] == name && r[2].is_empty() && r[3].is_empty()).then(|| r[1].clone()) + }; + let channel = meta("channel")?; + let start = csv_int(&meta("start")?)?; + let end = csv_int(&meta("end")?)?; + let currency = meta("currency")?; + let basis_ = meta("basis")?; + let rest: Vec<&Vec> = it.collect(); + let (period, rows) = rest.split_first()?; + if period[0] != "period" || !period[1].is_empty() { + return None; + } + let mut days = Vec::new(); + let mut programmes = Vec::new(); + let mut in_programmes = false; + for r in rows { + let (impressions, revenue) = (csv_int(&r[2])?, csv_int(&r[3])?); + match r[0].as_str() { + "day" if !in_programmes => days.push(Day { start: day_start(&r[1])?, day: r[1].clone(), impressions, revenue }), + "programme" => { + in_programmes = true; + programmes.push(Programme { asset: r[1].as_bytes().to_vec(), impressions, revenue }); + } + _ => return None, + } + } + Some(Reported { + channel: channel.into_bytes(), + start, + end, + currency: currency.as_bytes().try_into().ok()?, + basis: basis::code(&basis_)?, + impressions: csv_int(&period[2])?, + revenue: csv_int(&period[3])?, + days, + programmes, + }) +} + +/// §9.3's rules, short of the binding to the contract and the report. +fn rules(r: &Reported) -> bool { + let text = |t: &[u8]| (1..=1024).contains(&t.len()); + text(&r.channel) + && r.currency.iter().all(u8::is_ascii_uppercase) + && r.start < r.end + && r.days.iter().all(|d| r.start <= d.start && d.start.checked_add(DAY_US).is_some_and(|e| e <= r.end)) + && r.days.windows(2).all(|w| w[0].start < w[1].start) + && r.programmes.iter().all(|p| text(&p.asset)) + && r.programmes.windows(2).all(|w| w[0].asset < w[1].asset) +} + +/// Read a reported statement of a form (§9); `None` is `reported_invalid`. +pub fn read(form_: u8, text: &str) -> Option { + let r = match form_ { + form::JSON => from_json(&serde_json::from_str(text).ok()?)?, + form::CSV => from_csv(text)?, + _ => return None, + }; + rules(&r).then_some(r) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn days() { + assert_eq!(day_start("1970-01-01"), Some(0)); + assert_eq!(day_start("2026-10-16"), Some(1_792_108_800_000_000)); + assert_eq!(day_start("2024-02-29"), Some(19_782 * DAY_US)); + assert_eq!(day_start("2026-02-29"), None); + assert_eq!(day_start("1969-12-31"), None); + assert_eq!(day_start("2026-1-016"), None); + assert_eq!(day_start("2026-13-01"), None); + } + + #[test] + fn the_two_forms_read_alike() { + let json = r#"{"channel":"fast-9","start":0,"end":172800000000,"currency":"EUR","basis":"share","impressions":5,"revenue":7, + "days":[{"day":"1970-01-01","impressions":2,"revenue":3},{"day":"1970-01-02","impressions":3,"revenue":4}], + "programmes":[{"asset":"a,\"b\"","impressions":5,"revenue":7}]}"#; + let csv = "scope,key,impressions,revenue\r\nchannel,fast-9,,\r\nstart,0,,\r\nend,172800000000,,\r\ncurrency,EUR,,\r\nbasis,share,,\r\nperiod,,5,7\r\nday,1970-01-01,2,3\nday,1970-01-02,3,4\nprogramme,\"a,\"\"b\"\"\",5,7"; + let a = read(form::JSON, json).expect("the JSON form reads"); + assert_eq!(read(form::CSV, csv), Some(a)); + } + + #[test] + fn what_does_not_read() { + let base = "scope,key,impressions,revenue\nchannel,fast-9,,\nstart,0,,\nend,86400000000,,\ncurrency,EUR,,\nbasis,gross,,\nperiod,,5,7\n"; + assert!(read(form::CSV, base).is_some()); + assert!(read(form::CSV, &base.replace("period,,5", "period,,05")).is_none()); + assert!(read(form::CSV, &base.replace("basis,gross", "basis,net")).is_none()); + assert!(read(form::CSV, &format!("{base}day,1970-01-02,1,1\n")).is_none(), "a day past the period"); + assert!(read(form::CSV, &format!("{base}programme,b,1,1\nprogramme,a,1,1\n")).is_none(), "programmes out of order"); + assert!(read(form::CSV, &format!("{base}programme,a,1,1\nday,1970-01-01,1,1\n")).is_none(), "a day after a programme"); + assert!(read(form::CSV, &base.replace("fast-9", "fa\"st")).is_none(), "a bare quote"); + assert!(read(form::CSV, &format!("{base}\n")).is_none(), "an empty record"); + assert!(read(form::JSON, r#"{"channel":"x","start":0,"end":1,"currency":"EUR","basis":"gross","impressions":1.0,"revenue":1}"#).is_none()); + assert!(read(form::JSON, r#"{"channel":"x","start":0,"end":1,"currency":"EUR","basis":"gross","impressions":1,"revenue":1,"note":""}"#).is_none()); + } +} diff --git a/wai/wai-rs/src/opportunity_report/section.rs b/wai/wai-rs/src/opportunity_report/section.rs new file mode 100644 index 000000000..065b4de34 --- /dev/null +++ b/wai/wai-rs/src/opportunity_report/section.rs @@ -0,0 +1,468 @@ +//! The subset of an SCTE 35-1 `splice_info_section()` the opportunity report reads +//! (extensions/opportunity-report.md §5): the CRC, the header, `splice_insert()`, +//! `time_signal()` and `splice_null()`, and the segmentation descriptors of the descriptor loop, +//! for what a section states (§5.2): its id, its duration and its slots. A section is read only as +//! far as this; another command or descriptor is passed over whole. +//! +//! [`write`] builds sections from their fields for the corpus. + +/// The MPEG-2 CRC (ISO/IEC 13818-1 Annex A): polynomial 0x04C11DB7, register preset to all ones, +/// no reflection, no final inversion. +pub fn crc32(b: &[u8]) -> u32 { + let mut c: u32 = 0xFFFF_FFFF; + for &x in b { + c ^= (x as u32) << 24; + for _ in 0..8 { + c = if c & 0x8000_0000 != 0 { (c << 1) ^ 0x04C1_1DB7 } else { c << 1 }; + } + } + c +} + +/// The command types this text reads. +pub mod command { + pub const SPLICE_NULL: u8 = 0x00; + pub const SPLICE_INSERT: u8 = 0x05; + pub const TIME_SIGNAL: u8 = 0x06; +} + +/// The segmentation types whose start makes an out (cue-injection §6.2). +pub const OPENING: [u8; 5] = [0x22, 0x30, 0x32, 0x34, 0x36]; +/// The placement opportunity starts. +pub const PLACEMENT: [u8; 2] = [0x34, 0x36]; +/// The types that may carry the sub-segment fields (SCTE 35-1 Table 20). +pub const SUB_SEGMENTED: [u8; 8] = [0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x44, 0x46]; + +/// A segmentation descriptor, as far as §5.1 reads it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Segmentation { + pub event_id: u32, + pub cancelled: bool, + pub duration: Option, + pub type_id: u8, + /// (`sub_segment_num`, `sub_segments_expected`), when carried. + pub sub: Option<(u8, u8)>, +} + +/// What a section states (§5.2), and what §5.3 holds it to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct States { + /// `splice_command_type`, or `None` for an encrypted section. + pub command: Option, + /// A `splice_insert()`: (its cancel indicator, its `out_of_network_indicator`). + pub insert: Option<(bool, bool)>, + pub id: Option, + pub duration: Option, + pub slots: Option, +} + +impl States { + fn nothing(command: Option) -> States { + States { command, insert: None, id: None, duration: None, slots: None } + } + + /// The command's name, as the outcome states it. + pub fn command_name(&self) -> &'static str { + match self.command { + None => "encrypted", + Some(command::SPLICE_INSERT) => "splice_insert", + Some(command::TIME_SIGNAL) => "time_signal", + Some(command::SPLICE_NULL) => "splice_null", + Some(_) => "other", + } + } +} + +/// A big-endian bit cursor over `b[..end]`; running out is a refusal. +struct Bits<'a> { + b: &'a [u8], + /// The position, in bits. + at: usize, + /// The end, in bytes. + end: usize, +} + +impl Bits<'_> { + fn take(&mut self, n: usize) -> Result { + if self.at + n > self.end * 8 { + return Err(()); + } + let mut v: u64 = 0; + for _ in 0..n { + let bit = (self.b[self.at / 8] >> (7 - self.at % 8)) & 1; + v = (v << 1) | bit as u64; + self.at += 1; + } + Ok(v) + } + fn skip(&mut self, n_bytes: usize) -> Result<(), ()> { + if self.at / 8 + n_bytes > self.end { + return Err(()); + } + self.at += 8 * n_bytes; + Ok(()) + } + fn byte_pos(&self) -> usize { + self.at / 8 + } +} + +/// A `splice_time()`: one byte, or five with `pts_time`. +fn splice_time(r: &mut Bits) -> Result<(), ()> { + if r.take(1)? == 1 { + r.take(6)?; + r.take(33)?; + } else { + r.take(7)?; + } + Ok(()) +} + +/// `splice_insert()`: (cancel, out_of_network, splice_event_id, break duration). +fn splice_insert(r: &mut Bits) -> Result<(bool, bool, u32, Option), ()> { + let id = r.take(32)? as u32; + let cancel = r.take(1)? == 1; + r.take(7)?; + if cancel { + return Ok((true, false, id, None)); + } + let out = r.take(1)? == 1; + let program = r.take(1)? == 1; + let duration_flag = r.take(1)? == 1; + let immediate = r.take(1)? == 1; + r.take(4)?; + if program && !immediate { + splice_time(r)?; + } + if !program { + let n = r.take(8)?; + for _ in 0..n { + r.take(8)?; + if !immediate { + splice_time(r)?; + } + } + } + let duration = if duration_flag { + r.take(1)?; + r.take(6)?; + Some(r.take(33)?) + } else { + None + }; + r.take(16)?; + r.take(8)?; + r.take(8)?; + Ok((false, out, id, duration)) +} + +/// A segmentation descriptor's body after its identifier, filling `[from, to)` exactly. +fn segmentation(b: &[u8], from: usize, to: usize) -> Result { + let mut r = Bits { b, at: from * 8, end: to }; + let event_id = r.take(32)? as u32; + let cancelled = r.take(1)? == 1; + r.take(7)?; + if cancelled { + return if r.byte_pos() == to { Ok(Segmentation { event_id, cancelled, duration: None, type_id: 0, sub: None }) } else { Err(()) }; + } + let program = r.take(1)? == 1; + let duration_flag = r.take(1)? == 1; + r.take(6)?; + if !program { + let n = r.take(8)?; + r.skip(6 * n as usize)?; + } + let duration = if duration_flag { Some(r.take(40)?) } else { None }; + r.take(8)?; + let upid_len = r.take(8)?; + r.skip(upid_len as usize)?; + let type_id = r.take(8)? as u8; + r.take(8)?; + r.take(8)?; + let left = to - r.byte_pos(); + let sub = match left { + 0 => None, + 2 if SUB_SEGMENTED.contains(&type_id) => Some((r.take(8)? as u8, r.take(8)? as u8)), + _ => return Err(()), + }; + Ok(Segmentation { event_id, cancelled, duration, type_id, sub }) +} + +/// Read a section (§5.1) for what it states (§5.2); `Err` is `cue_section_invalid`. +pub fn read(s: &[u8]) -> Result { + let n = s.len(); + if n < 20 { + return Err(()); + } + let crc_at = n - 4; + if crc32(&s[..crc_at]) != u32::from_be_bytes(s[crc_at..].try_into().expect("4 bytes")) { + return Err(()); + } + if s[3] != 0 { + return Err(()); + } + if s[4] >> 7 == 1 { + return Ok(States::nothing(None)); + } + let cmd_len = (((s[11] & 0x0F) as usize) << 8) | s[12] as usize; + let cmd = s[13]; + let readable = matches!(cmd, command::SPLICE_INSERT | command::TIME_SIGNAL | command::SPLICE_NULL); + if cmd_len == 0xFFF && !readable { + return Ok(States::nothing(Some(cmd))); + } + let end = if cmd_len == 0xFFF { crc_at } else { 14 + cmd_len }; + if end > crc_at { + return Err(()); + } + let mut r = Bits { b: s, at: 14 * 8, end }; + let mut st = States::nothing(Some(cmd)); + match cmd { + command::SPLICE_INSERT => { + let (cancel, out, id, duration) = splice_insert(&mut r)?; + st.insert = Some((cancel, out)); + st.id = Some(id); + st.duration = duration; + } + command::TIME_SIGNAL => splice_time(&mut r)?, + command::SPLICE_NULL => {} + _ => r.at = end * 8, + } + let cmd_end = r.byte_pos(); + if cmd_len != 0xFFF && cmd_end != end { + return Err(()); + } + if cmd_end + 2 > crc_at { + return Err(()); + } + let loop_len = u16::from_be_bytes([s[cmd_end], s[cmd_end + 1]]) as usize; + let mut p = cmd_end + 2; + if p + loop_len != crc_at { + return Err(()); + } + let mut segs = Vec::new(); + while p < crc_at { + if p + 2 > crc_at { + return Err(()); + } + let (tag, len) = (s[p], s[p + 1] as usize); + let body = p + 2; + if body + len > crc_at { + return Err(()); + } + if tag == 0x02 && len >= 4 && &s[body..body + 4] == b"CUEI" { + segs.push(segmentation(s, body + 4, body + len)?); + } + p = body + len; + } + let live = |x: &&Segmentation| !x.cancelled; + if cmd == command::TIME_SIGNAL + && let Some(o) = segs.iter().filter(live).find(|x| OPENING.contains(&x.type_id)) + { + st.id = Some(o.event_id); + st.duration = o.duration; + } + st.slots = segs.iter().filter(live).find(|x| PLACEMENT.contains(&x.type_id)).and_then(|x| x.sub).map(|(_, e)| e as u64).filter(|e| *e > 0); + Ok(st) +} + +// --------------------------------------------------------------------------------------- +// Writing, for the corpus +// --------------------------------------------------------------------------------------- + +/// A big-endian bit writer. +#[derive(Default)] +struct Out { + b: Vec, + bits: usize, +} + +impl Out { + fn put(&mut self, n: usize, v: u64) { + for i in (0..n).rev() { + if self.bits % 8 == 0 { + self.b.push(0); + } + let bit = ((v >> i) & 1) as u8; + let last = self.b.len() - 1; + self.b[last] |= bit << (7 - self.bits % 8); + self.bits += 1; + } + } +} + +/// A segmentation descriptor to write. +#[derive(Clone, Debug)] +pub struct SegmentationSpec { + pub event_id: u32, + pub cancelled: bool, + /// In 90 kHz ticks. + pub duration: Option, + pub type_id: u8, + pub sub: Option<(u8, u8)>, +} + +/// A command to write. +#[derive(Clone, Debug)] +pub enum Command { + /// `splice_insert()`: id, cancel, out_of_network, and the break duration in ticks. + Insert { id: u32, cancel: bool, out: bool, duration: Option }, + /// `time_signal()` at a `pts_time`. + TimeSignal { pts: u64 }, +} + +fn segmentation_bytes(d: &SegmentationSpec) -> Vec { + let mut o = Out::default(); + o.put(32, u32::from_be_bytes(*b"CUEI") as u64); + o.put(32, d.event_id as u64); + o.put(1, d.cancelled as u64); + o.put(1, 1); + o.put(6, 0x3F); + if !d.cancelled { + o.put(1, 1); // program_segmentation_flag + o.put(1, d.duration.is_some() as u64); + o.put(1, 1); // delivery_not_restricted_flag + o.put(5, 0x1F); + if let Some(t) = d.duration { + o.put(40, t); + } + o.put(8, 0); // segmentation_upid_type: not used + o.put(8, 0); // segmentation_upid_length + o.put(8, d.type_id as u64); + o.put(8, 1); // segment_num + o.put(8, 1); // segments_expected + if let Some((n, e)) = d.sub { + o.put(8, n as u64); + o.put(8, e as u64); + } + } + let mut v = vec![0x02, o.b.len() as u8]; + v.extend(o.b); + v +} + +/// A section of one command and its segmentation descriptors, with its CRC. +pub fn write(cmd: &Command, descriptors: &[SegmentationSpec]) -> Vec { + let mut c = Out::default(); + let ty = match cmd { + Command::Insert { id, cancel, out, duration } => { + c.put(32, *id as u64); + c.put(1, *cancel as u64); + c.put(7, 0x7F); + if !cancel { + c.put(1, *out as u64); + c.put(1, 1); // program_splice_flag + c.put(1, duration.is_some() as u64); + c.put(1, 1); // splice_immediate_flag + c.put(1, 1); // event_id_compliance_flag + c.put(3, 0x7); + if let Some(t) = duration { + c.put(1, 1); + c.put(6, 0x3F); + c.put(33, *t); + } + c.put(16, 1); // unique_program_id + c.put(8, 0); // avail_num + c.put(8, 0); // avails_expected + } + command::SPLICE_INSERT + } + Command::TimeSignal { pts } => { + c.put(1, 1); + c.put(6, 0x3F); + c.put(33, *pts); + command::TIME_SIGNAL + } + }; + let mut loop_ = Vec::new(); + for d in descriptors { + loop_.extend(segmentation_bytes(d)); + } + let mut body = Out::default(); + body.put(8, 0); // protocol_version + body.put(1, 0); // encrypted_packet + body.put(6, 0); // encryption_algorithm + body.put(33, 0); // pts_adjustment + body.put(8, 0xFF); // cw_index + body.put(12, 0xFFF); // tier + body.put(12, c.b.len() as u64); + body.put(8, ty as u64); + let mut rest = body.b; + rest.extend(&c.b); + rest.extend((loop_.len() as u16).to_be_bytes()); + rest.extend(loop_); + let section_length = rest.len() + 4; + let mut s = vec![0xFC, 0x30 | ((section_length >> 8) as u8 & 0x0F), section_length as u8]; + s.extend(rest); + let crc = crc32(&s); + s.extend(crc.to_be_bytes()); + s +} + +#[cfg(test)] +mod tests { + use super::*; + + /// ANSI/SCTE 35-1 §14.1's sample, as cue-injection §5.7 holds it: a `time_signal()` with a + /// Provider Placement Opportunity Start of 307 s. + const SAMPLE: &str = "fc3034000000000000fffff00506fe72bd0050001e021c435545494800008e7fcf0001a599b00808000000002ca0a18a3402009ac9d17e"; + + fn unhex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + + #[test] + fn the_crc_of_the_standards_sample() { + let s = unhex(SAMPLE); + let n = s.len(); + assert_eq!(crc32(&s[..n - 4]), u32::from_be_bytes(s[n - 4..].try_into().unwrap())); + } + + #[test] + fn the_standards_sample_states_its_opportunity() { + let st = read(&unhex(SAMPLE)).expect("the sample reads"); + assert_eq!(st.command, Some(command::TIME_SIGNAL)); + assert_eq!(st.id, Some(0x4800_008e)); + // 307 s at 90 kHz. + assert_eq!(st.duration, Some(307 * 90_000)); + // Its descriptor (28 bytes) ends at `segments_expected`: it carries no sub-segment + // fields, so it states no slots. + assert_eq!(st.slots, None); + } + + #[test] + fn written_sections_read_back() { + let s = write(&Command::Insert { id: 7, cancel: false, out: true, duration: Some(5_400_000) }, &[]); + let st = read(&s).unwrap(); + assert_eq!((st.id, st.duration, st.slots, st.insert), (Some(7), Some(5_400_000), None, Some((false, true)))); + let d = SegmentationSpec { event_id: 9, cancelled: false, duration: Some(8_100_000), type_id: 0x34, sub: Some((1, 3)) }; + let s = write(&Command::TimeSignal { pts: 12_345 }, &[d]); + let st = read(&s).unwrap(); + assert_eq!((st.id, st.duration, st.slots), (Some(9), Some(8_100_000), Some(3))); + } + + #[test] + fn a_cancelled_descriptor_states_nothing() { + let c = SegmentationSpec { event_id: 3, cancelled: true, duration: None, type_id: 0, sub: None }; + let d = SegmentationSpec { event_id: 4, cancelled: false, duration: None, type_id: 0x36, sub: Some((1, 2)) }; + let st = read(&write(&Command::TimeSignal { pts: 0 }, &[c, d])).unwrap(); + assert_eq!((st.id, st.slots), (Some(4), Some(2))); + } + + #[test] + fn a_damaged_section_is_refused() { + let s = write(&Command::TimeSignal { pts: 1 }, &[]); + let mut crc = s.clone(); + let n = crc.len(); + crc[n - 1] ^= 1; + assert!(read(&crc).is_err()); + assert!(read(&s[..19]).is_err()); + // A descriptor loop that runs past the CRC, the CRC made good again. + let mut long = s.clone(); + let at = long.len() - 6; + long[at + 1] = 9; + let n = long.len(); + let c = crc32(&long[..n - 4]); + long[n - 4..].copy_from_slice(&c.to_be_bytes()); + assert!(read(&long).is_err()); + } +} diff --git a/wai/wai-rs/src/revenue_share.rs b/wai/wai-rs/src/revenue_share.rs index c0b9e7b57..037b54c24 100644 --- a/wai/wai-rs/src/revenue_share.rs +++ b/wai/wai-rs/src/revenue_share.rs @@ -906,8 +906,9 @@ pub struct Evidence { pub impressions: Vec, } -/// §8.2 step 4: the runs. -fn the_runs(c: &Contract, runs: &[Value]) -> Result, Refusal> { +/// §8.2 step 4: the runs. The opportunity report (extensions/opportunity-report.md §3.2) reads +/// runs as this step does. +pub(crate) fn the_runs(c: &Contract, runs: &[Value]) -> Result, Refusal> { let mut out: Vec = Vec::with_capacity(runs.len()); for (k, v) in runs.iter().enumerate() { let at = || json!(["run", k]); @@ -958,8 +959,9 @@ fn the_ledgers(c: &Contract, start: u64, end: u64, docs: &[Value]) -> Result Option<&str> { +/// The asset an instant's entry gives (§6.2): a programme's own, or by the rule. The opportunity +/// report attributes a break by it (extensions/opportunity-report.md §6). +pub(crate) fn asset_at(schedule: &[crate::wai_video_channel::ScheduleEntry], e: usize, rule_: u8) -> Option<&str> { let programme = |x: &&crate::wai_video_channel::ScheduleEntry| x.kind == EntryKind::Program; if schedule[e].kind == EntryKind::Program { return Some(&schedule[e].asset_id); diff --git a/wai/wai-rs/tests/opportunity_report.rs b/wai/wai-rs/tests/opportunity_report.rs new file mode 100644 index 000000000..c0a9faf80 --- /dev/null +++ b/wai/wai-rs/tests/opportunity_report.rs @@ -0,0 +1,173 @@ +//! `wai.delivery.opportunity` (extensions/opportunity-report.md): the committed corpus +//! `live-conformance/opportunity/vectors.json` regenerates byte for byte from its seeds and is the +//! only file there besides its README; every vector gets the outcome it states; every reason the +//! extension adds and every code it reuses refuses a vector; §13 lists exactly the reasons the +//! reference reports; and the worked example of §14: a channel's day of two owners, signed cues and +//! program-as-run, a reported statement that under-reports one owner's breaks, and the check that +//! shows it, computed and then recomputed from the public records alone. +#![cfg(feature = "opportunity_report")] + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; + +use serde_json::{Value, json}; +use wai::opportunity_report::{self as opp, Reason, corpus, scope}; + +fn wai_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") +} + +fn read(p: &str) -> String { + std::fs::read_to_string(wai_dir().join(p)).unwrap_or_else(|e| panic!("{p}: {e}")) +} + +fn committed() -> String { + read("live-conformance/opportunity/vectors.json") +} + +fn doc() -> Value { + serde_json::from_str(&committed()).expect("the corpus is JSON") +} + +#[test] +fn the_committed_corpus_regenerates_from_its_seeds() { + let generated = corpus::generate().expect("the corpus generates"); + assert!(committed() == generated, "live-conformance/opportunity/vectors.json differs from `wai_opportunity_vectors gen`"); + let mut files: Vec = std::fs::read_dir(wai_dir().join("live-conformance/opportunity")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect(); + files.sort(); + assert_eq!(files, ["README.md", "vectors.json"]); +} + +#[test] +fn every_vector_gets_its_stated_outcome() { + let fails = corpus::verify(&committed()); + assert!(fails.is_empty(), "{}", fails.join("\n")); +} + +/// Every reason this extension adds and every code it reuses refuses a vector, and the texts it +/// delegates to refuse one each at their own places. +#[test] +fn the_corpus_covers_every_reason_and_case() { + let d = doc(); + let vectors = d["vectors"].as_array().unwrap(); + let seen: BTreeSet<&str> = vectors.iter().filter_map(|v| v["outcome"]["reason"].as_str()).collect(); + for r in Reason::ALL { + assert!(seen.contains(r.code()), "nothing in the corpus is refused {r}"); + } + for r in Reason::REUSED { + assert!(seen.contains(r), "nothing in the corpus is refused {r}"); + } + let get = |n: &str| vectors.iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone(); + assert_eq!(get("contract_offered")["reason"], "not_countersigned"); + assert_eq!(get("run_aired_misstated")["at"], json!(["run", 0])); + assert_eq!(get("room_event_signature")["at"].as_array().unwrap()[..3], [json!("room"), json!(0), json!("room")]); + assert_eq!(get("check_report_refused")["at"], json!(["report", "report"])); + assert_eq!(get("check_statement_refused")["at"], json!(["statement", "statement"])); + for n in ["owner_a_day", "owner_b_day", "operator_day", "owner_a_runs_alone", "owner_b_two_rooms", "check_owner_b_under_reported", "check_owner_a_csv", "check_owner_b_without_statement", "check_owner_b_reports_a_programme"] { + assert_eq!(get(n)["outcome"], "accept", "{n}"); + } +} + +/// §13's table names exactly the codes the reference adds, in its order. +#[test] +fn section_13_lists_exactly_the_reference_codes() { + let text = read("extensions/opportunity-report.md"); + let at = text.find("\n## 13. Reasons").expect("§13"); + let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e); + let listed: Vec<&str> = text[at..end].lines().filter_map(|l| l.strip_prefix("| `")).filter_map(|l| l.split('`').next()).collect(); + let codes: Vec<&str> = Reason::ALL.iter().map(|r| r.code()).collect(); + assert_eq!(listed, codes); +} + +fn line<'a>(o: &'a Value, scope_: &str, key: &str) -> &'a Value { + o["lines"].as_array().unwrap().iter().find(|l| l["scope"] == scope_ && l["key"] == key).unwrap_or_else(|| panic!("no line {scope_} {key}")) +} + +/// The worked example (§14). Owner B computes and signs its report of the day from the channel's +/// two windows and the room's cues; its platform reports B's impressions per programme, and all +/// but leaves out B's second documentary; B checks the report against it and against the +/// operator's signed statement of the day. A party holding only the public records receives the +/// check's document as text, verifies it, and recomputes the opportunities of every line from +/// the attributed breaks and the contract alone. +#[test] +fn the_worked_example_shows_the_under_report_from_the_public_records() { + let w = corpus::world().expect("the world"); + let b = &w.reports["owner_b"]; + // B's report, computed from the documents without it, is the corpus's, byte for byte. + let (rep, report_doc) = opp::create(&w.pools.report_document(b, false), &corpus::key("owner-b"), corpus::pk("owner-b"), corpus::DAY16, corpus::DAY17).expect("B's report"); + assert_eq!(rep.to_bytes(), b.report); + // Four cued breaks in B's documentaries: 360 s, twelve slots (four, two, four, two). + assert_eq!((rep.breaks, rep.break_ms, rep.slots), (4, 360_000, 12)); + // The check of the platform's figures, beside the signed statement. + let inputs = w.pools.check_document(b, "b_programmes", Some(&w.statements["owner_b"]), None); + let (_, document) = opp::create_check(&inputs, &corpus::key("owner-b"), opp::FLOOR_PPM, opp::CEILING_PPM).expect("the check"); + + // A party holding only the public records: the document, as text. + let text = serde_json::to_string(&document).unwrap(); + let got: Value = serde_json::from_str(&text).unwrap(); + let a = opp::verify_check(&got).expect("the check verifies"); + let o = opp::check_json(&a); + let period = line(&o, "period", ""); + // Fourteen impressions reported against twenty-four the signed records prove, and revenue + // below B's signed part. + assert_eq!((period["impressions"].as_u64(), period["signed"]["impressions"].as_u64()), (Some(14), Some(24))); + assert_eq!(period["fill_ppm"], 1_166_666); + assert_eq!(period["flags"], json!(["impressions_below_signed", "revenue_below_signed"])); + // The first documentary is in step with the day; the second, as many slots and two + // impressions, falls far below it. + let doc_1 = line(&o, "programme", "b/doc-1"); + let doc_2 = line(&o, "programme", "b/doc-2"); + assert_eq!((doc_1["slots"].as_u64(), doc_1["flags"].clone()), (Some(6), json!([]))); + assert_eq!((doc_2["slots"].as_u64(), doc_2["fill_ppm"].as_u64(), doc_2["flags"].clone()), (Some(6), Some(333_333), json!(["below"]))); + + // Recomputed from the report's attributed breaks and the contract: each programme's breaks, + // seconds and slots, over the breaks of the splits B shares in. + let report = opp::verify(&report_doc).expect("the report verifies"); + let r = opp::accepted_json(&report); + let b_splits: BTreeSet = r["lines"].as_array().unwrap().iter().map(|l| l["split"].as_u64().unwrap()).collect(); + let mut per: BTreeMap = BTreeMap::new(); + for x in r["breaks"].as_array().unwrap() { + if !b_splits.contains(&x["split"].as_u64().unwrap()) { + continue; + } + let e = per.entry(x["asset"].as_str().unwrap_or("").to_string()).or_default(); + e.0 += 1; + e.1 += x["duration_ms"].as_u64().unwrap(); + e.2 += x["slots"].as_u64().unwrap_or(0); + } + for p in ["b/doc-1", "b/doc-2"] { + let l = line(&o, "programme", p); + assert_eq!(per[p], (l["breaks"].as_u64().unwrap(), l["break_ms"].as_u64().unwrap(), l["slots"].as_u64().unwrap()), "{p}"); + } + // The signed figures, from the statement's own lines. + let st = &a.statement.as_ref().expect("the statement").statement; + assert_eq!(st.lines.iter().map(|l| l.impressions).sum::(), 24); + assert_eq!(period["signed"]["revenue"].as_u64(), Some(st.total)); + // The check's lines are the only figures that change with the reported statement: the same + // report and statement beside owner A's fair CSV flag nothing. + let d = doc(); + let v = d["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "check_owner_a_csv").expect("A's check vector"); + let fair = opp::verify_check(&corpus::document(&d, v).expect("it reads")).expect("A's check"); + assert!(fair.check.lines.iter().all(|l| l.flags == 0), "A's reported statement is what its records prove"); + assert_eq!(fair.check.lines[0].scope, scope::PERIOD); +} + +/// Owner A's day holds every kind of break §4 makes: a break inside the ad slot after A's first +/// episode counts as cued and the slot is not counted again; the slot no cue covers is scheduled +/// and unslotted; slots come from a section or from the policy. +#[test] +fn owner_a_holds_every_kind_of_break() { + let w = corpus::world().expect("the world"); + let a = opp::verify(&w.pools.report_document(&w.reports["owner_a"], true)).expect("A's report"); + let mine: Vec<&opp::Break> = a.counted().collect(); + let cued: Vec<&&opp::Break> = mine.iter().filter(|b| b.class == opp::class::CUED).collect(); + let scheduled: Vec<&&opp::Break> = mine.iter().filter(|b| b.class == opp::class::SCHEDULED).collect(); + assert_eq!(cued.len(), 4); + assert_eq!(scheduled.len(), 1); + assert_eq!((scheduled[0].slots, scheduled[0].asset.as_deref()), (None, Some("a/ep-2")), "the uncued ad slot, by the rule preceding"); + let sources: Vec> = cued.iter().map(|b| b.slot_source).collect(); + assert_eq!(sources, [Some("policy"), Some("section"), Some("section"), Some("section")]); + assert_eq!(cued[2].asset.as_deref(), Some("a/ep-1"), "the break inside the ad slot, by the rule preceding"); + let runs_alone = opp::verify(&w.pools.report_document(&w.reports["owner_a_runs_alone"], true)).expect("from the runs alone"); + assert_eq!(runs_alone.counted().filter(|b| b.class == opp::class::SCHEDULED).count(), 2, "with no room, both ad slots are scheduled"); +} -- 2.54.0 (Apple Git-157) From 18e38db5cb3d78aa2aefc84df45d6ced4eb756be Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 29 Oct 2026 18:19:39 -0400 Subject: [PATCH 2/5] wai: opportunity_report_verify.py, the opportunity report's second implementation Written from extensions/opportunity-report.md and sharing no code with wai-rs: it reads every report, check, SCTE 35 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, requires every mutated copy of the corpus to fail and drops each of its own checks in turn. The corpus gains the vectors those checks need: a run of another channel, runs overlapping, an overlapping contract, a report of the first hour alone, and the first hour split across two windows that both air its ad slot. Co-Authored-By: Claude Opus 5.5 --- wai/live-conformance/opportunity/vectors.json | 608 +++++++++ .../opportunity_report_verify.py | 1102 +++++++++++++++++ wai/wai-rs/src/opportunity_report/corpus.rs | 34 +- 3 files changed, 1736 insertions(+), 8 deletions(-) create mode 100644 wai/live-conformance/opportunity_report_verify.py diff --git a/wai/live-conformance/opportunity/vectors.json b/wai/live-conformance/opportunity/vectors.json index e1eff5f2e..d82977666 100644 --- a/wai/live-conformance/opportunity/vectors.json +++ b/wai/live-conformance/opportunity/vectors.json @@ -13,9 +13,54 @@ "contracts": { "main": "5752433101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd00a0cd18bc5c06000040e2b52b5f06004555520600666173742d392825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972010200010001ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c70250e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a972e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7603002b2c0a00010015160500801a06000101c027090000000000020020a107000120a10700040000000600612f65702d3100000600612f65702d3200000700622f646f632d3101000700622f646f632d320100f97abb9f0e59098e4321c556fd094f1a426a7936ecfa8a44ac17f62a9569d99dd44c64bc5522f10c794ad1da659f463844d0e13c3f4172be887a5a1f698d160266c30b368b5cdc7d60df370f2181b7937e96f7ac783277fd944a2b643a75d58d2f017b333e619553fe46f889aff345c5d2eb067399e3320c0cbb8de84bc4ac00a21cc105df9d1457ac788e6d897a18e8da473939a78973aa55033fbbedf4078a985032d46620ad4c4888b64a23c65a9d855940458a6ff8d776785b807bf19002", "offered": "5752433101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd00a0cd18bc5c06000040e2b52b5f06004555520600666173742d392825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972010200010001ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c70250e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a972e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7603002b2c0a00010015160500801a06000101c027090000000000020020a107000120a10700040000000600612f65702d3100000600612f65702d3200000700622f646f632d3101000700622f646f632d320100f97abb9f0e59098e4321c556fd094f1a426a7936ecfa8a44ac17f62a9569d99dd44c64bc5522f10c794ad1da659f463844d0e13c3f4172be887a5a1f698d160266c30b368b5cdc7d60df370f2181b7937e96f7ac783277fd944a2b643a75d58d2f017b333e619553fe46f889aff345c5d2eb067399e3320c0cbb8de84bc4ac0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "overlapping": "5752433101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd0020a16d4e5e060000c0b50abe6006004555520600666173742d392825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972010200010001ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c70250e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a972e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7603002b2c0a00010015160500801a06000101c027090000000000020020a107000120a10700040000000600612f65702d3100000600612f65702d3200000700622f646f632d3101000700622f646f632d320100debd2d943b0d328ce5dd16b3429e9c5da8ac058f7b56fc84902a36132e132a7b11928dfe25cfc59ab6d459df3782208f34820e1a3b03d016f3de990cf3bc4404b882309b8b2789e2afef2d9c289e60bc78246884abdd4ba035a7b5cf082103b998f5cb0c09523206ef63a36375117f2d0eedbd7ae142ace13529aab2771b74056bde8c2a49159f3c58b452c29ec195df06eb13ddeb6fb6a909586bf1f54f46db9144aa4c86434357d20404b7289c8b5a1d6394d039e569aa28a9a766d07ed908", "two_rooms": "5752433101c877eed124620b99473412566e9172e9f0096f982c9bd0b3dc2ad218598b73fd00a0cd18bc5c06000040e2b52b5f06004555520600666173742d392825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f97201020002000115bd1b3c08e8dffba12bf2fa969c73098eed234c9118f2d5dc5dca2cdfd88c2101ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c70250e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a972e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7603002b2c0a00010015160500801a06000101c027090000000000020020a107000120a10700040000000600612f65702d3100000600612f65702d3200000700622f646f632d3101000700622f646f632d320100ccd9527d72ba978dc645ca188d50904b6ba6de991bafe4f2eec7ac6308e70bcdf3220ab06d58fcce694bcd8201a12c3b821c2729f6316902afd01dcaa6f1b40a9e90684b04af4cfee06d4d8f3d924ffd2113ddda0f443b7b81381fc220dd8912ff409d5b015674c0dc9a3ba629e1114a043eee3d9568181fda31254a5f60e80bb27549feb7b1542217fb9f3ac8afd7f121f42cdb668e5c026291a419ffbef91d74583e6239a4a02d6d40d2db43fd54999c5c8801250ba1bdb5aa77f99a24ee0f" }, "runs": { + "fast_8": { + "channel_id": "fast-8", + "schedule_root": "454671e80ae1720fa153767d918c74478fc6f1dace0f556f17de9b0bfa6f8a82", + "schedule": [ + { + "asset_id": "a/ep-1", + "start_ms": 1792108800000, + "duration_ms": 1800000, + "kind": "program" + }, + { + "asset_id": "slate-90", + "start_ms": 1792110600000, + "duration_ms": 90000, + "kind": "ad" + }, + { + "asset_id": "b/doc-1", + "start_ms": 1792110690000, + "duration_ms": 1710000, + "kind": "program" + } + ], + "window_start_ms": 1792108800000, + "window_end_ms": 1792112400000, + "aired": [ + 0, + 1, + 2 + ], + "work_schedule_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "capability": "video.channel", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": null + }, + "parent_receipt_hash": null, + "signer_pubkey": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "signer_id": "did:key:channel-signer", + "sig": "74e895135f0cc37ffc2e62daa8b6987f7008c72f3aaa61c966af961fe02176ed5999a6fddf60bba5ae784c9d422e2b9684d8b8a22cbd00c885c2c0367a0f1509" + }, "hour_1": { "channel_id": "fast-9", "schedule_root": "454671e80ae1720fa153767d918c74478fc6f1dace0f556f17de9b0bfa6f8a82", @@ -102,6 +147,134 @@ "signer_id": "did:key:channel-signer", "sig": "e16381690e0aa7f595645b2852900f851197019b6bd70e07081776f37601bc554a39e3dd85321110af2cfae7cca3b676d6fa581c1cdf3aa7274b14106a8f5b0a" }, + "hour_1_from_1850_s": { + "channel_id": "fast-9", + "schedule_root": "454671e80ae1720fa153767d918c74478fc6f1dace0f556f17de9b0bfa6f8a82", + "schedule": [ + { + "asset_id": "a/ep-1", + "start_ms": 1792108800000, + "duration_ms": 1800000, + "kind": "program" + }, + { + "asset_id": "slate-90", + "start_ms": 1792110600000, + "duration_ms": 90000, + "kind": "ad" + }, + { + "asset_id": "b/doc-1", + "start_ms": 1792110690000, + "duration_ms": 1710000, + "kind": "program" + } + ], + "window_start_ms": 1792110650000, + "window_end_ms": 1792112400000, + "aired": [ + 1, + 2 + ], + "work_schedule_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "capability": "video.channel", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": null + }, + "parent_receipt_hash": "c3e2e0ff297b201923adcbe09a82b45f4c0e7b02475747309352044a58257c42", + "signer_pubkey": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "signer_id": "did:key:channel-signer", + "sig": "42c79e75eec73b5d36f9638c815b41bb8ad685a89fed5bdf76ede58c6a90269a2cc2f41c73d9acd41d7852c688ed5fc37c596ca0cab91a1a11f3c0f8112d3208" + }, + "hour_1_overlapping": { + "channel_id": "fast-9", + "schedule_root": "454671e80ae1720fa153767d918c74478fc6f1dace0f556f17de9b0bfa6f8a82", + "schedule": [ + { + "asset_id": "a/ep-1", + "start_ms": 1792108800000, + "duration_ms": 1800000, + "kind": "program" + }, + { + "asset_id": "slate-90", + "start_ms": 1792110600000, + "duration_ms": 90000, + "kind": "ad" + }, + { + "asset_id": "b/doc-1", + "start_ms": 1792110690000, + "duration_ms": 1710000, + "kind": "program" + } + ], + "window_start_ms": 1792111800000, + "window_end_ms": 1792112800000, + "aired": [ + 2 + ], + "work_schedule_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "capability": "video.channel", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": null + }, + "parent_receipt_hash": null, + "signer_pubkey": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "signer_id": "did:key:channel-signer", + "sig": "49745c0c3ca4e6d99f43d0de947c01276de520c29ca4bf21e413213904cf24b5e30ae92094bdd65e7c6961c1e1c2e69e3612a9639e41ee9af5dbbe8fbd43740b" + }, + "hour_1_to_1850_s": { + "channel_id": "fast-9", + "schedule_root": "454671e80ae1720fa153767d918c74478fc6f1dace0f556f17de9b0bfa6f8a82", + "schedule": [ + { + "asset_id": "a/ep-1", + "start_ms": 1792108800000, + "duration_ms": 1800000, + "kind": "program" + }, + { + "asset_id": "slate-90", + "start_ms": 1792110600000, + "duration_ms": 90000, + "kind": "ad" + }, + { + "asset_id": "b/doc-1", + "start_ms": 1792110690000, + "duration_ms": 1710000, + "kind": "program" + } + ], + "window_start_ms": 1792108800000, + "window_end_ms": 1792110650000, + "aired": [ + 0, + 1 + ], + "work_schedule_ops": 3, + "joules_micro": 0, + "energy_provenance": null, + "grant": { + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "capability": "video.channel", + "joule_ceiling_micro": 18446744073709551615, + "funds_ceiling": null + }, + "parent_receipt_hash": null, + "signer_pubkey": "2825712d541b726f62f8d2c070ca11121adc6d33fed2a2c210b7c77b2627f972", + "signer_id": "did:key:channel-signer", + "sig": "4d229661db722815a7235e5945797f13cd56dc14cd7e561f6f3681333ba40333ea27ce19603b3e184046cfea38df7d0da3fcc61d85e7f91f132a754e4aff210d" + }, "hour_2": { "channel_id": "fast-9", "schedule_root": "962dff9c86732707b10ab4965bb6e8670c7f32ad0359db65604f5c9370ed3ff6", @@ -3323,6 +3496,363 @@ } } }, + { + "name": "owner_a_runs_split", + "about": "owner A's day from the runs alone, the first hour in two windows that both air its ad slot: one break, taken in the earlier window", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310181640fe9c7b63a1babed05713ed2f9958aa011b8d31edcf9c611f3751386873735f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d0600000003006389072934f5419fe7f657d016029112f909b6ca9c3138f43b493abd4fe40e61c3e2e0ff297b201923adcbe09a82b45f4c0e7b02475747309352044a58257c42fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b01000000020200000000000000f04902000000000000000000000000000200000000000000151605000200000058e8b91ff64fc996d0445b6ad6e598fafc0ef1d5e9f7a66860fd4f18f6b913d20200000000000000f04902000000000000000000000000008f6271f8bae88d9a55e303d9ca569e8e33a52d6e7425843dafcddd37cba23f22e377a85a7426524b713784d5a00c7e762ac10bf1f283eefd06d6ae575d6cc205", + "runs": [ + "hour_1_to_1850_s", + "hour_1_from_1850_s", + "hour_2" + ], + "rooms": [] + }, + "outcome": { + "outcome": "accept", + "report": "f9dcef4a0e9af03e5d5c7a8f03a483f8a76d3b278aaf93c6560d542e591354c3", + "contract": "35f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d", + "issuer": "81640fe9c7b63a1babed05713ed2f9958aa011b8d31edcf9c611f37513868737", + "party": "50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a9", + "role": "owner", + "start": 1792108800000000, + "end": 1792195200000000, + "rooms": [], + "runs": [ + "6389072934f5419fe7f657d016029112f909b6ca9c3138f43b493abd4fe40e61", + "c3e2e0ff297b201923adcbe09a82b45f4c0e7b02475747309352044a58257c42", + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + "breaks": [ + { + "class": "scheduled", + "room": null, + "index": null, + "taken": { + "run": 0, + "entry": 1 + }, + "cue": null, + "start": 1792110600000000, + "duration_ms": 90000, + "open": false, + "slots": null, + "slots_from": null, + "section": null, + "run": "c3e2e0ff297b201923adcbe09a82b45f4c0e7b02475747309352044a58257c42", + "entry": 1, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "c3e2e0ff297b201923adcbe09a82b45f4c0e7b02475747309352044a58257c42" + ] + ] + }, + { + "class": "scheduled", + "room": null, + "index": null, + "taken": { + "run": 2, + "entry": 1 + }, + "cue": null, + "start": 1792114140000000, + "duration_ms": 60000, + "open": false, + "slots": null, + "slots_from": null, + "section": null, + "run": "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b", + "entry": 1, + "asset": "a/ep-2", + "split": 0, + "cites": [ + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "lines": [ + { + "split": 0, + "class": "scheduled", + "breaks": 2, + "break_ms": 150000, + "slots": 0, + "unslotted": 2, + "ppm": 333333, + "cites": [ + [ + 10, + "c3e2e0ff297b201923adcbe09a82b45f4c0e7b02475747309352044a58257c42" + ], + [ + 10, + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ] + ] + } + ], + "totals": { + "breaks": 2, + "break_ms": 150000, + "slots": 0 + } + } + }, + { + "name": "owner_b_first_hour", + "about": "owner B's first hour: the breaks of the second hour are another period's", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310172e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7635f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e7600406cd8e95d060000e4ffaeea5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0100010001020000000000000020bf02000000000006000000000000000000000000000000c0270900050000005fe315388dba168d32c0ba8a4fcee6977bc8c1bd9ab0426252a7be09b9582c28020000000000000020bf02000000000006000000000000001f68b48792d99a78edcec3224aaf8dc7594f769ac6731d6807c04e9f3db4c666181d89938284e2a44a1b4b97b51b49b1b9d3b8eb4b24ee6c76c45f4df351d70a", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "accept", + "report": "9686d96e289ed61b42e6a3106751ad2c5c38738995637cbd155cfb9f5a3636db", + "contract": "35f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d", + "issuer": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "party": "72e1f3d8bbd19fa97aa01b5f5c795cfede961b22e046c5e207783e9f1b129e76", + "role": "owner", + "start": 1792108800000000, + "end": 1792112400000000, + "rooms": [ + "ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c7" + ], + "runs": [ + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "fdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b" + ], + "breaks": [ + { + "class": "cued", + "room": 0, + "index": 0, + "taken": null, + "cue": "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e", + "start": 1792109400000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": { + "command": "splice_insert", + "id": 1, + "duration": 5400000, + "slots": null + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "3f5ab874b6ab30b17ab09c5418738465d85534943885204d6eae6b706a92837e" + ], + [ + 12, + "8f8c0425587436e20a0dccb1b82c1756651ba5ba6fee9ef19becb7d545d36569" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 1, + "taken": null, + "cue": "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253", + "start": 1792110000000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 2, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 0, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "b59303dc5ff953bc14e120bf5ce39101905d1b8845b0f2dc6a89a69f7d2a5253" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 2, + "taken": null, + "cue": "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066", + "start": 1792110600000000, + "duration_ms": 90000, + "open": false, + "slots": 3, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 3, + "duration": 8100000, + "slots": 3 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 1, + "asset": "a/ep-1", + "split": 0, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "5ef070fa1d1aa64660ef38ae640def1b6df34c8763f8d50db48a46e0dec66066" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 3, + "taken": null, + "cue": "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94", + "start": 1792111200000000, + "duration_ms": 120000, + "open": false, + "slots": 4, + "slots_from": "section", + "section": { + "command": "time_signal", + "id": 4, + "duration": 10800000, + "slots": 4 + }, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ] + ] + }, + { + "class": "cued", + "room": 0, + "index": 4, + "taken": null, + "cue": "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a", + "start": 1792111800000000, + "duration_ms": 60000, + "open": false, + "slots": 2, + "slots_from": "policy", + "section": null, + "run": "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e", + "entry": 2, + "asset": "b/doc-1", + "split": 1, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + } + ], + "lines": [ + { + "split": 1, + "class": "cued", + "breaks": 2, + "break_ms": 180000, + "slots": 6, + "unslotted": 0, + "ppm": 600000, + "cites": [ + [ + 10, + "0499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13e" + ], + [ + 12, + "55a9a38df0f3c6873f89b48a05e7b4db750e698cad35049ea31f0fd436d4016a" + ], + [ + 12, + "76819c1bae0fe48eea3b1aa0bea79f1a91800ca48b7266c634e0924916485b94" + ], + [ + 12, + "89adab1875539315934024864137ef3354839e876c204027a254eef806417f49" + ], + [ + 13, + "1bcc1fe7b2fde4cbeb128531859ec1523fa80399d10fabb5ce937488125c5912" + ] + ] + } + ], + "totals": { + "breaks": 2, + "break_ms": 180000, + "slots": 6 + } + } + }, { "name": "owner_b_two_rooms", "about": "owner B's day under a contract naming two rooms: a break of the second room in its first documentary, overlapping none", @@ -4145,6 +4675,33 @@ ] } }, + { + "name": "contract_overlap", + "about": "another contract of the channel whose period overlaps the contract's", + "kind": "report", + "report": { + "contract": "main", + "contracts": [ + "overlapping" + ], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "contract_overlap", + "at": [ + "contracts", + 0 + ] + } + }, { "name": "report_truncated", "about": "a report of ten bytes", @@ -4362,6 +4919,57 @@ ] } }, + { + "name": "run_of_another_channel", + "about": "a run of another channel, by the same signer", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "fast_8" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "run_foreign", + "at": [ + "run", + 1 + ] + } + }, + { + "name": "runs_overlapping", + "about": "a third run whose window overlaps the first's", + "kind": "report", + "report": { + "contract": "main", + "contracts": [], + "report": "574f52310150e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a935f70c24cbf3d26f057396f0bb769784626a435817df7a29ca079af73a288d2d50e157ddcb30ee565b284ce4e78553f33f47aa748694d3da629c2ceeabbf06a900406cd8e95d060000a043f6fd5d06000100ab0ba2efcd8120e9f47d1f3aff234fe032622cc50b65d74824053bf5790f87c702000499a301829cd05163c98ca0556df7a2e69fbed31b19bba77544a6fbbec1d13efdb22f542d9252a0d5fb1276c2e0f9e31a99ef8b90d592476771f6c2a4f0849b0200000001040000000000000010090500000000000b0000000000000000000000000000001516050008000000188f35a1287439a42c907c36490293d2a0653516616527cf61ef31d493725db0000002010000000000000060ea00000000000000000000000000000100000000000000151605000100000024cfd91282849deb797f1d2a86e9e3d538ce625bb9c2658eec2480fc9591f965050000000000000070f30500000000000b00000000000000714357af6f594f6626fc25d06310464d5e687ee824a475c301ea4d74d6aa57e7c0f73a4933a995cdc11947965a748e8a62108a031e3d43942543f7040be29809", + "runs": [ + "hour_1", + "hour_2", + "hour_1_overlapping" + ], + "rooms": [ + "main" + ] + }, + "outcome": { + "outcome": "refuse", + "reason": "run_overlap", + "at": [ + "run", + 2 + ] + } + }, { "name": "room_event_signature", "about": "a room whose producer's third event has a byte of its signature flipped", diff --git a/wai/live-conformance/opportunity_report_verify.py b/wai/live-conformance/opportunity_report_verify.py new file mode 100644 index 000000000..6c3627511 --- /dev/null +++ b/wai/live-conformance/opportunity_report_verify.py @@ -0,0 +1,1102 @@ +#!/usr/bin/env python3 +"""Second implementation of extensions/opportunity-report.md (wai.delivery.opportunity). + +Written from the extension's text and sharing no code with wai-rs: it reads every opportunity +report (WOR1), statement check (WCK1), SCTE 35 section (§5) and reported statement (§9) itself; +verifies contracts, runs and signed revenue-share statements with revenue_share_verify.py, and +rooms (their events, grants, policy and the schedule of breaks their cues make) with +live_ads_verify.py; and recomputes every break, attribution, line, citation and flag. + + python3 -I opportunity_report_verify.py + python3 -I opportunity_report_verify.py --document FILE + +With a corpus directory it gives every vector of vectors.json its outcome and compares it with the +one the corpus states, requires every reason of §13 and every code it reuses to refuse a vector, +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. With --document it prints the outcome of one +report's or check's verification document, status 0 only when it is accepted. Standard library +only. +""" +import copy +import hashlib +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance")) +import revenue_share_verify as RSV # noqa: E402 +import live_ads_verify as LAV # noqa: E402 +import live_rooms_verify as LRV # noqa: E402 +import live_events_verify as LEV # noqa: E402 +import ad_presentation_verify as APV # noqa: E402 + +EXTENSION = os.path.join(HERE, "..", "extensions", "opportunity-report.md") + +REPORT_MAGIC, CHECK_MAGIC = b"WOR1", b"WCK1" # §7.1, §10.1 +REPORT_DOMAIN = b"wai:opportunity-report\x01" # §7.4 +CHECK_DOMAIN = b"wai:statement-check\x01" # §10.5 +CITES_DOMAIN = b"wai:opportunity-cites\x01" # §7.3 +CUED, SCHEDULED = 1, 2 # §4.3 +CLASS_NAMES = {CUED: "cued", SCHEDULED: "scheduled"} +RUN_KIND, CUE_KIND, POLICY_KIND = 10, 12, 13 # §7.3 +PERIOD, DAY, PROGRAMME = 1, 2, 3 # §10.2 +SCOPE_NAMES = {PERIOD: "period", DAY: "day", PROGRAMME: "programme"} +FLAGS = ["unproved", "above", "below", "impressions_above_signed", "impressions_below_signed", "revenue_above_signed", + "revenue_below_signed"] # §10.3 +FORMS = {"json": 1, "csv": 2} +FORM_NAMES = {1: "json", 2: "csv"} +BASES = {"gross": 1, "share": 2} +BASIS_NAMES = {1: "gross", 2: "share"} +WHOLE = 10 ** 6 +U64 = 2 ** 64 - 1 +DAY_US = 86_400_000_000 +ZERO = bytes(32) +ROOM_CARRIER = 1 # revenue-share §3.1 +OPENING = (0x22, 0x30, 0x32, 0x34, 0x36) # §5.2, cue-injection §6.2 +PLACEMENT = (0x34, 0x36) +SUB_SEGMENTED = (0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x44, 0x46) # §5.1 step 7 +REUSED = ["truncated", "malformed", "unknown_version", "bad_key", "bad_signature", "contract_mismatch", "outside_contract", + "delivery_gap", "break_overlap", "outside_window", "asset_unmapped", "line_unsourced", "citation_mismatch"] + +SKIP = set() +Refuse = RSV.Refuse +# Pure steps, held once per input: a room's live-ads verification, a run's, a signed statement's. +_ROOMS, _RUNS, _STATEMENTS = {}, {}, {} + + +def memo(cache, value, f): + k = json.dumps(value, sort_keys=True) + if k not in cache: + cache[k] = f() + return cache[k] + + +def sha(*parts): + h = hashlib.sha256() + for p in parts: + h.update(p) + return h.digest() + + +def ed_verify(key, msg, sig): + return RSV.ed_verify(key, msg, sig) + + +def lowhex(v): + """A byte field of a document: lowercase hexadecimal, or None.""" + if not isinstance(v, str) or len(v) % 2 or any(c not in "0123456789abcdef" for c in v): + return None + return bytes.fromhex(v) + + +# ---------------------------------------------------------------- §5 the section a cue carries + +class Bad(Exception): + pass + + +def crc32(b): + """ISO/IEC 13818-1 Annex A: 0x04C11DB7, preset all ones, no reflection, no final inversion.""" + c = 0xFFFFFFFF + for x in b: + c ^= x << 24 + for _ in range(8): + c = ((c << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if c & 0x80000000 else (c << 1) & 0xFFFFFFFF + return c + + +class Bits: + """Big-endian bit fields of b, up to the byte `end`; running out is Bad.""" + + def __init__(self, b, start, end): + self.b, self.at, self.end = b, start * 8, end + + def take(self, n): + if self.at + n > self.end * 8: + raise Bad() + v = 0 + for _ in range(n): + v = (v << 1) | ((self.b[self.at // 8] >> (7 - self.at % 8)) & 1) + self.at += 1 + return v + + def skip(self, nbytes): + if self.at // 8 + nbytes > self.end: + raise Bad() + self.at += 8 * nbytes + + +def splice_time(r): + if r.take(1): + r.take(6) + r.take(33) + else: + r.take(7) + + +def segmentation(s, start, end): + """SCTE 35-1 Table 20 after its identifier, filling [start, end) exactly.""" + r = Bits(s, start, end) + d = {"event_id": r.take(32), "cancelled": r.take(1) == 1, "duration": None, "type": None, "sub": None} + r.take(7) + if d["cancelled"]: + if r.at // 8 != end: + raise Bad() + return d + program, flagged = r.take(1), r.take(1) + r.take(6) # delivery_not_restricted_flag and the restriction bits + if not program: + r.skip(6 * r.take(8)) + if flagged: + d["duration"] = r.take(40) + r.take(8) # segmentation_upid_type + r.skip(r.take(8)) + d["type"] = r.take(8) + r.take(8) # segment_num + r.take(8) # segments_expected + left = end - r.at // 8 + if left == 2 and d["type"] in SUB_SEGMENTED: + d["sub"] = (r.take(8), r.take(8)) + elif left != 0: + raise Bad() + return d + + +def nothing(command): + return {"command": command, "insert": None, "id": None, "duration": None, "slots": None} + + +def read_section(s): + """§5.1, for what §5.2 says a section states; Bad is cue_section_invalid.""" + n = len(s) + if n < 20: + raise Bad() + crc_at = n - 4 + if crc32(s[:crc_at]) != int.from_bytes(s[crc_at:], "big") and "section-crc" not in SKIP: + raise Bad() + if s[3] != 0: + raise Bad() + if s[4] >> 7: + return nothing(None) + cmd_len = ((s[11] & 0x0F) << 8) | s[12] + cmd = s[13] + if cmd_len == 0xFFF and cmd not in (0x00, 0x05, 0x06): + return nothing(cmd) + end = crc_at if cmd_len == 0xFFF else 14 + cmd_len + if end > crc_at: + raise Bad() + r = Bits(s, 14, end) + st = nothing(cmd) + if cmd == 0x05: # splice_insert() + st["id"] = r.take(32) + cancel = r.take(1) == 1 + r.take(7) + out = False + if not cancel: + out, program, flagged, immediate = r.take(1) == 1, r.take(1), r.take(1), r.take(1) + r.take(4) + if program and not immediate: + splice_time(r) + if not program: + for _ in range(r.take(8)): + r.take(8) + if not immediate: + splice_time(r) + if flagged: + r.take(7) + st["duration"] = r.take(33) + r.take(16) + r.take(8) + r.take(8) + st["insert"] = (cancel, out) + elif cmd == 0x06: # time_signal() + splice_time(r) + elif cmd != 0x00: + r.at = end * 8 + cmd_end = r.at // 8 + if cmd_len != 0xFFF and cmd_end != end: + raise Bad() + if cmd_end + 2 > crc_at: + raise Bad() + p = cmd_end + 2 + if p + int.from_bytes(s[cmd_end:p], "big") != crc_at: + raise Bad() + segs = [] + while p < crc_at: + if p + 2 > crc_at: + raise Bad() + tag, ln = s[p], s[p + 1] + if p + 2 + ln > crc_at: + raise Bad() + if tag == 0x02 and ln >= 4 and s[p + 2:p + 6] == b"CUEI": + segs.append(segmentation(s, p + 6, p + 2 + ln)) + p += 2 + ln + live = [d for d in segs if not d["cancelled"]] + if cmd == 0x06: + opening = next((d for d in live if d["type"] in OPENING), None) + if opening is not None: + st["id"], st["duration"] = opening["event_id"], opening["duration"] + ppo = next((d for d in live if d["type"] in PLACEMENT), None) + if ppo is not None and ppo["sub"] is not None and ppo["sub"][1] > 0: + st["slots"] = ppo["sub"][1] + return st + + +def agrees(st, cue): + """§5.3: the section and its opening cue's summary describe one splice.""" + if st["insert"] is not None and (st["insert"][0] or not st["insert"][1]): + return False + if st["id"] is not None and st["id"] != cue["cue_id"]: + return False + return st["duration"] is None or st["duration"] // 90 == cue["duration_ms"] + + +def command_name(c): + return {None: "encrypted", 0x05: "splice_insert", 0x06: "time_signal", 0x00: "splice_null"}.get(c, "other") + + +# ---------------------------------------------------------------- §3 the evidence + +def the_contract(doc): + """§8.2 steps 1 and 2.""" + c = RSV.contract_at(RSV.unhex(doc["contract"]), ["contract"]) + for k, h in enumerate(doc["contracts"]): + o = RSV.contract_at(RSV.unhex(h), ["contracts", k]) + if o["digest"] != c["digest"] and RSV.overlaps(o, c) and "contract-overlap" not in SKIP: + raise Refuse("contract_overlap", ["contracts", k]) + return c + + +def the_runs(c, runs): + """§8.2 step 4, as revenue-share §8.2 step 4.""" + out = [] + for k, o in enumerate(runs): + try: + r = RSV.read_run(o) + except RSV.NoRead: + raise Refuse("malformed", ["run", k]) + if "run-verify" not in SKIP and not memo(_RUNS, o, lambda: RSV.run_verifies(r)): + raise Refuse("run_invalid", ["run", k]) + if "run-foreign" not in SKIP and (r["channel_id"].encode() != c["channel"] or r["signer_pubkey"] != c["channel_signer"]): + raise Refuse("run_foreign", ["run", k]) + if "run-overlap" not in SKIP and any(r["window_start_ms"] < x["window_end_ms"] and x["window_start_ms"] < r["window_end_ms"] + for x in out): + raise Refuse("run_overlap", ["run", k]) + out.append(r) + return out + + +def room_reads(v): + """§8.1: a room's object.""" + if not isinstance(v, dict) or set(v) != {"descriptor", "grants", "key_log", "chains", "policy"}: + return False + if lowhex(v["descriptor"]) is None or lowhex(v["policy"]) is None: + return False + if not isinstance(v["grants"], list) or any(lowhex(g) is None for g in v["grants"]): + return False + if v["key_log"] is not None and not isinstance(v["key_log"], str): + return False + return isinstance(v["chains"], list) and all(isinstance(c, list) and all(lowhex(e) is not None for e in c) for c in v["chains"]) + + +def room_outcome(v): + """live-ads §9.2 steps 1 to 3 over a room with no receipts: live_ads_verify.py's outcome.""" + holder = {"creatives": [], "issuers": []} + APV.held(holder) + lv = dict(v, pt0_ms=0, receipts=[], pods=[], claims=[], creatives=[], issuers=[], name="room") + return LAV.ads(lv, judge=lambda vv, d, p: APV.Judge(holder, vv, p))[0] + + +def the_rooms(c, rooms): + """§8.2 step 5: each verified as live-ads §9.2 steps 1 to 3 verify one, with no receipts.""" + out = [] + for k, v in enumerate(rooms): + at = ["room", k] + if not room_reads(v): + raise Refuse("malformed", at) + got = memo(_ROOMS, v, lambda: room_outcome(v)) + if got["outcome"] == "gap": + raise Refuse("delivery_gap", at) + if got["outcome"] != "accept": + raise Refuse(got["reason"], at + got["at"]) + room_id = bytes.fromhex(got["room"]) + if (ROOM_CARRIER, room_id) not in c["carriers"] and "room-foreign" not in SKIP: + raise Refuse("room_foreign", at) + if any(x["id"] == room_id for x in out) and "room-repeated" not in SKIP: + raise Refuse("room_repeated", at) + policy = LAV.read_policy(bytes.fromhex(v["policy"])) + events = {} + for chain in v["chains"]: + for h in chain: + ev = LEV.read_record(bytes.fromhex(h)) + events[ev["digest"]] = ev + out.append({"id": room_id, "t0": LRV.read_descriptor(bytes.fromhex(v["descriptor"]))["t0"], "slots": policy["slots"], + "policy": sha(policy["body"]), "breaks": got["breaks"], "events": events}) + return out + + +# ---------------------------------------------------------------- §4, §6 breaks + +def share_of(c, party, split): + return next((p for k, p in RSV.parties(c, split) if k == party), None) + + +def attribute(c, run, rid, e, at): + """§6: an entry's asset by the rule, and its split; the between split for none.""" + asset = RSV.asset_of(run["schedule"], e, c["rule"]) + split = c["between"] + if asset is not None: + split = RSV.split_of(c, asset.encode()) + if split is None: + if "unmapped" not in SKIP: + raise Refuse("asset_unmapped", at) + split = c["between"] + return rid, e, asset, split + + +def place(runs, t): + """§6: the run whose window holds t, and its entry whose span does.""" + for k, r in enumerate(runs): + if r["window_start_ms"] * 1000 <= t < r["window_end_ms"] * 1000: + for e, x in enumerate(r["schedule"]): + if x["start_ms"] * 1000 <= t < (x["start_ms"] + x["duration_ms"]) * 1000: + return k, e + return None + return None + + +def the_breaks(c, start, end, runs, ids, rooms): + """§8.2 steps 6 and 7.""" + out, starts = [], [] + for k, room in enumerate(rooms): + mine = [] + for i, b in enumerate(room["breaks"]): + at = ["room", k, "break", i] + s = room["t0"] + b["t"] + if s > U64: + continue + starts.append(s) + if not (start <= s < end) and "period" not in SKIP: + continue + cue = LEV.read_payload(0x02, room["events"][bytes.fromhex(b["digest"])]["payload"], 65535) + st = None + if cue["form"] == 1: + try: + st = read_section(cue["scte"]) + except Bad: + if "section-read" not in SKIP: + raise Refuse("cue_section_invalid", at) + if st is not None and not agrees(st, cue) and "section-agree" not in SKIP: + raise Refuse("cue_section_mismatch", at) + duration = b["duration_ms"] if b["duration_ms"] is not None else b["avail_ms"] + if "break-overlap" not in SKIP and any(o["start"] < s + duration * 1000 and s < o["start"] + o["duration_ms"] * 1000 for o in out): + raise Refuse("break_overlap", at) + where = place(runs, s) + if where is None: + if "window" in SKIP: + continue + raise Refuse("outside_window", at) + rid, e, asset, split = attribute(c, runs[where[0]], ids[where[0]], where[1], at) + if st is not None and st["slots"] is not None and "slots-section" not in SKIP: + slots, source, pol = st["slots"], "section", None + else: + slots, source, pol = room["slots"], "policy", room["policy"] + cited = {(CUE_KIND, bytes.fromhex(b["digest"])), (RUN_KIND, rid)} + if b["closed_by"] is not None: + cited.add((CUE_KIND, bytes.fromhex(b["closed_by"]))) + if pol is not None: + cited.add((POLICY_KIND, pol)) + mine.append({"class": CUED, "room": k, "index": i, "taken": None, "cue": b["digest"], "start": s, "duration_ms": duration, + "open": b["type"] == "out" and b["closed_by"] is None, "slots": slots, "slots_from": source, + "section": None if st is None else {"command": command_name(st["command"]), "id": st["id"], + "duration": st["duration"], "slots": st["slots"]}, + "run": rid, "entry": e, "asset": asset, "split": split, "cited": cited}) + out += mine + seen = set() + for k in sorted(range(len(runs)), key=lambda j: runs[j]["window_start_ms"]): + r = runs[k] + for e in r["aired"]: + x = r["schedule"][e] + if x["kind"] != "ad": + continue + key = (x["asset_id"], x["start_ms"], x["duration_ms"]) + if key in seen and "dedupe" not in SKIP: + continue + seen.add(key) + s, z = x["start_ms"] * 1000, (x["start_ms"] + x["duration_ms"]) * 1000 + if any(s <= t < z for t in starts) and "cued-test" not in SKIP: + continue + if not (start <= s < end) and "period" not in SKIP: + continue + rid, e2, asset, split = attribute(c, r, ids[k], e, ["run", k, "entry", e]) + out.append({"class": SCHEDULED, "room": None, "index": None, "taken": {"run": k, "entry": e}, "cue": None, "start": s, + "duration_ms": x["duration_ms"], "open": False, "slots": None, "slots_from": None, "section": None, + "run": rid, "entry": e2, "asset": asset, "split": split, "cited": {(RUN_KIND, rid)}}) + return out + + +# ---------------------------------------------------------------- §7 the report + +def cites_digest(cited): + return sha(CITES_DOMAIN, *[bytes([k]) + i for k, i in sorted(cited)]) + + +def read_report(b): + """§7.5, or Refuse at `report`.""" + at = ["report"] + bad = RSV.head(b, REPORT_MAGIC) + if bad: + raise Refuse(bad, at) + r = RSV.R(b, 5) + try: + rep = {"issuer": r.take(32), "contract": r.take(32), "party": r.take(32), "start": r.u(8), "end": r.u(8)} + rep["rooms"] = [r.take(32) for _ in range(r.u(2))] + rep["runs"] = [r.take(32) for _ in range(r.u(2))] + rep["lines"] = [] + for _ in range(r.u(2)): + rep["lines"].append({"split": r.u(2), "class": r.u(1), "breaks": r.u(8), "break_ms": r.u(8), "slots": r.u(8), + "unslotted": r.u(8), "ppm": r.u(4), "cited": r.u(4), "cites": r.take(32)}) + rep["totals"] = (r.u(8), r.u(8), r.u(8)) + rep["sig"] = r.take(64) + except ValueError: + raise Refuse("truncated", at) + if r.p != len(b): + raise Refuse("malformed", at) + keys = [(x["split"], x["class"]) for x in rep["lines"]] + rules = (rep["start"] < rep["end"] and rep["issuer"] != ZERO and rep["party"] != ZERO and RSV.ascending(rep["rooms"]) + and RSV.ascending(rep["runs"]) and RSV.ascending(keys) + and all(x["class"] in (1, 2) and x["breaks"] > 0 and x["cited"] > 0 and x["unslotted"] <= x["breaks"] + and 1 <= x["ppm"] <= WHOLE for x in rep["lines"])) + if not rules and "report-rules" not in SKIP: + raise Refuse("opportunity_report_invalid", at) + rep["body"] = b[:-64] + rep["digest"] = sha(rep["body"]) + return rep + + +def groups_of(breaks): + g = {} + for b in breaks: + x = g.setdefault((b["split"], b["class"]), {"breaks": 0, "break_ms": 0, "slots": 0, "unslotted": 0, "cited": set()}) + x["breaks"] += 1 + x["break_ms"] += b["duration_ms"] + if b["slots"] is None: + x["unslotted"] += 1 + else: + x["slots"] += b["slots"] + x["cited"] |= b["cited"] + return g + + +def lines_of(c, party, breaks): + """§7.2, §7.3: the party's lines and totals, or None for a figure past its field.""" + out = [] + for (split, cls), g in sorted(groups_of(breaks).items()): + ppm = share_of(c, party, split) + if ppm is None: + continue + out.append({"split": split, "class": cls, "breaks": g["breaks"], "break_ms": g["break_ms"], "slots": g["slots"], + "unslotted": g["unslotted"], "ppm": ppm, "cited": len(g["cited"]), "cites": cites_digest(g["cited"]), + "cited_records": sorted(g["cited"])}) + totals = (sum(x["breaks"] for x in out), sum(x["break_ms"] for x in out), sum(x["slots"] for x in out)) + if any(x[f] > U64 for x in out for f in ("break_ms", "slots")) or any(t > U64 for t in totals): + return None, None + return out, totals + + +LINE_FIELDS = ("split", "class", "breaks", "break_ms", "slots", "unslotted", "ppm", "cited", "cites") + + +def verify_report(doc): + """§8.2: (outcome, held) where held is what a check reads of an accepted report.""" + try: + if not isinstance(doc, dict) or set(doc) != {"contract", "contracts", "report", "runs", "rooms"}: + raise Refuse("malformed", []) + if not all(isinstance(doc[k], list) for k in ("contracts", "runs", "rooms")): + raise Refuse("malformed", []) + if lowhex(doc["contract"]) is None or lowhex(doc["report"]) is None or any(lowhex(h) is None for h in doc["contracts"]): + raise Refuse("malformed", []) + c = the_contract(doc) # steps 1, 2 + rep = read_report(bytes.fromhex(doc["report"])) # step 3 + if "binding" not in SKIP and (rep["contract"] != c["digest"] or (rep["party"] != c["operator"] and rep["party"] not in c["owners"])): + raise Refuse("contract_mismatch", ["report"]) + if "contract-period" not in SKIP and not (c["start"] <= rep["start"] and rep["end"] <= c["end"]): + raise Refuse("outside_contract", ["report"]) + if "report-signature" not in SKIP: + bad = ed_verify(rep["issuer"], REPORT_DOMAIN + rep["body"], rep["sig"]) + if bad: + raise Refuse(bad, ["report"]) + runs = the_runs(c, doc["runs"]) # step 4 + ids = [RSV.run_identity(r) for r in runs] + rooms = the_rooms(c, doc["rooms"]) # step 5 + breaks = the_breaks(c, rep["start"], rep["end"], runs, ids, rooms) # steps 6, 7 + groups = groups_of(breaks) # step 8 + for x in rep["lines"]: + g = groups.get((x["split"], x["class"])) + holds = x["split"] < len(c["splits"]) and share_of(c, rep["party"], x["split"]) is not None + if "unsourced" not in SKIP and (g is None or not holds or x["breaks"] > g["breaks"]): + raise Refuse("line_unsourced", ["report"]) + lines, totals = lines_of(c, rep["party"], breaks) + if lines is None: + raise Refuse("opportunity_report_mismatch", ["report"]) + for x in rep["lines"]: + mine = [y for y in lines if (y["split"], y["class"]) == (x["split"], x["class"])] + if mine and "citations" not in SKIP and (x["cited"] != mine[0]["cited"] or x["cites"] != mine[0]["cites"]): + raise Refuse("citation_mismatch", ["report"]) + same = (rep["rooms"] == sorted(r["id"] for r in rooms) and rep["runs"] == sorted(ids) + and [{k: x[k] for k in LINE_FIELDS} for x in rep["lines"]] == [{k: y[k] for k in LINE_FIELDS} for y in lines] + and rep["totals"] == totals) + if not same and "mismatch" not in SKIP: + raise Refuse("opportunity_report_mismatch", ["report"]) + except Refuse as e: + return {"outcome": "refuse", "reason": e.reason, "at": e.at}, None + counted = [b for b in breaks if share_of(c, rep["party"], b["split"]) is not None] + return accepted_report(c, rep, breaks, lines), {"contract": c, "report": rep, "counted": counted} + + +def accepted_report(c, rep, breaks, lines): + """§8.3.""" + return { + "outcome": "accept", + "report": rep["digest"].hex(), + "contract": rep["contract"].hex(), + "issuer": rep["issuer"].hex(), + "party": rep["party"].hex(), + "role": "operator" if rep["party"] == c["operator"] else "owner", + "start": rep["start"], + "end": rep["end"], + "rooms": [d.hex() for d in rep["rooms"]], + "runs": [d.hex() for d in rep["runs"]], + "breaks": [{"class": CLASS_NAMES[b["class"]], "room": b["room"], "index": b["index"], "taken": b["taken"], "cue": b["cue"], + "start": b["start"], "duration_ms": b["duration_ms"], "open": b["open"], "slots": b["slots"], + "slots_from": b["slots_from"], "section": b["section"], "run": b["run"].hex(), "entry": b["entry"], + "asset": b["asset"], "split": b["split"], "cites": [[k, d.hex()] for k, d in sorted(b["cited"])]} for b in breaks], + "lines": [{"split": y["split"], "class": CLASS_NAMES[y["class"]], "breaks": y["breaks"], "break_ms": y["break_ms"], + "slots": y["slots"], "unslotted": y["unslotted"], "ppm": y["ppm"], + "cites": [[k, d.hex()] for k, d in y["cited_records"]]} for y in lines], + "totals": {"breaks": rep["totals"][0], "break_ms": rep["totals"][1], "slots": rep["totals"][2]}, + } + + +# ---------------------------------------------------------------- §9 the reported statement + +DATE = re.compile(r"(\d{4})-(\d{2})-(\d{2})") + + +def day_start(s): + """§2: the start of a day `YYYY-MM-DD` from 1970 to 9999, in microseconds, or None.""" + m = DATE.fullmatch(s) if isinstance(s, str) and s.isascii() else None + if m is None: + return None + y, mo, d = (int(x) for x in m.groups()) + leap = (y % 4 == 0 and y % 100 != 0) or y % 400 == 0 + days = [31, 29 if leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + if not (1970 <= y <= 9999 and 1 <= mo <= 12 and 1 <= d <= days[mo - 1]): + return None + ordinal = sum(366 if (x % 4 == 0 and x % 100 != 0) or x % 400 == 0 else 365 for x in range(1970, y)) + sum(days[:mo - 1]) + d - 1 + return ordinal * DAY_US + + +def an_int(v): + return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= U64 + + +def utf8(s): + try: + return s.encode("utf-8") + except UnicodeEncodeError: + return None + + +def from_json(text): + try: + o = json.loads(text) + except ValueError: + return None + need = {"channel", "start", "end", "currency", "basis", "impressions", "revenue"} + if not isinstance(o, dict) or not need <= set(o) or not set(o) <= need | {"days", "programmes"}: + return None + if not all(isinstance(o[k], str) for k in ("channel", "currency", "basis")) or not all(an_int(o[k]) for k in ("start", "end", "impressions", "revenue")): + return None + if o["basis"] not in BASES: + return None + r = {"channel": utf8(o["channel"]), "start": o["start"], "end": o["end"], "currency": utf8(o["currency"]), "basis": BASES[o["basis"]], + "impressions": o["impressions"], "revenue": o["revenue"], "days": [], "programmes": []} + for name, key in (("days", "day"), ("programmes", "asset")): + items = o.get(name, []) + if not isinstance(items, list): + return None + for x in items: + if not isinstance(x, dict) or set(x) != {key, "impressions", "revenue"} or not isinstance(x[key], str): + return None + if not an_int(x["impressions"]) or not an_int(x["revenue"]): + return None + if name == "days": + start = day_start(x["day"]) + if start is None: + return None + r["days"].append({"day": x["day"], "start": start, "impressions": x["impressions"], "revenue": x["revenue"]}) + else: + r["programmes"].append({"asset": utf8(x["asset"]), "impressions": x["impressions"], "revenue": x["revenue"]}) + if r["channel"] is None or r["currency"] is None or any(p["asset"] is None for p in r["programmes"]): + return None + return r + + +def csv_records(text): + """RFC 4180 records, each ended by CRLF or LF, the last line break optional; None when not.""" + b, n, p, out = text, len(text), 0, [] + if n == 0: + return None + while True: + rec = [] + while True: + f = [] + if p < n and b[p] == '"': + p += 1 + while True: + if p >= n: + return None + if b[p] == '"': + if p + 1 < n and b[p + 1] == '"': + f.append('"') + p += 2 + else: + p += 1 + break + else: + f.append(b[p]) + p += 1 + else: + while p < n and b[p] not in ",\r\n": + if b[p] == '"': + return None + f.append(b[p]) + p += 1 + rec.append("".join(f)) + if p < n and b[p] == ",": + p += 1 + continue + break + out.append(rec) + if p == n: + return out + if b[p] == "\r" and p + 1 < n and b[p + 1] == "\n": + p += 2 + elif b[p] == "\n": + p += 1 + else: + return None + if p == n: + return out + + +def csv_int(s): + if not re.fullmatch(r"0|[1-9][0-9]*", s or "", re.ASCII): + return None + v = int(s) + return v if v <= U64 else None + + +def from_csv(text): + recs = csv_records(text) + if recs is None or any(len(r) != 4 for r in recs) or not recs or recs[0] != ["scope", "key", "impressions", "revenue"]: + return None + rows = recs[1:] + meta = {} + for name in ("channel", "start", "end", "currency", "basis"): + if not rows or rows[0][0] != name or rows[0][2] != "" or rows[0][3] != "": + return None + meta[name] = rows[0][1] + rows = rows[1:] + if not rows or rows[0][0] != "period" or rows[0][1] != "": + return None + period, rows = rows[0], rows[1:] + start, end, imps, rev = csv_int(meta["start"]), csv_int(meta["end"]), csv_int(period[2]), csv_int(period[3]) + if None in (start, end, imps, rev) or meta["basis"] not in BASES: + return None + r = {"channel": meta["channel"].encode(), "start": start, "end": end, "currency": meta["currency"].encode(), + "basis": BASES[meta["basis"]], "impressions": imps, "revenue": rev, "days": [], "programmes": []} + in_programmes = False + for row in rows: + i, v = csv_int(row[2]), csv_int(row[3]) + if i is None or v is None: + return None + if row[0] == "day" and not in_programmes: + ds = day_start(row[1]) + if ds is None: + return None + r["days"].append({"day": row[1], "start": ds, "impressions": i, "revenue": v}) + elif row[0] == "programme": + in_programmes = True + r["programmes"].append({"asset": row[1].encode(), "impressions": i, "revenue": v}) + else: + return None + return r + + +def read_reported(form, text): + """§9: the reported statement of a form, held to §9.3's rules, or None (reported_invalid).""" + r = from_json(text) if form == 1 else from_csv(text) + if r is None: + return None + rules = (1 <= len(r["channel"]) <= 1024 and len(r["currency"]) == 3 and all(0x41 <= x <= 0x5A for x in r["currency"]) + and r["start"] < r["end"] + and all(r["start"] <= d["start"] and d["start"] + DAY_US <= r["end"] for d in r["days"]) + and all(a["start"] < b["start"] for a, b in zip(r["days"], r["days"][1:])) + and all(1 <= len(p["asset"]) <= 1024 for p in r["programmes"]) + and all(a["asset"] < b["asset"] for a, b in zip(r["programmes"], r["programmes"][1:]))) + if not rules and "reported-rules" not in SKIP: + return None + return r + + +# ---------------------------------------------------------------- §10 the check + +def read_check(b): + """§10.6, or Refuse at `check`.""" + at = ["check"] + bad = RSV.head(b, CHECK_MAGIC) + if bad: + raise Refuse(bad, at) + r = RSV.R(b, 5) + try: + c = {"issuer": r.take(32), "report": r.take(32), "reported": r.take(32), "form": r.u(1), "statement": r.take(32), + "floor": r.u(4), "ceiling": r.u(4), "lines": []} + for _ in range(r.u(2)): + x = {"scope": r.u(1), "key": r.text()} + for f in ("breaks", "break_ms", "slots", "unslotted", "impressions", "revenue", "fill_ppm", "revenue_per_slot"): + x[f] = r.u(8) + signed = r.u(1) + if signed not in (0, 1): + raise Refuse("statement_check_invalid", at) + x["signed"] = (r.u(8), r.u(8)) if signed else None + x["flags"] = r.u(1) + c["lines"].append(x) + c["sig"] = r.take(64) + except ValueError: + raise Refuse("truncated", at) + if r.p != len(b): + raise Refuse("malformed", at) + + def key_ok(x): + if x["scope"] == PERIOD: + return x["key"] == b"" + try: + k = x["key"].decode("utf-8") + except UnicodeDecodeError: + return False + if x["scope"] == DAY: + return day_start(k) is not None + return x["scope"] == PROGRAMME and 1 <= len(x["key"]) <= 1024 + + ls = c["lines"] + rules = (c["issuer"] != ZERO and c["form"] in (1, 2) and c["floor"] <= WHOLE and c["ceiling"] >= WHOLE + and bool(ls) and ls[0]["scope"] == PERIOD and sum(1 for x in ls if x["scope"] == PERIOD) == 1 + and RSV.ascending([(x["scope"], x["key"]) for x in ls]) + and all(key_ok(x) and (x["signed"] is None or x["scope"] == PERIOD) and x["flags"] < 0x80 for x in ls)) + if not rules and "check-rules" not in SKIP: + raise Refuse("statement_check_invalid", at) + c["body"] = b[:-64] + c["digest"] = sha(c["body"]) + return c + + +def ratio(a, m, b): + return 0 if b == 0 else min(a * m // b, U64) + + +def tally(bs): + return (len(bs), min(sum(b["duration_ms"] for b in bs), U64), min(sum(b["slots"] for b in bs if b["slots"] is not None), U64), + sum(1 for b in bs if b["slots"] is None)) + + +def check_lines(counted, reported, signed, floor, ceiling): + """§10.2 to §10.4.""" + def line(scope, key, t, imps, rev, sig): + return {"scope": scope, "key": key, "breaks": t[0], "break_ms": t[1], "slots": t[2], "unslotted": t[3], "impressions": imps, + "revenue": rev, "fill_ppm": ratio(imps, WHOLE, t[2]), "revenue_per_slot": ratio(rev, 1, t[2]), "signed": sig, "flags": 0} + out = [line(PERIOD, b"", tally(counted), reported["impressions"], reported["revenue"], signed)] + for d in reported["days"]: + out.append(line(DAY, d["day"].encode(), tally([b for b in counted if d["start"] <= b["start"] < d["start"] + DAY_US]), + d["impressions"], d["revenue"], None)) + for p in reported["programmes"]: + out.append(line(PROGRAMME, p["asset"], tally([b for b in counted if b["asset"] is not None and b["asset"].encode() == p["asset"]]), + p["impressions"], p["revenue"], None)) + rate, period_slots = out[0]["fill_ppm"], out[0]["slots"] + for x in out: + f = 0 + if x["impressions"] > 0 and x["breaks"] == 0 and "flag-unproved" not in SKIP: + f |= 1 + if x["scope"] != PERIOD and x["slots"] > 0 and period_slots > 0 and "flag-bounds" not in SKIP: + if x["fill_ppm"] * WHOLE > rate * ceiling: + f |= 2 + if x["fill_ppm"] * WHOLE < rate * floor: + f |= 4 + if x["signed"] is not None and "flag-signed" not in SKIP: + si, sr = x["signed"] + f |= (8 if x["impressions"] > si else 0) | (16 if x["impressions"] < si else 0) + f |= (32 if x["revenue"] > sr else 0) | (64 if x["revenue"] < sr else 0) + x["flags"] = f + return out + + +CHECK_LINE_FIELDS = ("scope", "key", "breaks", "break_ms", "slots", "unslotted", "impressions", "revenue", "fill_ppm", "revenue_per_slot", + "signed", "flags") + + +def verify_check(doc): + """§11.2.""" + try: + if not isinstance(doc, dict) or set(doc) != {"report", "reported", "statement", "check"}: + raise Refuse("malformed", []) + rp = doc["reported"] + if not isinstance(rp, dict) or set(rp) != {"form", "text"} or rp["form"] not in FORMS or not isinstance(rp["text"], str): + raise Refuse("malformed", []) + cb = lowhex(doc["check"]) + if cb is None or not (doc["statement"] is None or isinstance(doc["statement"], dict)): + raise Refuse("malformed", []) + got, held = verify_report(doc["report"]) # step 1 + if got["outcome"] != "accept": + raise Refuse(got["reason"], ["report"] + got["at"]) + c, rep = held["contract"], held["report"] + form = FORMS[rp["form"]] + reported = read_reported(form, rp["text"]) # step 2 + if reported is None: + raise Refuse("reported_invalid", ["reported"]) + if "reported-foreign" not in SKIP and (reported["channel"] != c["channel"] or reported["currency"] != c["currency"] + or (reported["start"], reported["end"]) != (rep["start"], rep["end"])): + raise Refuse("reported_foreign", ["reported"]) + statement = None + if doc["statement"] is not None: # step 3 + sg = memo(_STATEMENTS, doc["statement"], lambda: RSV.verify(doc["statement"])) + if sg["outcome"] != "accept": + raise Refuse(sg["reason"], ["statement"] + sg["at"]) + statement = RSV.read_statement(bytes.fromhex(doc["statement"]["statement"])) + if "statement-foreign" not in SKIP and (statement["contract"] != rep["contract"] or statement["party"] != rep["party"] + or (statement["start"], statement["end"]) != (rep["start"], rep["end"])): + raise Refuse("statement_foreign", ["statement"]) + ch = read_check(cb) # step 4 + if "check-signature" not in SKIP: + bad = ed_verify(ch["issuer"], CHECK_DOMAIN + ch["body"], ch["sig"]) + if bad: + raise Refuse(bad, ["check"]) + signed = None + if statement is not None: + imps = min(sum(x["impressions"] for x in statement["lines"]), U64) + rev = min(sum(x["revenue"] for x in statement["lines"]), U64) if reported["basis"] == 1 else statement["total"] + signed = (imps, rev) + lines = check_lines(held["counted"], reported, signed, ch["floor"], ch["ceiling"]) + same = (ch["report"] == rep["digest"] and ch["reported"] == sha(rp["text"].encode()) and ch["form"] == form + and ch["statement"] == (ZERO if statement is None else statement["digest"]) + and [{k: x[k] for k in CHECK_LINE_FIELDS} for x in ch["lines"]] == [{k: y[k] for k in CHECK_LINE_FIELDS} for y in lines]) + if not same and "check-mismatch" not in SKIP: + raise Refuse("statement_check_mismatch", ["check"]) + except Refuse as e: + return {"outcome": "refuse", "reason": e.reason, "at": e.at} + return { + "outcome": "accept", + "check": ch["digest"].hex(), + "issuer": ch["issuer"].hex(), + "report": ch["report"].hex(), + "party": rep["party"].hex(), + "start": rep["start"], + "end": rep["end"], + "reported": ch["reported"].hex(), + "form": FORM_NAMES[ch["form"]], + "basis": BASIS_NAMES[reported["basis"]], + "statement": None if statement is None else statement["digest"].hex(), + "floor_ppm": ch["floor"], + "ceiling_ppm": ch["ceiling"], + "lines": [{"scope": SCOPE_NAMES[x["scope"]], "key": x["key"].decode("utf-8"), "breaks": x["breaks"], "break_ms": x["break_ms"], + "slots": x["slots"], "unslotted": x["unslotted"], "impressions": x["impressions"], "revenue": x["revenue"], + "fill_ppm": x["fill_ppm"], "revenue_per_slot": x["revenue_per_slot"], + "signed": None if x["signed"] is None else {"impressions": x["signed"][0], "revenue": x["signed"][1]}, + "flags": [n for i, n in enumerate(FLAGS) if x["flags"] & (1 << i)]} for x in ch["lines"]], + } + + +def verify(doc): + """A report's or a check's document: a check's holds `reported`.""" + if isinstance(doc, dict) and "reported" in doc: + return verify_check(doc) + return verify_report(doc)[0] + + +# ---------------------------------------------------------------- the corpus + +def document(doc, v): + """A vector as its verification document, its parts resolved by name.""" + def report_doc(p): + return {"contract": doc["contracts"][p["contract"]], "contracts": [doc["contracts"][n] for n in p["contracts"]], + "report": p["report"], "runs": None if p["runs"] is None else [doc["runs"][n] for n in p["runs"]], + "rooms": [doc["rooms"][n] for n in p["rooms"]]} + if v["kind"] == "report": + return report_doc(v["report"]) + s = v["statement"] + sd = None if s is None else {"contract": doc["contracts"][s["contract"]], "contracts": [], "statement": s["statement"], + "runs": [doc["runs"][n] for n in s["runs"]], "ledgers": [doc["ledgers"][n] for n in s["ledgers"]]} + return {"report": report_doc(v["report"]), "reported": doc["reported"][v["reported"]], "statement": sd, "check": v["check"]} + + +def check(doc, quiet=False): + """The failures: every vector whose stated outcome is not the one this verifier reaches.""" + fails = [] + for v in doc.get("vectors", []): + try: + got = verify(document(doc, v)) + except Exception as e: # a crash on a mutated copy is a failure to verify it + got = {"outcome": "crash", "error": "%s: %s" % (type(e).__name__, e)} + ok = got == v.get("outcome") + if not quiet: + what = got["outcome"] + (" %s at %s" % (got["reason"], got["at"]) if got["outcome"] == "refuse" else "") + print(("PASS" if ok else "FAIL") + " %s: %s" % (v.get("name"), what)) + if not ok: + fails.append(v.get("name")) + return fails + + +def reasons_listed(): + """§13's table, in order.""" + text = open(EXTENSION, encoding="utf-8").read() + at = text.index("\n## 13. Reasons") + end = text.find("\n## ", at + 1) + return [m.group(1) for m in re.finditer(r"^\| `([a-z_]+)` \|", text[at:end], re.M)] + + +def flip_hex(h, at): + b = bytearray(bytes.fromhex(h)) + b[at % len(b)] ^= 0x01 + return b.hex() + + +def mutations(doc): + """Copies of the corpus, one defect each, that a verifier must reject.""" + out = [] + names = [v["name"] for v in doc["vectors"]] + + def mutate(label, f): + m = copy.deepcopy(doc) + f(m) + out.append((label, m)) + + for k, v in enumerate(doc["vectors"]): + if v["outcome"]["outcome"] != "accept": + continue + if v["kind"] == "report": + for at in (6, 40, 100, 140, -90, -70, -1): + mutate("%s: report byte %d flipped" % (v["name"], at), lambda m, k=k, at=at: m["vectors"][k]["report"].update( + report=flip_hex(m["vectors"][k]["report"]["report"], at))) + mutate("%s: its stated totals raised" % v["name"], lambda m, k=k: m["vectors"][k]["outcome"]["totals"].update( + breaks=m["vectors"][k]["outcome"]["totals"]["breaks"] + 1)) + if v["outcome"]["breaks"]: + mutate("%s: its first break moved a second" % v["name"], lambda m, k=k: m["vectors"][k]["outcome"]["breaks"][0].update( + start=m["vectors"][k]["outcome"]["breaks"][0]["start"] + 1_000_000)) + mutate("%s: its first break's slots changed" % v["name"], lambda m, k=k: m["vectors"][k]["outcome"]["breaks"][0].update( + slots=7)) + if v["report"]["rooms"]: + mutate("%s: its last room dropped" % v["name"], lambda m, k=k: m["vectors"][k]["report"]["rooms"].pop()) + else: + for at in (40, 104, 150, -80, -66, -1): + mutate("%s: check byte %d flipped" % (v["name"], at), lambda m, k=k, at=at: m["vectors"][k].update( + check=flip_hex(m["vectors"][k]["check"], at))) + mutate("%s: a line's flags changed in its outcome" % v["name"], lambda m, k=k: m["vectors"][k]["outcome"]["lines"][-1].update( + flags=["above"])) + if v["statement"] is not None: + mutate("%s: its signed statement left out" % v["name"], lambda m, k=k: m["vectors"][k].update(statement=None)) + for name, at in (("main", -1), ("main", 40), ("two_rooms", 60)): + mutate("the contract %s: byte %d flipped" % (name, at), lambda m, name=name, at=at: m["contracts"].update( + {name: flip_hex(doc["contracts"][name], at)})) + mutate("the run of the first hour: its signature flipped", + lambda m: m["runs"]["hour_1"].update(sig=flip_hex(doc["runs"]["hour_1"]["sig"], 3))) + mutate("the run of the second hour: its first programme renamed", + lambda m: m["runs"]["hour_2"]["schedule"][0].update(asset_id="a/ep-1")) + mutate("the main room: an event of its producer's flipped", + lambda m: m["rooms"]["main"]["chains"][1].__setitem__(5, flip_hex(doc["rooms"]["main"]["chains"][1][5], 130))) + mutate("the main room: its policy flipped", lambda m: m["rooms"]["main"].update(policy=flip_hex(doc["rooms"]["main"]["policy"], 70))) + mutate("owner B's reported statement: one impression more", + lambda m: m["reported"]["b_programmes"].update(text=doc["reported"]["b_programmes"]["text"].replace('"impressions":14', '"impressions":15'))) + mutate("owner A's CSV: its line breaks made LF", + lambda m: m["reported"]["a_days"].update(text=doc["reported"]["a_days"]["text"].replace("\r\n", "\n"))) + for k, v in enumerate(doc["vectors"]): + if v["outcome"]["outcome"] == "refuse": + mutate("%s: its stated reason changed" % v["name"], lambda m, k=k, v=v: m["vectors"][k]["outcome"].update( + reason="malformed" if v["outcome"]["reason"] != "malformed" else "truncated")) + break + assert len(names) == len(set(names)), "vector names are unique" + return out + + +CHECKS = ["contract-overlap", "report-rules", "binding", "contract-period", "report-signature", "run-verify", "run-foreign", "run-overlap", + "room-foreign", "room-repeated", "period", "section-crc", "section-read", "section-agree", "break-overlap", "window", "unmapped", + "slots-section", "dedupe", "cued-test", "unsourced", "citations", "mismatch", "reported-rules", "reported-foreign", + "statement-foreign", "check-rules", "check-signature", "check-mismatch", "flag-unproved", "flag-bounds", "flag-signed"] + + +def self_tests(): + """The standard's sample (SCTE 35-1 §14.1, as cue-injection §5.7 holds it) reads as §5 reads it.""" + sample = bytes.fromhex("fc3034000000000000fffff00506fe72bd0050001e021c435545494800008e7fcf0001a599b00808000000002ca0a18a3402009ac9d17e") + st = read_section(sample) + fails = [] + if (st["command"], st["id"], st["duration"], st["slots"]) != (0x06, 0x4800008E, 307 * 90_000, None): + fails.append("the standard's sample does not state its opportunity") + damaged = bytearray(sample) + damaged[-1] ^= 1 + try: + read_section(bytes(damaged)) + fails.append("a section with a damaged CRC reads") + except Bad: + pass + print("%s the standard's SCTE 35 sample reads, and a damaged copy does not" % ("PASS" if not fails else "FAIL")) + return fails + + +def main(corpus_dir): + doc = json.load(open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8")) + fails = self_tests() + fails += check(doc) + files = sorted(os.listdir(corpus_dir)) + if files != ["README.md", "vectors.json"]: + fails.append("the corpus holds other files: %s" % files) + seen = {v["outcome"].get("reason") for v in doc["vectors"]} + listed = reasons_listed() + missing = [r for r in listed + REUSED if r not in seen] + if missing: + fails.append("no vector is refused %s" % missing) + print("%s every reason of §13 and every reused code refuses a vector (%d listed)" % ("PASS" if not missing else "FAIL", len(listed))) + muts = mutations(doc) + weak = [name for name, m in muts if not check(m, quiet=True)] + print("%s every mutated copy of the corpus fails (%d copies)" % ("PASS" if not weak else "FAIL", len(muts))) + fails += ["the mutated copy %r verifies" % w for w in weak] + undetected = [] + for c in CHECKS: + SKIP.add(c) + try: + if not check(doc, quiet=True): + undetected.append(c) + finally: + SKIP.discard(c) + print("%s the corpus catches the verifier with each of its checks dropped (%d checks)" % ("PASS" if not undetected else "FAIL", len(CHECKS))) + fails += ["dropping the check %r goes unnoticed" % c for c in undetected] + for f in fails: + print("FAIL", f) + return 1 if fails else 0 + + +def check_document(path): + doc = json.load(open(path, encoding="utf-8")) + got = verify(doc) + print(json.dumps(got, indent=1)) + return 0 if got["outcome"] == "accept" else 1 + + +if __name__ == "__main__": + if len(sys.argv) == 3 and sys.argv[1] == "--document": + sys.exit(check_document(sys.argv[2])) + if len(sys.argv) != 2: + print("usage: opportunity_report_verify.py | --document FILE", file=sys.stderr) + sys.exit(2) + sys.exit(main(sys.argv[1])) diff --git a/wai/wai-rs/src/opportunity_report/corpus.rs b/wai/wai-rs/src/opportunity_report/corpus.rs index 37fde2c55..176b67007 100644 --- a/wai/wai-rs/src/opportunity_report/corpus.rs +++ b/wai/wai-rs/src/opportunity_report/corpus.rs @@ -509,6 +509,13 @@ pub fn world() -> Result { tampered.aired = vec![0]; tampered.sig = key("channel").sign(&tampered.payload()).to_bytes(); runs_.insert("hour_1_aired_misstated".to_string(), tampered.to_json()); + let part_1 = run(r1.schedule.clone(), (0, 1_850), None); + let part_2 = run(r1.schedule.clone(), (1_850, 3_600), Some(part_1.receipt_hash())); + runs_.insert("hour_1_to_1850_s".to_string(), part_1.to_json()); + runs_.insert("hour_1_from_1850_s".to_string(), part_2.to_json()); + let other = ChannelReceipt::seal(&key("channel"), "did:key:channel-signer", "fast-8", r1.schedule.clone(), r1.window_start_ms, r1.window_end_ms, 0, None, GrantRef::unbounded("video.channel"), None); + runs_.insert("fast_8".to_string(), other.to_json()); + runs_.insert("hour_1_overlapping".to_string(), run(r1.schedule.clone(), (3_000, 4_000), None).to_json()); let c = in_force(contract(&[main.id])); let two = in_force(contract(&[main.id, backup.id])); @@ -517,6 +524,9 @@ pub fn world() -> Result { contracts.insert("main".to_string(), json!(hex(&c.to_bytes()))); contracts.insert("two_rooms".to_string(), json!(hex(&two.to_bytes()))); contracts.insert("offered".to_string(), json!(hex(&offered.to_bytes()))); + let mut late = contract(&[main.id]); + (late.start, late.end) = (OCT + 20 * DAY_US, NOV + 20 * DAY_US); + contracts.insert("overlapping".to_string(), json!(hex(&in_force(late).to_bytes()))); // The ledger: the room's receipts for every break, billed for the day. let source = ads_source(&main, &opened); @@ -564,15 +574,18 @@ pub fn world() -> Result { // The reports of the day. let mut reports = BTreeMap::new(); let both = || Some(vec!["hour_1", "hour_2"]); - for (name, contract_, rooms_, who, issuer) in [ - ("owner_a", "main", vec!["main"], "owner-a", "owner-a"), - ("owner_b", "main", vec!["main"], "owner-b", "owner-b"), - ("operator", "main", vec!["main"], "operator", "operator"), - ("owner_a_runs_alone", "main", vec![], "owner-a", "auditor"), - ("owner_b_two_rooms", "two_rooms", vec!["main", "backup"], "owner-b", "auditor"), + let split = || Some(vec!["hour_1_to_1850_s", "hour_1_from_1850_s", "hour_2"]); + for (name, contract_, runs__, rooms_, who, issuer, end) in [ + ("owner_a", "main", both(), vec!["main"], "owner-a", "owner-a", DAY17), + ("owner_b", "main", both(), vec!["main"], "owner-b", "owner-b", DAY17), + ("operator", "main", both(), vec!["main"], "operator", "operator", DAY17), + ("owner_a_runs_alone", "main", both(), vec![], "owner-a", "auditor", DAY17), + ("owner_a_runs_split", "main", split(), vec![], "owner-a", "auditor", DAY17), + ("owner_b_first_hour", "main", both(), vec!["main"], "owner-b", "owner-b", DAY16 + 3_600 * S), + ("owner_b_two_rooms", "two_rooms", both(), vec!["main", "backup"], "owner-b", "auditor", DAY17), ] { - let mut rp = ReportParts { contract: contract_, contracts: vec![], report: Vec::new(), runs: both(), rooms: rooms_ }; - let (r, _) = super::create(&pools.report_document(&rp, false), &key(issuer), pk(who), DAY16, DAY17).map_err(|e| format!("the report {name}: {e:?}"))?; + let mut rp = ReportParts { contract: contract_, contracts: vec![], report: Vec::new(), runs: runs__, rooms: rooms_ }; + let (r, _) = super::create(&pools.report_document(&rp, false), &key(issuer), pk(who), DAY16, end).map_err(|e| format!("the report {name}: {e:?}"))?; rp.report = r.to_bytes(); reports.insert(name, rp); } @@ -654,6 +667,8 @@ pub fn generate() -> Result { Vector::Report { name: "owner_b_day", about: "owner B's day: four cued breaks in its documentaries, slots from placement opportunities and from the room's policy", parts: rep("owner_b") }, Vector::Report { name: "operator_day", about: "the operator's day: its share of both owners' splits; no line of the between split, in which it holds no share", parts: rep("operator") }, Vector::Report { name: "owner_a_runs_alone", about: "owner A's day from the runs alone: both ad slots scheduled, no cued break", parts: rep("owner_a_runs_alone") }, + Vector::Report { name: "owner_a_runs_split", about: "owner A's day from the runs alone, the first hour in two windows that both air its ad slot: one break, taken in the earlier window", parts: rep("owner_a_runs_split") }, + Vector::Report { name: "owner_b_first_hour", about: "owner B's first hour: the breaks of the second hour are another period's", parts: rep("owner_b_first_hour") }, Vector::Report { name: "owner_b_two_rooms", about: "owner B's day under a contract naming two rooms: a break of the second room in its first documentary, overlapping none", parts: rep("owner_b_two_rooms") }, // Accepted checks. check("check_owner_b_under_reported", "owner B's reported statement per programme beside its report and the signed statement: its second documentary far below the day's rate, and the day below the signed impressions and revenue", b.clone(), "b_programmes", Some(st("owner_b")), cb.clone()), @@ -663,6 +678,7 @@ pub fn generate() -> Result { // The report's document and contract. with("document_malformed", "a report's document whose runs are not a list", &|x| x.runs = None), with("contract_offered", "a contract owner B has not countersigned", &|x| x.contract = "offered"), + with("contract_overlap", "another contract of the channel whose period overlaps the contract's", &|x| x.contracts = vec!["overlapping"]), // The report. report("report_truncated", "a report of ten bytes", ra[..10].to_vec()), report("report_magic", "a report whose magic is WOR2", [b"WOR2".as_slice(), &ra[4..]].concat()), @@ -678,6 +694,8 @@ pub fn generate() -> Result { }), // The runs and the rooms. with("run_aired_misstated", "a run whose aired set the window does not give, signed again", &|x| x.runs = Some(vec!["hour_1_aired_misstated", "hour_2"])), + with("run_of_another_channel", "a run of another channel, by the same signer", &|x| x.runs = Some(vec!["hour_1", "fast_8"])), + with("runs_overlapping", "a third run whose window overlaps the first's", &|x| x.runs = Some(vec!["hour_1", "hour_2", "hour_1_overlapping"])), with("room_event_signature", "a room whose producer's third event has a byte of its signature flipped", &|x| x.rooms = vec!["main_signature_flipped"]), with("room_with_a_gap", "a room whose producer's chain lacks its fifth event", &|x| x.rooms = vec!["main_with_a_gap"]), with("room_foreign", "a room the contract does not name", &|x| x.rooms = vec!["backup"]), -- 2.54.0 (Apple Git-157) From d15b4a7a3eb0ccc9b473e650b9e0b086ea070422 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 29 Oct 2026 18:31:56 -0400 Subject: [PATCH 3/5] wai: the opportunity report's operations, registry entry, SPEC paragraph and CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit opportunity.create (wai opportunity create), opportunity.verify (wai opportunity verify, a report's or a check's document) and statement.check (wai statement check), under the agent rules, in a build with opportunity_report; tests/cli_ops.rs holds --dry-run on the two that write. The catalogue export, Studio's generated actions and the registry export are regenerated. REGISTRY.md registers wai.delivery.opportunity and the magics WOR1 and WCK1; SPEC.md §5 and CHANGELOG.md describe it. The conformance workflow runs the module's tests, tests/opportunity_report.rs and opportunity_report_verify.py; the catalogue workflow runs cli_ops with the feature. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-catalogue.yml | 8 +- .github/workflows/wai-conformance.yml | 33 ++- wai/CHANGELOG.md | 29 ++ wai/REGISTRY.md | 3 + wai/SPEC.md | 18 ++ wai/catalogue/ops.json | 283 +++++++++++++++++++ wai/registry/capabilities.json | 28 ++ wai/vocab/v2.ttl | 7 + wai/wai-rs/src/bin/wai_cli/dx/mod.rs | 11 +- wai/wai-rs/src/bin/wai_cli/dx/opportunity.rs | 187 ++++++++++++ wai/wai-rs/src/bin/wai_cli/dx/share.rs | 16 +- wai/wai-rs/src/catalogue.rs | 67 +++++ wai/wai-rs/src/codecs/registry.rs | 6 + wai/wai-rs/tests/cli_ops.rs | 17 ++ wai/wai-studio/src/actions.generated.js | 5 +- 15 files changed, 703 insertions(+), 15 deletions(-) create mode 100644 wai/wai-rs/src/bin/wai_cli/dx/opportunity.rs diff --git a/.github/workflows/wai-catalogue.yml b/.github/workflows/wai-catalogue.yml index ab6073666..c79832c9b 100644 --- a/.github/workflows/wai-catalogue.yml +++ b/.github/workflows/wai-catalogue.yml @@ -123,8 +123,10 @@ jobs: # --confirm, its deprecated spelling), # first in the archive's build (quickstart), then with every command-line feature that # needs no system library, so --dry-run is held on generate, score and stage too, with - # Create's (app), so it is held on wai app export, and with the revenue share's - # (revenue_share), so it is held on wai contract create|accept and wai statement create. + # Create's (app), so it is held on wai app export, with the revenue share's + # (revenue_share), so it is held on wai contract create|accept and wai statement create, and + # with the opportunity report's (opportunity_report), so it is held on wai opportunity + # create and wai statement check. - name: The catalogue's rules, and the command line through it shell: bash working-directory: wai/wai-rs @@ -141,3 +143,5 @@ jobs: ran "$RUNNER_TEMP/cli-app.log" cargo test --locked --no-default-features --features quickstart,revenue_share --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli-share.log" ran "$RUNNER_TEMP/cli-share.log" + cargo test --locked --no-default-features --features quickstart,opportunity_report --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli-opportunity.log" + ran "$RUNNER_TEMP/cli-opportunity.log" diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 7e7ea2570..61b0b8f7c 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -200,6 +200,10 @@ on: # test reads its sections. Its corpus is generated from the ledger corpus, under the paths # above. - 'wai/extensions/revenue-share.md' + # tests/opportunity_report.rs requires this extension's §13 to list the codes the reference + # reports, opportunity_report_verify.py reads that table, and the registry's wire identifiers + # test reads its sections. Its corpus is generated from fixed seeds, under the paths above. + - 'wai/extensions/opportunity-report.md' # tests/stream_audit.rs requires this extension's §14 to list the codes the reference # reports, stream_audit_verify.py reads that table, and the registry's wire identifiers test # reads its sections. @@ -1160,7 +1164,7 @@ jobs: name: independent verifiers · claims, JWP, key discovery and integer payloads (Python) runs-on: linux-arm64 env: - WAI_FEATURES: key_discovery,http,video_manifest,stage_measure,live_events,live_room,capture_take,live_compose,live_record,timed_text,access_services,live_metering,energy_steering,live_moderation,dred_exact,live_ads,live_provenance,synthesis,synthesis_mark,neural_int,exact_upscale,robustness,ai_usage,contribution,settlement,browser_sink,receipt_claims,takedown,influence,direction,co_performance,age_assurance,bearer_receipts,exploration,co_watch,telemetry_aggregate,exact_tonemap,splat_exact,live_watermark,derive_edge,sink_energy,key_release,stitched_presentation,prior_wire,scene_map,avatar_face,receipt_ledger,stream_audit,fleet,native_sink,platform_encode,remote_desktop,stream_termination,live_world,format_probe,live_messaging,conform_result,revenue_share + WAI_FEATURES: key_discovery,http,video_manifest,stage_measure,live_events,live_room,capture_take,live_compose,live_record,timed_text,access_services,live_metering,energy_steering,live_moderation,dred_exact,live_ads,live_provenance,synthesis,synthesis_mark,neural_int,exact_upscale,robustness,ai_usage,contribution,settlement,browser_sink,receipt_claims,takedown,influence,direction,co_performance,age_assurance,bearer_receipts,exploration,co_watch,telemetry_aggregate,exact_tonemap,splat_exact,live_watermark,derive_edge,sink_energy,key_release,stitched_presentation,prior_wire,scene_map,avatar_face,receipt_ledger,stream_audit,fleet,native_sink,platform_encode,remote_desktop,stream_termination,live_world,format_probe,live_messaging,conform_result,revenue_share,opportunity_report WAI_PQ_FEATURES: synthesis,pq_signatures WAI_RELEASE_FEATURES: dred_exact,synthesis_mark,robustness,avatar_face steps: @@ -1255,7 +1259,7 @@ jobs: --test stitched_presentation --test prior_wire --test scene_map --test receipt_ledger \ --test stream_audit --test fleet --test native_sink --test platform_encode --test remote_desktop \ --test stream_termination --test live_world --test format_probe --test live_messaging \ - --test conform_result --test revenue_share + --test conform_result --test revenue_share --test opportunity_report cargo test --release --no-run --no-default-features --features "$WAI_RELEASE_FEATURES" \ --test dred_exact --test dred_exact_measure --test synthesis_mark --test robustness \ --test avatar_face @@ -2014,6 +2018,29 @@ jobs: (cd wai-rs && cargo test --no-default-features --features "$WAI_FEATURES" --lib wai_video_channel::) (cd wai-rs && cargo test --no-default-features --features "$WAI_FEATURES" --test revenue_share) python3 -I live-conformance/revenue_share_verify.py live-conformance/share + # wai.delivery.opportunity (extensions/opportunity-report.md): the reference module's unit + # tests (the SCTE 35 subset reader against the standard's sample and damaged sections, the + # reported statement's JSON and CSV forms and dates, the report's and the check's reading and + # rules, the ratios); tests/opportunity_report.rs (vectors.json regenerates byte for byte + # from its fixed seeds and no other file is there; wai-rs gives every vector its stated + # outcome; every reason of §13 and every reused code refuses a vector; §13 lists exactly the + # reference's codes; the worked example, a channel's day whose reported statement + # under-reports one owner's programme, and the check that shows it, recomputed from the + # public records alone); then the corpus's second implementation, opportunity_report_verify.py, + # written from the extension's text and sharing no code with wai-rs, which verifies rooms + # with live_ads_verify.py and contracts, runs and signed statements with + # revenue_share_verify.py, reads every report, check, section and reported statement itself, + # recomputes every break, attribution, line, citation and flag, 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; standard library only, run isolated (-I). + - name: opportunity report · wai-rs and opportunity_report_verify.py + working-directory: wai + shell: bash + run: | + set -euo pipefail + (cd wai-rs && cargo test --no-default-features --features "$WAI_FEATURES" --lib opportunity_report::) + (cd wai-rs && cargo test --no-default-features --features "$WAI_FEATURES" --test opportunity_report) + python3 -I live-conformance/opportunity_report_verify.py live-conformance/opportunity # wai.delivery.audit (extensions/stream-audit.md): the reference module's unit tests (the # acknowledgement windows, chains and repeats of §4.2, the origin rule, the report's bytes, # channel names) and tests/stream_audit.rs (vectors.json regenerates byte for byte from its @@ -2692,7 +2719,7 @@ jobs: synthesis,pq_signatures sink_energy key_release stitched_presentation prior_wire \ prior_wire,neural_int scene_map avatar_face receipt_ledger stream_audit fleet native_sink \ platform_encode remote_desktop stream_termination live_world format_probe live_messaging \ - conform_result revenue_share; do + conform_result revenue_share opportunity_report; do echo "::group::$f" cargo check --no-default-features --features "$f" --tests echo "::endgroup::" diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index aa453afde..09483e960 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,35 @@ wire format may change and such changes are recorded here. ## Unreleased +### The opportunity report and the statement check: what a channel's records prove, beside what a platform reported + +- **`wai.delivery.opportunity`** ([`extensions/opportunity-report.md`](extensions/opportunity-report.md)), a new + record. 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: the breaks a room the contract names makes of its + signed cues (`wai.live.ads` §4.3), each cue's SCTE 35 section read for its id, duration and slots (a placement + opportunity's `sub_segments_expected`, or the room policy's slots) and held to the cue's summary, and the ad entries + of the channel's signed program-as-run that no cue covers; each placed at its start on the window that aired it and + attributed to the programme on air by the contract's map and rule. Each line, per split and class (cued, + scheduled), states the breaks, their seconds, their slots and those with none, with a citation digest of the cues, + policies and runs it came from. The report is signed by its issuer, and every figure recomputes from the records. +- **A reported statement**: a platform's figures for the period, per day or per programme, in a documented JSON or + CSV (RFC 4180) form; read, labelled reported, and never evidence. +- **A statement check**, `WCK1`: per period, day and programme, the opportunities, the reported impressions and + revenue, the implied fill (impressions per slot, in millionths) and revenue per slot; flags for a figure no break + stands behind and for a line far from the period's own rate at bounds the check states; and, where the operator's + signed revenue-share statement of the same party and period is given, its impressions and revenue (gross, or the + party's part) beside the reported ones. +- **Operations** `opportunity.create` (`wai opportunity create`), `opportunity.verify` (`wai opportunity verify`, a + report's or a check's document) and `statement.check` (`wai statement check`), in a build with the feature + `opportunity_report`, under the agent rules. +- **Conformance**: `live-conformance/opportunity/`, written by `wai_opportunity_vectors` from fixed seeds: a channel's + day of two owners, a room cueing breaks with every kind of section the text reads, the operator's signed + statements of the day, the reports of both owners and the operator, checks in JSON and CSV (one under-reporting an + owner's programme), and a refused vector for each reason. Its second implementation, + `live-conformance/opportunity_report_verify.py`, verifies rooms with `live_ads_verify.py` and contracts, runs and + statements with `revenue_share_verify.py`, and recomputes every break, line and flag itself. +- `revenue_share`'s run reader and asset rule are shared with the new module (`pub(crate)`); no record's bytes change. + ### The revenue share: what each owner of a channel is owed, recomputable from the public records - **`wai.delivery.revenue_share`** ([`extensions/revenue-share.md`](extensions/revenue-share.md)), a new record. An diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 4f4a7506c..1b3774104 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -187,6 +187,7 @@ and a role are fixed once registered. | `wai.delivery.audit` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.delivery.fleet` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.delivery.ledger` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | +| `wai.delivery.opportunity` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.delivery.revenue_share` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.delivery.settlement` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.derive.crop` | derivation | registered | — | — | patent-pool | — | — | §5 Prior artifacts, general tensors and derivation operations | @@ -381,6 +382,7 @@ and a role are fixed once registered. | `WCA1` | magic | defined | live-messaging §10.3 | | `WCD1` | magic | defined | live-compose §3.1 | | `WCI1` | magic | defined | live-messaging §10.2 | +| `WCK1` | magic | defined | opportunity-report §10.1 | | `WCL1` | magic | defined | live-recording §5.1 | | `WCN1` | magic | defined | capture-binding §8.1 | | `WCR1` | magic | defined | live-compose §8.1 | @@ -413,6 +415,7 @@ and a role are fixed once registered. | `WMP1` | magic | defined | live-moderation §3.1 | | `WMR1` | magic | defined | live-metering §4.1 | | `WMS1` | magic | defined | live-moderation §4.2 | +| `WOR1` | magic | defined | opportunity-report §7.1 | | `WPA1` | magic | defined | co-performance §7.3 | | `WPC1` | magic | defined | ad-presentation §5.1 | | `WPF1` | magic | defined | co-performance §5.6 | diff --git a/wai/SPEC.md b/wai/SPEC.md index b5d5b2a24..c8eb47ca1 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -1544,6 +1544,24 @@ not map, a contract an owner did not countersign, two contracts that overlap, an invoices bill, a split whose shares do not sum, and a line no record covers. No payment and no currency conversion: [`extensions/revenue-share.md`](extensions/revenue-share.md). +And for **checking a platform's reported figures against the opportunities a channel's own records +prove** — `wai.delivery.opportunity`: 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 from the signed cues of a room the contract names (the schedule `wai.live.ads` makes +of them, its slots those an SCTE 35 placement opportunity in the cue's section states, or the room +policy's) or from an ad entry of the channel's signed program-as-run that no cue covers, placed at its +start on the window that aired it and attributed to the programme on air; per split and class it +states the breaks, their seconds and their slots, with a citation digest of the cues, policy and runs +they came from. A reported statement, the platform's own figures in a documented JSON or CSV form, is +read and never treated as evidence. A statement check, `WCK1`, sets them side by side for the period, +each day and each programme: the opportunities, the reported impressions and revenue, impressions and +revenue per slot, flags for a figure no break stands behind and for one far from the period's own +rate, and, where the operator's signed revenue-share statement of the same party and period is given, +its impressions and revenue beside the reported ones. A verifier refuses a cue whose section its +summary contradicts, a break no window places, two rooms' breaks at once, and a report or a check that +does not recompute. No parser of any platform's own format: +[`extensions/opportunity-report.md`](extensions/opportunity-report.md). + And for **auditing a stream another service delivers** — `wai.delivery.audit`: a content owner's players, with nothing asked of the service that packages and delivers the stream over HTTP. For each playback session a player makes a fresh key and signs with it settlement acknowledgements of every diff --git a/wai/catalogue/ops.json b/wai/catalogue/ops.json index 702e27425..2c17f0756 100644 --- a/wai/catalogue/ops.json +++ b/wai/catalogue/ops.json @@ -6428,6 +6428,289 @@ "sdk": "wai.statement.verify" } }, + { + "id": "opportunity.create", + "summary": "A party's opportunity report for a period: the ad breaks a channel's signed cues and program-as-run prove in the programmes it shares in, signed with the issuer key", + "cli": "wai opportunity create", + "aliases": [], + "usage": [ + "wai opportunity create --party --start µs --end µs --key [--out F] [--yes] the report", + " (WOR1) of the party for [start, end), computed from a verification document without its report (the", + " contract, other contracts, runs and rooms), signed with the issuer key and written with its document to F", + " (.wai-opportunity.json) for `wai opportunity verify`; replacing a file waits for --yes" + ], + "placement": "local", + "effect": "writes-project", + "dry_run": true, + "confirm": "when-replacing-a-file", + "route": null, + "doors": { + "cli": true, + "mcp": false, + "http": false + }, + "toolset": "core", + "stability": "preview", + "input": { + "type": "object", + "properties": { + "inputs": { + "type": "string", + "format": "path", + "description": "A verification document without its report: contract, contracts, runs and rooms.", + "x-cli": { + "positional": true + } + }, + "party": { + "type": "string", + "description": "The party's key, 64 hexadecimal digits: the operator or an owner.", + "x-cli": { + "flag": "--party" + } + }, + "start": { + "type": "integer", + "description": "The period's start, microseconds since 1970.", + "x-cli": { + "flag": "--start" + } + }, + "end": { + "type": "integer", + "description": "The period's end, after its start.", + "x-cli": { + "flag": "--end" + } + }, + "key": { + "type": "string", + "format": "path", + "description": "The issuer key's file: 32 bytes, or 64 hexadecimal digits.", + "x-cli": { + "flag": "--key" + } + }, + "out": { + "type": "string", + "format": "path", + "description": "The verification document's file (.wai-opportunity.json).", + "x-cli": { + "flag": "--out" + } + } + }, + "required": [ + "inputs", + "party", + "start", + "end", + "key" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "required": [ + "file", + "report", + "outcome" + ], + "description": "The opportunity report (WOR1, hexadecimal), the file its verification document went to, and its outcome, recomputed here" + }, + "stream": null, + "since": "0.3.0", + "deprecated": null, + "receipt": true, + "needs": { + "all": [ + "opportunity_report" + ] + }, + "names": { + "cli": "wai opportunity create", + "mcp": null, + "c": "wai_opportunity_create", + "rust": "wai::opportunity::create", + "sdk": "wai.opportunity.create" + } + }, + { + "id": "opportunity.verify", + "summary": "An opportunity report's or a statement check's verification document checked: every break, line and flag recomputed from the records, or the reason it is refused", + "cli": "wai opportunity verify", + "aliases": [], + "usage": [ + "wai opportunity verify the outcome of a report's or a check's verification document: each break", + " attributed, each line and flag recomputed; exit 1 with the reason on a refusal" + ], + "placement": "local", + "effect": "reads", + "dry_run": false, + "confirm": "never", + "route": null, + "doors": { + "cli": true, + "mcp": false, + "http": false + }, + "toolset": "core", + "stability": "preview", + "input": { + "type": "object", + "properties": { + "document": { + "type": "string", + "format": "path", + "description": "The verification document: a report's (contract, contracts, report, runs, rooms) or a check's (report, reported, statement, check).", + "x-cli": { + "positional": true + } + } + }, + "required": [ + "document" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "required": [ + "outcome" + ], + "description": "An accepted report's outcome (opportunity-report §8.3: every break attributed, each line and the totals) or an accepted check's (§11.3: each line's opportunities, reported and signed figures and flags)" + }, + "stream": null, + "since": "0.3.0", + "deprecated": null, + "receipt": false, + "needs": { + "all": [ + "opportunity_report" + ] + }, + "names": { + "cli": "wai opportunity verify", + "mcp": null, + "c": "wai_opportunity_verify", + "rust": "wai::opportunity::verify", + "sdk": "wai.opportunity.verify" + } + }, + { + "id": "statement.check", + "summary": "A platform's reported statement checked against an opportunity report and, when given, the operator's signed statement, signed with the issuer key", + "cli": "wai statement check", + "aliases": [], + "usage": [ + "wai statement check [--statement ] [--floor ppm] [--ceiling ppm] --key [--out F] [--yes]", + " the check (WCK1) of a reported statement (JSON, or CSV for a file named .csv) beside a report's verification", + " document and a signed statement's, at the bounds given (500000 and 2000000 when not), signed with the issuer", + " key and written with its document to F (.wai-check.json); replacing a file waits for --yes" + ], + "placement": "local", + "effect": "writes-project", + "dry_run": true, + "confirm": "when-replacing-a-file", + "route": null, + "doors": { + "cli": true, + "mcp": false, + "http": false + }, + "toolset": "core", + "stability": "preview", + "input": { + "type": "object", + "properties": { + "report": { + "type": "string", + "format": "path", + "description": "A report's verification document, with its report.", + "x-cli": { + "positional": true + } + }, + "reported": { + "type": "string", + "format": "path", + "description": "The reported statement: JSON, or CSV for a file named .csv.", + "x-cli": { + "positional": true + } + }, + "statement": { + "type": "string", + "format": "path", + "description": "A signed revenue-share statement's verification document of the same party and period.", + "x-cli": { + "flag": "--statement" + } + }, + "floor": { + "type": "integer", + "description": "The lower bound, in millionths of the period's rate (500000).", + "x-cli": { + "flag": "--floor" + } + }, + "ceiling": { + "type": "integer", + "description": "The upper bound, in millionths of the period's rate (2000000).", + "x-cli": { + "flag": "--ceiling" + } + }, + "key": { + "type": "string", + "format": "path", + "description": "The issuer key's file: 32 bytes, or 64 hexadecimal digits.", + "x-cli": { + "flag": "--key" + } + }, + "out": { + "type": "string", + "format": "path", + "description": "The verification document's file (.wai-check.json).", + "x-cli": { + "flag": "--out" + } + } + }, + "required": [ + "report", + "reported", + "key" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "required": [ + "file", + "check", + "outcome" + ], + "description": "The statement check (WCK1, hexadecimal), the file its verification document went to, and its outcome, recomputed here" + }, + "stream": null, + "since": "0.3.0", + "deprecated": null, + "receipt": true, + "needs": { + "all": [ + "opportunity_report" + ] + }, + "names": { + "cli": "wai statement check", + "mcp": null, + "c": "wai_statement_check", + "rust": "wai::statement::check", + "sdk": "wai.statement.check" + } + }, { "id": "ops.list", "summary": "Every operation of this catalogue: its id, command line, effect and whether this build holds it", diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index ace01a158..149f97aa6 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -404,6 +404,20 @@ "edition": null, "registered": "2026-10-25" }, + { + "capability": "wai.delivery.opportunity", + "kind": "record", + "status": "registered", + "media": null, + "role": null, + "tier": null, + "licence": null, + "replicate": null, + "sets": [], + "spec_section": "§5 Interactive, temporal & computed content", + "edition": null, + "registered": "2026-10-29" + }, { "capability": "wai.delivery.revenue_share", "kind": "record", @@ -2765,6 +2779,13 @@ "section": "§10.2", "status": "defined" }, + { + "name": "WCK1", + "kind": "magic", + "document": "extensions/opportunity-report.md", + "section": "§10.1", + "status": "defined" + }, { "name": "WCL1", "kind": "magic", @@ -2989,6 +3010,13 @@ "section": "§4.2", "status": "defined" }, + { + "name": "WOR1", + "kind": "magic", + "document": "extensions/opportunity-report.md", + "section": "§7.1", + "status": "defined" + }, { "name": "WPA1", "kind": "magic", diff --git a/wai/vocab/v2.ttl b/wai/vocab/v2.ttl index d053b3a77..734786dc6 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -373,6 +373,13 @@ wai:wai.delivery.ledger a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-10-25"^^xsd:date . +wai:wai.delivery.opportunity a wai:Capability ; + rdfs:label "wai.delivery.opportunity" ; + wai:kind wai:kind-record ; + wai:status wai:status-registered ; + wai:specSection "§5 Interactive, temporal & computed content" ; + wai:registeredOn "2026-10-29"^^xsd:date . + wai:wai.delivery.revenue_share a wai:Capability ; rdfs:label "wai.delivery.revenue_share" ; wai:kind wai:kind-record ; diff --git a/wai/wai-rs/src/bin/wai_cli/dx/mod.rs b/wai/wai-rs/src/bin/wai_cli/dx/mod.rs index 0c40c3544..12e35aa36 100644 --- a/wai/wai-rs/src/bin/wai_cli/dx/mod.rs +++ b/wai/wai-rs/src/bin/wai_cli/dx/mod.rs @@ -24,7 +24,11 @@ //! - `contract.create`, `contract.update` (`wai contract create|accept`) and `statement.create`, //! `statement.verify` (`wai statement create|verify`): a channel's share contract and the //! revenue-share statements recomputed from the ledger's invoices and the channel's -//! program-as-run (extensions/revenue-share.md §10; `share.rs`). +//! program-as-run (extensions/revenue-share.md §10; `share.rs`); +//! - `opportunity.create`, `opportunity.verify` (`wai opportunity create|verify`) and +//! `statement.check` (`wai statement check`): the ad opportunities a channel's signed records +//! prove for an owner, and a platform's reported figures checked against them +//! (extensions/opportunity-report.md §12; `opportunity.rs`). //! //! [`envelope`] holds the agent rules every command keeps: the exit codes, the JSON envelope, //! `--dry-run`, `--yes`. @@ -45,6 +49,7 @@ mod dev; pub(crate) mod envelope; mod fleet; mod hello; +mod opportunity; mod share; mod verify; @@ -83,6 +88,10 @@ pub const VERBS: &[Impl] = &[ Impl { id: "contract.update", run: share::contract_update, json: Json::Native }, Impl { id: "statement.create", run: share::statement_create, json: Json::Native }, Impl { id: "statement.verify", run: share::statement_verify, json: Json::Native }, + // The opportunity report and the statement check (extensions/opportunity-report.md §12). + Impl { id: "opportunity.create", run: opportunity::opportunity_create, json: Json::Native }, + Impl { id: "opportunity.verify", run: opportunity::opportunity_verify, json: Json::Native }, + Impl { id: "statement.check", run: opportunity::statement_check, json: Json::Native }, ]; /// An operation's synopsis, from the catalogue, for a usage error. diff --git a/wai/wai-rs/src/bin/wai_cli/dx/opportunity.rs b/wai/wai-rs/src/bin/wai_cli/dx/opportunity.rs new file mode 100644 index 000000000..e6a3d2130 --- /dev/null +++ b/wai/wai-rs/src/bin/wai_cli/dx/opportunity.rs @@ -0,0 +1,187 @@ +//! The opportunity report's operations (extensions/opportunity-report.md §12): `wai opportunity +//! create` (`opportunity.create`), `wai opportunity verify` (`opportunity.verify`) and `wai +//! statement check` (`statement.check`), on files on this machine. +//! +//! They keep the agent rules of [`super::envelope`], as every operation of the catalogue does: +//! the exit codes, the JSON envelope with `--json`, `--dry-run` on the two that write (every file +//! through `out.rs`, nothing written), and `--yes` before a file that exists is replaced. A +//! refusal by the extension's text has its reason as the code (`cue_section_mismatch`, +//! `reported_foreign`, ...), at exit 1. + +#[cfg(feature = "opportunity_report")] +use super::envelope as agent; +use super::envelope::{EXIT_FAILED, OpError}; +#[cfg(feature = "opportunity_report")] +use super::share::{done, hex, io_fail, number, read_json, read_key, required, write}; +#[cfg(feature = "opportunity_report")] +use crate::out; +#[cfg(feature = "opportunity_report")] +use serde_json::{Value, json}; + +type R = Result; + +/// A refusal by the extension's text, its reason as the code. +#[cfg_attr(not(feature = "opportunity_report"), allow(dead_code))] +fn refused(what: &str, reason: &'static str, at: &serde_json::Value) -> OpError { + OpError::new(EXIT_FAILED, reason, format!("{what} is refused: {reason} at {at}"), "extensions/opportunity-report.md §13 names each reason") +} + +/// A key of 64 hexadecimal digits. +#[cfg(feature = "opportunity_report")] +fn key_arg(v: &str, flag: &str, usage: &str) -> Result<[u8; 32], OpError> { + (v.len() == 64 && v.bytes().all(|c| c.is_ascii_hexdigit())) + .then(|| (0..32).map(|i| u8::from_str_radix(&v[2 * i..2 * i + 2], 16).expect("hex")).collect::>()) + .and_then(|b| b.try_into().ok()) + .ok_or_else(|| OpError::usage(format!("{flag}: a key, 64 hexadecimal digits"), usage)) +} + +/// A report's outcome, for a person: its party, its lines and its totals. +#[cfg(feature = "opportunity_report")] +fn summarise(o: &Value) -> String { + let d = o["report"].as_str().unwrap_or(""); + let party = o["party"].as_str().unwrap_or(""); + let mut t = format!( + "opportunity report {}… of the {} {}… for [{}, {}): {} break(s), {} ms, {} slot(s)", + &d[..16.min(d.len())], + o["role"].as_str().unwrap_or("?"), + &party[..16.min(party.len())], + o["start"], + o["end"], + o["totals"]["breaks"], + o["totals"]["break_ms"], + o["totals"]["slots"], + ); + for l in o["lines"].as_array().into_iter().flatten() { + t.push_str(&format!("\n split {} {:<9} {} break(s), {} ms, {} slot(s), {} unslotted, share {} ppm", l["split"], l["class"].as_str().unwrap_or(""), l["breaks"], l["break_ms"], l["slots"], l["unslotted"], l["ppm"])); + } + t +} + +/// A check's outcome, for a person: each line with its figures and flags. +#[cfg(feature = "opportunity_report")] +fn summarise_check(o: &Value) -> String { + let d = o["check"].as_str().unwrap_or(""); + let r = o["report"].as_str().unwrap_or(""); + let mut t = format!( + "statement check {}… of report {}… beside a reported statement ({}, {}){}", + &d[..16.min(d.len())], + &r[..16.min(r.len())], + o["form"].as_str().unwrap_or("?"), + o["basis"].as_str().unwrap_or("?"), + if o["statement"].is_null() { "" } else { " and the signed statement" }, + ); + for l in o["lines"].as_array().into_iter().flatten() { + let flags: Vec<&str> = l["flags"].as_array().into_iter().flatten().filter_map(Value::as_str).collect(); + t.push_str(&format!( + "\n {:<9} {:<12} {} slot(s), {} impression(s) reported, fill {} ppm{}{}", + l["scope"].as_str().unwrap_or(""), + l["key"].as_str().unwrap_or(""), + l["slots"], + l["impressions"], + l["fill_ppm"], + l["signed"].get("impressions").map(|s| format!(", {s} signed")).unwrap_or_default(), + if flags.is_empty() { String::new() } else { format!(" [{}]", flags.join(", ")) }, + )); + } + t +} + +#[cfg(not(feature = "opportunity_report"))] +pub fn opportunity_create(_: &[String]) -> R { + Err(crate::door::not_built(wai::catalogue::op("opportunity.create").expect("listed"))) +} + +/// `wai opportunity create --party --start µs --end µs --key [--out F]`: +/// the report of a party and a period from a document without its report (§8.1), signed by the +/// issuer key and written with its document. +#[cfg(feature = "opportunity_report")] +pub fn opportunity_create(args: &[String]) -> R { + use wai::opportunity_report as opp; + let usage = super::usage("opportunity.create"); + let with_value = ["--party", "--start", "--end", "--key", "--out"]; + let pos = agent::check_args(args, &with_value, &with_value, 1, &usage)?; + let path = *pos.first().ok_or_else(|| OpError::usage("the inputs: a report's verification document without its report", &usage))?; + let party = key_arg(required(args, "--party", "the party's key: 64 hexadecimal digits", &usage)?, "--party", &usage)?; + let (start, end) = (number(args, "--start", &usage)?, number(args, "--end", &usage)?); + let key = read_key(required(args, "--key", "the issuer key's file", &usage)?, "the issuer key's file (32 bytes, or 64 hexadecimal digits)")?; + let inputs = read_json(path, "the report's inputs")?; + let (r, doc) = opp::create(&inputs, &key, party, start, end).map_err(|(reason, at)| refused("the report", reason, &at))?; + let o = opp::outcome_json(&opp::verify(&doc)); + let file = super::opt(args, "--out").map(str::to_string).unwrap_or_else(|| format!("{}.wai-opportunity.json", &hex(&r.digest())[..16])); + write(&file, (serde_json::to_string_pretty(&doc).expect("JSON") + "\n").as_bytes(), "the report's document")?; + let text = format!("{}{}", summarise(&o), if out::dry_run() { String::new() } else { format!("\nwritten to {file}; check it anywhere with `wai opportunity verify {file}`") }); + done("opportunity.create", true, json!({"file": file, "report": hex(&r.to_bytes()), "outcome": o}), &text) +} + +#[cfg(not(feature = "opportunity_report"))] +pub fn opportunity_verify(_: &[String]) -> R { + Err(crate::door::not_built(wai::catalogue::op("opportunity.verify").expect("listed"))) +} + +/// `wai opportunity verify `: a report's (§8.1) or a check's (§11.1) document, +/// verified; a refusal is exit 1 with its reason as the code. +#[cfg(feature = "opportunity_report")] +pub fn opportunity_verify(args: &[String]) -> R { + use wai::opportunity_report as opp; + let usage = super::usage("opportunity.verify"); + let pos = agent::check_args(args, &[], &[], 1, &usage)?; + let path = *pos.first().ok_or_else(|| OpError::usage("the verification document", &usage))?; + let doc = read_json(path, "the verification document")?; + if opp::is_check_document(&doc) { + let a = opp::verify_check(&doc).map_err(|(r, at)| refused("the check", r, &at))?; + let o = opp::check_json(&a); + let text = summarise_check(&o); + return done("opportunity.verify", false, o, &text); + } + let a = opp::verify(&doc).map_err(|(r, at)| refused("the report", r, &at))?; + let o = opp::accepted_json(&a); + let text = summarise(&o); + done("opportunity.verify", false, o, &text) +} + +#[cfg(not(feature = "opportunity_report"))] +pub fn statement_check(_: &[String]) -> R { + Err(crate::door::not_built(wai::catalogue::op("statement.check").expect("listed"))) +} + +/// A bound in millionths. +#[cfg(feature = "opportunity_report")] +fn ppm(args: &[String], flag: &str, default: u32, usage: &str) -> Result { + match super::opt(args, flag) { + None => Ok(default), + Some(v) => v.parse().map_err(|_| OpError::usage(format!("{flag} {v}: millionths, an integer"), usage)), + } +} + +/// `wai statement check [--statement ] [--floor ppm] +/// [--ceiling ppm] --key [--out F]`: the check of a reported statement (§9; CSV for a file +/// named `.csv`) against a report's document and, when given, a signed statement's (§10), signed +/// by the issuer key and written with its document. +#[cfg(feature = "opportunity_report")] +pub fn statement_check(args: &[String]) -> R { + use wai::opportunity_report as opp; + let usage = super::usage("statement.check"); + let with_value = ["--statement", "--floor", "--ceiling", "--key", "--out"]; + let pos = agent::check_args(args, &with_value, &with_value, 2, &usage)?; + let (report_path, reported_path) = match pos.as_slice() { + [a, b] => (*a, *b), + _ => return Err(OpError::usage("the report's document and the reported statement", &usage)), + }; + let key = read_key(required(args, "--key", "the issuer key's file", &usage)?, "the issuer key's file (32 bytes, or 64 hexadecimal digits)")?; + let (floor, ceiling) = (ppm(args, "--floor", opp::FLOOR_PPM, &usage)?, ppm(args, "--ceiling", opp::CEILING_PPM, &usage)?); + let report = read_json(report_path, "the report's verification document")?; + let raw = std::fs::read(reported_path).map_err(|e| io_fail(reported_path, &e, "the reported statement"))?; + let text = String::from_utf8(raw).map_err(|_| OpError::usage(format!("{reported_path}: not UTF-8 text"), &usage))?; + let form = if reported_path.ends_with(".csv") { "csv" } else { "json" }; + let statement = match super::opt(args, "--statement") { + Some(p) => read_json(p, "the signed statement's verification document")?, + None => Value::Null, + }; + let inputs = json!({"report": report, "reported": {"form": form, "text": text}, "statement": statement}); + let (c, doc) = opp::create_check(&inputs, &key, floor, ceiling).map_err(|(reason, at)| refused("the check", reason, &at))?; + let o = opp::check_outcome_json(&opp::verify_check(&doc)); + let file = super::opt(args, "--out").map(str::to_string).unwrap_or_else(|| format!("{}.wai-check.json", &hex(&c.digest())[..16])); + write(&file, (serde_json::to_string_pretty(&doc).expect("JSON") + "\n").as_bytes(), "the check's document")?; + let text = format!("{}{}", summarise_check(&o), if out::dry_run() { String::new() } else { format!("\nwritten to {file}; check it anywhere with `wai opportunity verify {file}`") }); + done("statement.check", true, json!({"file": file, "check": hex(&c.to_bytes()), "outcome": o}), &text) +} diff --git a/wai/wai-rs/src/bin/wai_cli/dx/share.rs b/wai/wai-rs/src/bin/wai_cli/dx/share.rs index 00012a8b2..47f2e605d 100644 --- a/wai/wai-rs/src/bin/wai_cli/dx/share.rs +++ b/wai/wai-rs/src/bin/wai_cli/dx/share.rs @@ -22,7 +22,7 @@ type R = Result; /// Print what an operation did: its envelope with `--json`, its text otherwise; in a dry run of /// an operation that writes, with the plan of what it would have written. #[cfg_attr(not(feature = "revenue_share"), allow(dead_code))] -fn done(op: &str, writes: bool, data: Value, text: &str) -> R { +pub(super) fn done(op: &str, writes: bool, data: Value, text: &str) -> R { let plan = (writes && out::dry_run()).then(out::plan); if agent::ctx().json { agent::print(&agent::envelope_with(op, true, data, plan)); @@ -40,7 +40,7 @@ fn done(op: &str, writes: bool, data: Value, text: &str) -> R { /// A file that could not be read or written. #[cfg_attr(not(feature = "revenue_share"), allow(dead_code))] -fn io_fail(path: &str, e: &std::io::Error, hint: &str) -> OpError { +pub(super) fn io_fail(path: &str, e: &std::io::Error, hint: &str) -> OpError { match e.kind() { std::io::ErrorKind::NotFound => OpError::new(EXIT_NOT_FOUND, "not-found", format!("{path}: {e}"), hint), std::io::ErrorKind::PermissionDenied => OpError::new(EXIT_PERMISSION, "permission-denied", format!("{path}: {e}"), "the file's owner runs this"), @@ -55,13 +55,13 @@ fn refused(what: &str, reason: &'static str, at: &Value) -> OpError { } #[cfg_attr(not(feature = "revenue_share"), allow(dead_code))] -fn hex(b: &[u8]) -> String { +pub(super) fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() } /// A key file: 32 bytes, or 64 hexadecimal digits. #[cfg(feature = "revenue_share")] -fn read_key(path: &str, what: &str) -> Result { +pub(super) fn read_key(path: &str, what: &str) -> Result { let raw = std::fs::read(path).map_err(|e| io_fail(path, &e, what))?; let seed: [u8; 32] = if raw.len() == 32 { raw.try_into().expect("32") @@ -78,14 +78,14 @@ fn read_key(path: &str, what: &str) -> Result Result { +pub(super) fn read_json(path: &str, what: &str) -> Result { let b = std::fs::read(path).map_err(|e| io_fail(path, &e, what))?; serde_json::from_slice(&b).map_err(|e| OpError::usage(format!("{path}: not JSON: {e}"), what)) } /// Write `bytes` to `file`, asking first when it exists; in a dry run, plan it. #[cfg(feature = "revenue_share")] -fn write(file: &str, bytes: &[u8], what: &str) -> Result<(), OpError> { +pub(super) fn write(file: &str, bytes: &[u8], what: &str) -> Result<(), OpError> { if !out::dry_run() && std::path::Path::new(file).exists() { agent::confirm(&format!("{file} exists, and {what} would replace it"), vec![json!({"write": file, "bytes": bytes.len(), "replaces": true})])?; } @@ -93,12 +93,12 @@ fn write(file: &str, bytes: &[u8], what: &str) -> Result<(), OpError> { } #[cfg(feature = "revenue_share")] -fn required<'a>(args: &'a [String], flag: &str, what: &str, usage: &str) -> Result<&'a str, OpError> { +pub(super) fn required<'a>(args: &'a [String], flag: &str, what: &str, usage: &str) -> Result<&'a str, OpError> { super::opt(args, flag).ok_or_else(|| OpError::usage(format!("{flag}: {what}"), usage)) } #[cfg(feature = "revenue_share")] -fn number(args: &[String], flag: &str, usage: &str) -> Result { +pub(super) fn number(args: &[String], flag: &str, usage: &str) -> Result { let v = required(args, flag, "microseconds since 1970", usage)?; v.parse().map_err(|_| OpError::usage(format!("{flag} {v}: microseconds since 1970, an integer"), usage)) } diff --git a/wai/wai-rs/src/catalogue.rs b/wai/wai-rs/src/catalogue.rs index b80d17c17..7ffb3b7ce 100644 --- a/wai/wai-rs/src/catalogue.rs +++ b/wai/wai-rs/src/catalogue.rs @@ -301,6 +301,10 @@ const S_APP_CHECK: &str = r#"{"type":"object","required":["program","errors","fa const S_SHARE_CONTRACT: &str = r#"{"type":"object","required":["digest","operator","channel","owners","in_force","contract","file"],"description":"The share contract (WRC1, hexadecimal), its digest, each owner and whether it has countersigned, and the file it went to"}"#; const S_SHARE_STATEMENT: &str = r#"{"type":"object","required":["file","statement","outcome"],"description":"The statement (WRS1, hexadecimal), the file its verification document went to, and its outcome, recomputed here"}"#; const S_SHARE_OUTCOME: &str = r#"{"type":"object","required":["outcome","statement","contract","party","role","lines","total"],"description":"An accepted statement's outcome (revenue-share §8.3): its lines, each split's division, every impression attributed, and the total"}"#; +// The opportunity report's outputs (extensions/opportunity-report.md §12). +const S_OPPORTUNITY: &str = r#"{"type":"object","required":["file","report","outcome"],"description":"The opportunity report (WOR1, hexadecimal), the file its verification document went to, and its outcome, recomputed here"}"#; +const S_OPPORTUNITY_OUTCOME: &str = r#"{"type":"object","required":["outcome"],"description":"An accepted report's outcome (opportunity-report §8.3: every break attributed, each line and the totals) or an accepted check's (§11.3: each line's opportunities, reported and signed figures and flags)"}"#; +const S_STATEMENT_CHECK: &str = r#"{"type":"object","required":["file","check","outcome"],"description":"The statement check (WCK1, hexadecimal), the file its verification document went to, and its outcome, recomputed here"}"#; const S_ENROLMENT: &str = r#"{"type":"object","required":["node","role","organisation","digest","enrolment"],"description":"The enrolment (WFE1, hexadecimal), its digest, and the file and service it went to"}"#; const S_ENROLMENTS: &str = r#"{"type":"object","required":["enrolments"],"description":"Each node the fleet service holds enrolled: its key, role, label and records"}"#; const S_REPORT: &str = r#"{"type":"object","required":["report","outcome"],"description":"The report's digest and its outcome as extensions/fleet.md §7.3 states it"}"#; @@ -1559,6 +1563,68 @@ http vectors diff|fuzz --seed S --count N --out F", needs: Needs::All(&["revenue_share"]), ..OP }, + // ─── the opportunity report and the statement check (extensions/opportunity-report.md §12) ─── + Op { + id: "opportunity.create", + summary: "A party's opportunity report for a period: the ad breaks a channel's signed cues and program-as-run prove in the programmes it shares in, signed with the issuer key", + cli: Some("opportunity create"), + usage: "opportunity create --party --start µs --end µs --key [--out F] [--yes] the report + (WOR1) of the party for [start, end), computed from a verification document without its report (the + contract, other contracts, runs and rooms), signed with the issuer key and written with its document to F + (.wai-opportunity.json) for `wai opportunity verify`; replacing a file waits for --yes", + effect: Effect::WritesProject, + stability: Stability::Preview, + input: &[ + pos("inputs", Ty::Path, true, "A verification document without its report: contract, contracts, runs and rooms."), + flag("party", "--party", Ty::String, true, "The party's key, 64 hexadecimal digits: the operator or an owner."), + flag("start", "--start", Ty::Integer, true, "The period's start, microseconds since 1970."), + flag("end", "--end", Ty::Integer, true, "The period's end, after its start."), + flag("key", "--key", Ty::Path, true, "The issuer key's file: 32 bytes, or 64 hexadecimal digits."), + flag("out", "--out", Ty::Path, false, "The verification document's file (.wai-opportunity.json)."), + ], + output: Output::Json(S_OPPORTUNITY), + receipt: true, + needs: Needs::All(&["opportunity_report"]), + confirm: Some(Confirm::AnyFile), + ..OP + }, + Op { + id: "opportunity.verify", + summary: "An opportunity report's or a statement check's verification document checked: every break, line and flag recomputed from the records, or the reason it is refused", + cli: Some("opportunity verify"), + usage: "opportunity verify the outcome of a report's or a check's verification document: each break + attributed, each line and flag recomputed; exit 1 with the reason on a refusal", + stability: Stability::Preview, + input: &[pos("document", Ty::Path, true, "The verification document: a report's (contract, contracts, report, runs, rooms) or a check's (report, reported, statement, check).")], + output: Output::Json(S_OPPORTUNITY_OUTCOME), + needs: Needs::All(&["opportunity_report"]), + ..OP + }, + Op { + id: "statement.check", + summary: "A platform's reported statement checked against an opportunity report and, when given, the operator's signed statement, signed with the issuer key", + cli: Some("statement check"), + usage: "statement check [--statement ] [--floor ppm] [--ceiling ppm] --key [--out F] [--yes] + the check (WCK1) of a reported statement (JSON, or CSV for a file named .csv) beside a report's verification + document and a signed statement's, at the bounds given (500000 and 2000000 when not), signed with the issuer + key and written with its document to F (.wai-check.json); replacing a file waits for --yes", + effect: Effect::WritesProject, + stability: Stability::Preview, + input: &[ + pos("report", Ty::Path, true, "A report's verification document, with its report."), + pos("reported", Ty::Path, true, "The reported statement: JSON, or CSV for a file named .csv."), + flag("statement", "--statement", Ty::Path, false, "A signed revenue-share statement's verification document of the same party and period."), + flag("floor", "--floor", Ty::Integer, false, "The lower bound, in millionths of the period's rate (500000)."), + flag("ceiling", "--ceiling", Ty::Integer, false, "The upper bound, in millionths of the period's rate (2000000)."), + flag("key", "--key", Ty::Path, true, "The issuer key's file: 32 bytes, or 64 hexadecimal digits."), + flag("out", "--out", Ty::Path, false, "The verification document's file (.wai-check.json)."), + ], + output: Output::Json(S_STATEMENT_CHECK), + receipt: true, + needs: Needs::All(&["opportunity_report"]), + confirm: Some(Confirm::AnyFile), + ..OP + }, // ─── the catalogue itself ─── Op { id: "ops.list", @@ -1629,6 +1695,7 @@ pub fn feature_on(feature: &str) -> Option { "app" => cfg!(feature = "app"), "fleet" => cfg!(feature = "fleet"), "revenue_share" => cfg!(feature = "revenue_share"), + "opportunity_report" => cfg!(feature = "opportunity_report"), _ => return None, }) } diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 8fde5e039..267bcf298 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -240,6 +240,7 @@ pub const CAP_DELIVERY_LEDGER: &str = "wai.delivery.ledger"; pub const CAP_DELIVERY_AUDIT: &str = "wai.delivery.audit"; pub const CAP_DELIVERY_FLEET: &str = "wai.delivery.fleet"; pub const CAP_DELIVERY_REVENUE_SHARE: &str = "wai.delivery.revenue_share"; +pub const CAP_DELIVERY_OPPORTUNITY: &str = "wai.delivery.opportunity"; pub const CAP_MEDIA_TAKEDOWN: &str = "wai.media.takedown"; pub const CAP_LIVE_COPERFORMANCE: &str = "wai.live.coperformance"; pub const CAP_MEDIA_INFLUENCE: &str = "wai.media.influence"; @@ -367,6 +368,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[ (CAP_DELIVERY_AUDIT, S_INTERACTIVE, None, "2026-10-25"), (CAP_DELIVERY_FLEET, S_INTERACTIVE, None, "2026-10-27"), (CAP_DELIVERY_REVENUE_SHARE, S_INTERACTIVE, None, "2026-10-29"), + (CAP_DELIVERY_OPPORTUNITY, S_INTERACTIVE, None, "2026-10-29"), (CAP_MEDIA_TAKEDOWN, S_INTERACTIVE, None, "2026-10-21"), (CAP_LIVE_COPERFORMANCE, S_INTERACTIVE, None, "2026-10-21"), (CAP_MEDIA_INFLUENCE, S_INTERACTIVE, None, "2026-10-21"), @@ -461,6 +463,7 @@ const RECORDS: &[&str] = &[ CAP_DELIVERY_AUDIT, CAP_DELIVERY_FLEET, CAP_DELIVERY_REVENUE_SHARE, + CAP_DELIVERY_OPPORTUNITY, CAP_MEDIA_TAKEDOWN, CAP_LIVE_COPERFORMANCE, CAP_MEDIA_INFLUENCE, @@ -660,6 +663,7 @@ const LEDGER: &str = "extensions/receipt-ledger.md"; const STREAM_AUDIT: &str = "extensions/stream-audit.md"; const FLEET: &str = "extensions/fleet.md"; const REVENUE_SHARE: &str = "extensions/revenue-share.md"; +const OPPORTUNITY: &str = "extensions/opportunity-report.md"; const BROWSER_SINK: &str = "extensions/browser-sink.md"; const TAKEDOWN: &str = "extensions/takedown.md"; const INFLUENCE: &str = "extensions/influence.md"; @@ -837,6 +841,8 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire(".wai-fleet.json", WireKind::Path, FLEET, "§7.1", WireStatus::Defined), wire("WRC1", WireKind::Magic, REVENUE_SHARE, "§3.1", WireStatus::Defined), wire("WRS1", WireKind::Magic, REVENUE_SHARE, "§7.1", WireStatus::Defined), + wire("WOR1", WireKind::Magic, OPPORTUNITY, "§7.1", WireStatus::Defined), + wire("WCK1", WireKind::Magic, OPPORTUNITY, "§10.1", WireStatus::Defined), wire("contentBinding", WireKind::FrameMetadata, BROWSER_SINK, "§7.1", WireStatus::Proposed), wire("wai-takedown-clocks", WireKind::DocumentKind, TAKEDOWN, "§4", WireStatus::Defined), wire("wai-takedown-policy", WireKind::DocumentKind, TAKEDOWN, "§5.1", WireStatus::Defined), diff --git a/wai/wai-rs/tests/cli_ops.rs b/wai/wai-rs/tests/cli_ops.rs index 62e3b900f..898521b54 100644 --- a/wai/wai-rs/tests/cli_ops.rs +++ b/wai/wai-rs/tests/cli_ops.rs @@ -449,6 +449,23 @@ fn samples(d: &Path, origin_port: u16) -> Vec<(&'static str, Vec, bool)> true, )); } + #[cfg(feature = "opportunity_report")] + { + // The opportunity report: owner B's report of the opportunity corpus's day, recomputed + // here from its runs and room, and the check of B's reported statement beside it. + use wai::opportunity_report::corpus; + let hexs = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); + let w = corpus::world().unwrap(); + let b = &w.reports["owner_b"]; + std::fs::write(d.join("owner-b.key"), hexs(&corpus::key("owner-b").to_bytes())).unwrap(); + std::fs::write(d.join("opp-inputs.json"), w.pools.report_document(b, false).to_string()).unwrap(); + std::fs::write(d.join("opp-report.json"), w.pools.report_document(b, true).to_string()).unwrap(); + std::fs::write(d.join("reported.json"), w.pools.reported["b_programmes"]["text"].as_str().unwrap()).unwrap(); + let party = hexs(&corpus::pk("owner-b")); + let (start, end) = (corpus::DAY16.to_string(), corpus::DAY17.to_string()); + more.push(("opportunity.create", s(&["opportunity", "create", "opp-inputs.json", "--party", &party, "--start", &start, "--end", &end, "--key", "owner-b.key", "--out", "b.wai-opportunity.json"]), true)); + more.push(("statement.check", s(&["statement", "check", "opp-report.json", "reported.json", "--key", "owner-b.key", "--out", "b.wai-check.json"]), true)); + } #[cfg(feature = "generate")] { let model = wai_dir().join("wai-rs/tests/vectors/generate/generate_spectral_model.json"); diff --git a/wai/wai-studio/src/actions.generated.js b/wai/wai-studio/src/actions.generated.js index 53de43d29..0fe72f926 100644 --- a/wai/wai-studio/src/actions.generated.js +++ b/wai/wai-studio/src/actions.generated.js @@ -1,7 +1,7 @@ // GENERATED by scripts/actions.mjs from the WAI operation catalogue. Do not edit: change the // catalogue (wai-rs/src/catalogue.rs, exported by `wai ops export`), then run // `node scripts/actions.mjs`. `node scripts/actions.mjs --check` fails when this file differs. -export const CATALOGUE = {"format":"wai.ops/1","toolchain":"0.4.0","sha256":"0ef1aab4f7b44a109dc7a0168c9394b0338458fe5fd23c75feb8892818304e1a"}; +export const CATALOGUE = {"format":"wai.ops/1","toolchain":"0.4.0","sha256":"ab16d7ceb6b7dfd8f50d469bc6efbb7451fee513da067a4408aeec078c4ffa10"}; export const ACTIONS = [ {"id":"toolchain.test","noun":"toolchain","verb":"test","menu":"Toolchain","item":"Test","palette":"toolchain test","summary":"WAI's four steps (name, dispatch, rebuild, receipt) on a picture made here, its receipt checked offline","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai hello","mcp":null,"route":null,"call":"toolchain_test"}, {"id":"project.create","noun":"project","verb":"create","menu":"Project","item":"Create","palette":"project create","summary":"A new project from one of the examples, written into a new or empty directory; --list names the templates","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai create","mcp":null,"route":null,"call":"project_create"}, @@ -99,6 +99,9 @@ export const ACTIONS = [ {"id":"contract.update","noun":"contract","verb":"update","menu":"Contract","item":"Update","palette":"contract update","summary":"An owner's countersignature added to a share contract, after its rules and the operator's signature are checked","placement":"local","effect":"writes-project","stability":"preview","toolset":"core","cli":"wai contract accept","mcp":null,"route":null,"call":"contract_update"}, {"id":"statement.create","noun":"statement","verb":"create","menu":"Statement","item":"Create","palette":"statement create","summary":"A party's revenue-share statement for a period, recomputed from the ledger's invoices and the channel's program-as-run, signed with the operator key","placement":"local","effect":"writes-project","stability":"preview","toolset":"core","cli":"wai statement create","mcp":null,"route":null,"call":"statement_create"}, {"id":"statement.verify","noun":"statement","verb":"verify","menu":"Statement","item":"Verify","palette":"statement verify","summary":"A revenue-share statement's verification document checked: every line recomputed from the public records, or the reason it is refused","placement":"local","effect":"reads","stability":"preview","toolset":"core","cli":"wai statement verify","mcp":null,"route":null,"call":"statement_verify"}, + {"id":"opportunity.create","noun":"opportunity","verb":"create","menu":"Opportunity","item":"Create","palette":"opportunity create","summary":"A party's opportunity report for a period: the ad breaks a channel's signed cues and program-as-run prove in the programmes it shares in, signed with the issuer key","placement":"local","effect":"writes-project","stability":"preview","toolset":"core","cli":"wai opportunity create","mcp":null,"route":null,"call":"opportunity_create"}, + {"id":"opportunity.verify","noun":"opportunity","verb":"verify","menu":"Opportunity","item":"Verify","palette":"opportunity verify","summary":"An opportunity report's or a statement check's verification document checked: every break, line and flag recomputed from the records, or the reason it is refused","placement":"local","effect":"reads","stability":"preview","toolset":"core","cli":"wai opportunity verify","mcp":null,"route":null,"call":"opportunity_verify"}, + {"id":"statement.check","noun":"statement","verb":"check","menu":"Statement","item":"Check","palette":"statement check","summary":"A platform's reported statement checked against an opportunity report and, when given, the operator's signed statement, signed with the issuer key","placement":"local","effect":"writes-project","stability":"preview","toolset":"core","cli":"wai statement check","mcp":null,"route":null,"call":"statement_check"}, {"id":"ops.list","noun":"ops","verb":"list","menu":"Ops","item":"List","palette":"ops list","summary":"Every operation of this catalogue: its id, command line, effect and whether this build holds it","placement":"local","effect":"reads","stability":"stable","toolset":"core","cli":"wai ops list","mcp":null,"route":null,"call":"ops_list"}, {"id":"ops.search","noun":"ops","verb":"search","menu":"Ops","item":"Search","palette":"ops search","summary":"The operations that match a task, ranked, offline: up to five","placement":"local","effect":"reads","stability":"stable","toolset":"core","cli":"wai ops search","mcp":null,"route":null,"call":"ops_search"}, {"id":"ops.show","noun":"ops","verb":"show","menu":"Ops","item":"Show","palette":"ops show","summary":"One operation's entry: its input and output schemas, effect, placement and command line","placement":"local","effect":"reads","stability":"stable","toolset":"core","cli":"wai ops show","mcp":null,"route":null,"call":"ops_show"}, -- 2.54.0 (Apple Git-157) From 722cf820ead59142c11e217437c683d102eea6b3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 29 Oct 2026 18:33:48 -0400 Subject: [PATCH 4/5] =?UTF-8?q?wai=20opportunity=5Freport:=20clippy=20?= =?UTF-8?q?=E2=80=94=20a=20section's=20own=20error=20type,=20a=20type=20fo?= =?UTF-8?q?r=20where=20a=20break=20is=20placed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/opportunity_report.rs | 7 +++++-- wai/wai-rs/src/opportunity_report/section.rs | 14 +++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/wai/wai-rs/src/opportunity_report.rs b/wai/wai-rs/src/opportunity_report.rs index 3371fcfa8..f3de7efc9 100644 --- a/wai/wai-rs/src/opportunity_report.rs +++ b/wai/wai-rs/src/opportunity_report.rs @@ -647,8 +647,11 @@ fn agrees(st: §ion::States, cue: &events::Cue) -> bool { st.duration.is_none_or(|d| d / 90 == cue.duration_ms as u64) } +/// Where a break is attributed (§6): its run's identity, its entry, its asset and its split. +type Placed = ([u8; 32], u64, Option, u16); + /// §6: the run and entry of an instant, the asset and the split; `None` is `outside_window`. -fn placed(c: &Contract, runs: &[ChannelReceipt], ids: &[[u8; 32]], t: u64) -> Option, u16), &'static str>> { +fn placed(c: &Contract, runs: &[ChannelReceipt], ids: &[[u8; 32]], t: u64) -> Option> { let r = runs.iter().position(|r| us(r.window_start_ms) <= t as u128 && (t as u128) < us(r.window_end_ms))?; let sched = &runs[r].schedule; let e = sched.iter().position(|x| us(x.start_ms) <= t as u128 && (t as u128) < us(x.start_ms) + us(x.duration_ms))?; @@ -656,7 +659,7 @@ fn placed(c: &Contract, runs: &[ChannelReceipt], ids: &[[u8; 32]], t: u64) -> Op } /// §6: the asset an entry gives, and its split. -fn attributed(c: &Contract, run: &ChannelReceipt, id: [u8; 32], e: usize) -> Result<([u8; 32], u64, Option, u16), &'static str> { +fn attributed(c: &Contract, run: &ChannelReceipt, id: [u8; 32], e: usize) -> Result { let asset = share::asset_at(&run.schedule, e, c.rule).map(str::to_string); let split = match &asset { Some(a) => c.split_of(a.as_bytes()).ok_or(ASSET_UNMAPPED)?, diff --git a/wai/wai-rs/src/opportunity_report/section.rs b/wai/wai-rs/src/opportunity_report/section.rs index 065b4de34..918b17cad 100644 --- a/wai/wai-rs/src/opportunity_report/section.rs +++ b/wai/wai-rs/src/opportunity_report/section.rs @@ -188,8 +188,16 @@ fn segmentation(b: &[u8], from: usize, to: usize) -> Result { Ok(Segmentation { event_id, cancelled, duration, type_id, sub }) } -/// Read a section (§5.1) for what it states (§5.2); `Err` is `cue_section_invalid`. -pub fn read(s: &[u8]) -> Result { +/// A section that does not read (§5.1): `cue_section_invalid`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Invalid; + +/// Read a section (§5.1) for what it states (§5.2). +pub fn read(s: &[u8]) -> Result { + read_section(s).map_err(|()| Invalid) +} + +fn read_section(s: &[u8]) -> Result { let n = s.len(); if n < 20 { return Err(()); @@ -279,7 +287,7 @@ struct Out { impl Out { fn put(&mut self, n: usize, v: u64) { for i in (0..n).rev() { - if self.bits % 8 == 0 { + if self.bits.is_multiple_of(8) { self.b.push(0); } let bit = ((v >> i) & 1) as u8; -- 2.54.0 (Apple Git-157) From d9839467e2522b09abe8293f8e231f7b0f6c03de Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 29 Oct 2026 18:45:03 -0400 Subject: [PATCH 5/5] =?UTF-8?q?opportunity=5Freport=5Fverify.py:=20every?= =?UTF-8?q?=20refusal=20of=20the=20corpus=20accounted=20for=20by=20=C2=A71?= =?UTF-8?q?3,=20its=20reused=20codes=20or=20a=20delegated=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So a reason left out of §13's table is caught on this side too, as the Rust test that §13 lists exactly the reference's codes catches it. Co-Authored-By: Claude Opus 5.5 --- wai/live-conformance/opportunity_report_verify.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wai/live-conformance/opportunity_report_verify.py b/wai/live-conformance/opportunity_report_verify.py index 6c3627511..6f5fac4b1 100644 --- a/wai/live-conformance/opportunity_report_verify.py +++ b/wai/live-conformance/opportunity_report_verify.py @@ -1067,6 +1067,14 @@ def main(corpus_dir): if missing: fails.append("no vector is refused %s" % missing) print("%s every reason of §13 and every reused code refuses a vector (%d listed)" % ("PASS" if not missing else "FAIL", len(listed))) + # And no refusal is unaccounted for: each is §13's, a code §13 reuses, or a code a text it + # delegates to (revenue-share, live-ads) raises there. + delegated = set(RSV.reasons_listed()) | set(RSV.REUSED) | set(LAV.REASONS) + unexplained = sorted(r for r in seen if r is not None and r not in listed and r not in REUSED and r not in delegated) + if unexplained: + fails.append("refusals §13 does not account for: %s" % unexplained) + print("%s every refusal of the corpus is a reason of §13, a code it reuses, or one a text it delegates to raises" % ( + "PASS" if not unexplained else "FAIL")) muts = mutations(doc) weak = [name for name, m in muts if not check(m, quiet=True)] print("%s every mutated copy of the corpus fails (%d copies)" % ("PASS" if not weak else "FAIL", len(muts))) -- 2.54.0 (Apple Git-157)