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/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..d82977666 --- /dev/null +++ b/wai/live-conformance/opportunity/vectors.json @@ -0,0 +1,5714 @@ +{ + "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", + "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", + "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_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", + "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_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", + "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": "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", + "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": "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", + "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/live-conformance/opportunity_report_verify.py b/wai/live-conformance/opportunity_report_verify.py new file mode 100644 index 000000000..6f5fac4b1 --- /dev/null +++ b/wai/live-conformance/opportunity_report_verify.py @@ -0,0 +1,1110 @@ +#!/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))) + # 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))) + 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/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/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_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/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/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/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..f3de7efc9 --- /dev/null +++ b/wai/wai-rs/src/opportunity_report.rs @@ -0,0 +1,1551 @@ +//! 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) +} + +/// 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> { + 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 { + 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..176b67007 --- /dev/null +++ b/wai/wai-rs/src/opportunity_report/corpus.rs @@ -0,0 +1,853 @@ +//! 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 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])); + 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()))); + 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); + 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"]); + 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: 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); + } + 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_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()), + 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"), + 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()), + 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("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"]), + 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..918b17cad --- /dev/null +++ b/wai/wai-rs/src/opportunity_report/section.rs @@ -0,0 +1,476 @@ +//! 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 }) +} + +/// 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(()); + } + 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.is_multiple_of(8) { + 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/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-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"); +} 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"},