Skip to main content

WAI Extension: Revenue Share

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

Status: Draft. Registers the record wai.delivery.revenue_share: what each content owner of a linear channel is owed from the ads sold in the channel’s breaks, as a statement anyone holding the public records recomputes. An operator and every owner it names sign a share contract, WRC1: its period, its channel and the key that signs the channel’s program-as-run, the carriers whose ad evidence belongs to the channel, the map from each asset to a split of integer millionths among owners and the operator, and the rule for a break that falls between programmes. The operator signs a statement, WRS1, for one party and one period: the invoices of receipt-ledger it rests on, the program-as-run windows of wai.video.channel it places impressions in, and one line per split and class, each with the impressions attributed, the revenue the invoices’ tariffs priced them at, the party’s share of it divided by the largest-remainder rule, and the records the line came from. A verifier given the contract, the windows and the ledgers’ own verification documents recomputes every line and refuses a statement that does not recompute. No new transport, no payment and no currency conversion: a statement is the recomputable figures a payment to an owner rests on. The reference implementation is the revenue_share module of wai-rs (feature revenue_share); the corpus is live-conformance/share/, and its second implementation is live-conformance/revenue_share_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: the WAI texts this one cites; ISO 4217 (the alphabetic currency code); RFC 3339 §5.6, for the date-time form of §5.4; RFC 8032; RFC 8259; FIPS 180-4.

Why the name: the extension states how revenue is shared, and the record names both of its parts, the contract and the statement, under one string, as wai.delivery.ledger names its tariff and its invoice.

1. Scope

In scope: the share contract, its splits, its signatures and when two contracts overlap (§3); the program-as-run as a verifier is given it, the JSON carriage of a wai.video.channel receipt, whose signed bytes this text writes out and does not change (§4); the evidence, invoices of the receipt ledger and the impressions their sources state, with their instants (§5); attribution, from an impression’s instant to an asset and its split (§6); the statement, its lines, rounding and citations (§7); verification (§8); how a statement composes with the ledger (§9); and the operations (§10).

Out of scope: a transport; paying an owner, an owner’s account and any contact with a party outside the records; invoicing a buyer, which the receipt ledger does; converting a currency; how an operator chooses an owner’s share; placing in time a presentation whose marker states no time this text reads (§5.4), which is kept in a class of its own; and the records themselves, which their own extensions define, verify and leave as they are. The channel receipt keeps its signed bytes: §4 writes them out for a verifier that is not wai-rs.

Relationship to the core spec: wai.delivery.revenue_share is a record (SPEC §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable.

2. Terms

3. The share contract

3.1 Layout

fieldsizemeaning
magic4WRC1 (ASCII)
version11 in this revision
operator32the operator’s key, which signs the contract and every statement under it
start, end8 eachu64: the period the contract is in force
currency3three capitals A–Z (ISO 4217): the currency of every invoice it shares
channel2 + na text field of 1 to 1024 bytes: the channel_id of the channel’s runs
channel_signer32the key that signs the channel’s runs
rule1for an impression in an entry that is not a programme (§6.2): 1 preceding, 2 following, 3 between
between2u16: the index of the between split (§6.2, §6.3)
carriers2 + 33 × ca u16 count, then each carrier: kind (u8: 1 a room, 2 a stitcher) and id (32: the room id, or the stitcher’s key), in strictly ascending order of (kind, id)
owners1 + 32 × oa u8 count, then each owner’s key, in strictly ascending order
splits2 + …a u16 count, then each split (§3.2)
assets4 + …a u32 count, then each asset: asset (a text field of 1 to 1024 bytes, an asset_id of the channel’s schedule) and split (u16, the index of its split), in strictly ascending order of asset’s bytes
operator_sig64§3.3
countersigs64 × oeach owner’s countersignature, in the order of owners (§3.3)

3.2 Splits

A split divides revenue among parties:

fieldsizemeaning
operator_ppm4u32: the operator’s share, 0 to 10^6
m1u8: the owners it names
shares5 × meach: owner (u8, the owner’s index in owners) and ppm (u32, its share), in strictly ascending order of owner

The parties of a split, in their order, are its owners in the order it lists them, then the operator when operator_ppm is above 0. A party holds a share of a split when it is one of its parties. The shares of a split sum to 10^6 exactly: a split is the whole of the revenue it is given, and no part of it is left unassigned.

3.3 Digest and signatures

The body of a contract is its bytes before operator_sig, and its digest is SHA-256 over the body. operator_sig is Ed25519 by operator over the bytes wai:share-contract (ASCII) and 0x01, followed by the body. Each owner’s countersignature is Ed25519 by that owner over the bytes wai:share-accept (ASCII) and 0x01, followed by the body; 64 zero bytes in its place state that the owner has not countersigned. An operator offers a contract with every countersignature zero, and each owner fills its own. Every signature is verified strictly, as SPEC §7 Signatures requires: a key that fails step 1 or 2 is bad_key, and a signature that fails a later step is bad_signature.

A contract is in force only when every owner it names has countersigned it: the splits and the asset map bind each owner only by its own signature, and a contract some owner did not sign shares nothing.

3.4 Reading and rules

A reader reads exactly the bytes it is given as one contract, in the order of §3.1, and refuses at the first of these that holds: fewer than 4 bytes, truncated; a magic other than WRC1, malformed; no fifth byte, truncated; a version other than 1, unknown_version; the bytes end before a field the layout requires, truncated; bytes after the last countersignature, malformed. It then refuses as contract_invalid a contract that breaks any of these:

It refuses as split_not_whole a contract with a split whose operator_ppm and ppms do not sum to 10^6. Then it verifies operator_sig (bad_key, bad_signature), then each countersignature in turn: one of 64 zero bytes is not_countersigned, and one that does not verify is bad_key or bad_signature. A refusal of a signature names the operator, or the owner and its index.

3.5 Contracts that overlap

Two contracts overlap when their periods share an instant and they name the same channel, or a carrier in common. An asset aired under two contracts at once would have two owners, and a statement under either would share revenue the other also shares; a verifier holding both refuses a statement under either (§8.2 step 2, contract_overlap). An operator MUST NOT sign two contracts that overlap; one that changes a contract’s terms signs a new contract whose period begins where the old one’s ends.

4. The program-as-run

4.1 Its JSON carriage

A run is carried as one JSON object (RFC 8259) with exactly these members; a byte field is lowercase hexadecimal of its exact length, and an integer a JSON integer from 0 to 2^64 − 1:

membervalue
channel_ida string: the channel
schedule_root32 bytes: §4.2
schedulean array of entries, each {"asset_id", "start_ms", "duration_ms", "kind"}: asset_id a string, kind one of program, ad and filler
window_start_ms, window_end_msthe window
airedan array of integers: the indices of the entries on air in the window
work_schedule_opsthe entries’ count, as the receipt states it
joules_microthe assembly’s energy, or 0 unmetered
energy_provenanceits class’s label (energy-measurement §2.7: ModelBased, Estimator, OnChipCounter, CalibratedInstrument), or null unmetered
energy_uncertainty, energy_counter_filtering, energy_calibration_refonly for a class that carries them, as live-ads §8.2 writes them
grant{"grant_hash", "capability", "joule_ceiling_micro", "funds_ceiling"}, funds_ceiling null for none
parent_receipt_hash32 bytes: the run before it in the channel’s chain, or null
signer_pubkey32 bytes: the channel signer’s key
signer_ida string
sig64 bytes: §4.2

A run reads when it is such an object and each entry’s start_ms + duration_ms is at most 2^64 − 1. The legacy class label HwShunt (energy-measurement §2.6) does not read in this carriage.

4.2 Its signed bytes and identity

These are the bytes wai.video.channel signs, written out; BLAKE3 throughout, integers big-endian:

4.3 When a run verifies

A run verifies when:

  1. schedule_root is the schedule root of its entries, and work_schedule_ops their count;
  2. window_start_ms is below window_end_ms;
  3. the schedule is continuous: each entry starts where the one before it ends;
  4. aired is exactly the indices, ascending, of the entries that touch the window: those whose start_ms is below window_end_ms and whose start_ms + duration_ms is above window_start_ms;
  5. the figure and its class cohere as live-ads §8.5 rule 5 states it; and
  6. its signature verifies.

So the program-as-run is the schedule the channel signer signed, and what aired in a window is recomputed from it rather than read from a log.

5. The evidence

5.1 Invoices

A statement rests on invoices of the receipt ledger, each given as its ledger document (receipt-ledger §7.1): the tariff, the invoice, the invoice before it in its chain, and the sources. A ledger document is accepted when the receipt ledger’s verification (§7.2 there) accepts it. Its tariff states the prices, its invoice the period and the records, and its sources the impressions. A statement prices nothing itself: an impression’s revenue is the price the invoice billed it at.

Each invoice of a statement is one the operator sold as the seller of its tariff, in the contract’s currency; the buyer is whoever bought the channel’s breaks under that tariff. A channel whose breaks several buyers bought rests on several invoices, one chain per tariff.

5.2 Impressions and their classes

An accepted ledger document contributes, from its sources in order:

Record kinds are receipt-ledger §5.2’s. Every other source, and every impression of a carrier the contract does not name, contributes nothing: an invoice may bill a buyer for several channels, and a statement shares the impressions of its own.

An impression’s price is its invoice’s tariff’s ads price (receipt-ledger §4.1): per presented impression for the classes presented and unplaced, the unit presented_impressions bills; per billable impression for the class beacons, the unit billable_impressions bills. So an impression is priced at the unit the invoice billed it under, and the classes keep apart what the players’ own claims show presented and what rests on a decider’s beacons alone. A class is never summed into another: each has its own lines (§7.2).

5.3 Its instant

An impression’s instant places it on the channel’s timeline:

5.4 The instant a marker states

A marker of carriage 1 or 2 (stitched-presentation §3.1) carries the START-DATE of its date range, the second long field of its body. It states an instant when it is exactly this form, in ASCII, of RFC 3339 §5.6’s date-time with its letters in capitals:

YYYY "-" MM "-" DD "T" hh ":" mm ":" ss [ "." 1 to 9 digits ] ( "Z" / ( "+" / "-" ) hh ":" mm )

with YYYY a year of four digits, MM 01 to 12, DD 01 to the days of that month in the Gregorian calendar, hh 00 to 23, mm 00 to 59 and ss 00 to 59 (an offset’s hh and mm the same), and the instant it names, in microseconds, at or after 1970-01-01T00:00:00Z. The instant is the date and time less the offset, its fraction truncated to the microsecond. A marker of carriage 3 carries no date (§3.4 there): its presentations are unplaced, as are those of a START-DATE that does not state an instant.

6. Attribution

6.1 The run of an instant

The runs a verifier is given are of the contract’s channel and signer and do not overlap (§8.2 step 4). The run of an instant is the run whose window holds it; the entry of the instant is the entry of that run’s schedule whose span holds it. An impression with an instant and no run, or a run and no entry (an instant past its schedule’s end), aired in no window the verifier holds: it is refused outside_window.

6.2 The asset and its split

The entry of an impression’s instant gives its asset:

An impression with an asset is attributed to the asset’s split in the contract’s assets; an asset the contract does not map is refused asset_unmapped. One with no asset is attributed to the between split. The rule applies to the run’s own signed schedule, so anyone holding the run finds the same programme.

6.3 Unplaced impressions

An unplaced impression is attributed to the between split, in the class unplaced. It is revenue the channel earned in some break that no record places in a programme, and the contract’s between split is how its parties agreed to share such revenue. It is never counted in the class presented: a statement states it apart, and an owner sees how much of its revenue no record placed.

7. The statement

7.1 Layout

fieldsizemeaning
magic4WRS1 (ASCII)
version11 in this revision
contract32the contract’s digest (§3.3)
party32the party it is for: the operator or an owner
start, end8 eachu64: the period it states
currency3the contract’s
invoices2 + 32 × ia u16 count, then each invoice’s digest (receipt-ledger §6.7), in strictly ascending order
runs2 + 32 × ra u16 count, then each run’s identity (§4.2), in strictly ascending order
lines2 + 67 × la u16 count, then each line (§7.2), in strictly ascending order of (split, class)
total8u64: the sum of the lines’ amounts
sig64§7.5

7.2 Lines

A line is 67 bytes:

fieldsizemeaning
split2u16: the split’s index
class11 presented, 2 beacons, 3 unplaced (§5.2)
impressions8u64: the impressions of the class attributed to the split
revenue8u64: the sum of their prices
ppm4u32: the party’s share of the split
amount8u64: the party’s part of the revenue (§7.3)
cited4u32: the records the line cites (§7.4)
cites32their citation digest (§7.4)

There is one line for each split of which the party holds a share and each class with at least one impression attributed to that split, and none other. A party’s statement so states the whole revenue of every split it shares in, and its part of each.

7.3 Revenue, shares and rounding

A line’s revenue is divided among the parties of its split by the largest-remainder rule, so that the parts are integers and sum to the revenue exactly:

  1. For each party i of the split, in its order (§3.2), its quota is revenue × ppmᵢ, computed in integers wide enough not to overflow; its base is ⌊quota / 10^6⌋, and its remainder is quota − base × 10^6.
  2. The leftover is revenue less the sum of the bases. It is less than the count of parties.
  3. The parties with the largest remainders, as many as the leftover, each receive one more than their base; between equal remainders, the earlier party in the order receives it first.
  4. A party’s part is its base, plus one when step 3 gives it one.

The parts of a split’s parties sum to its revenue, whoever’s statement states them: the operator’s statement and each owner’s state the same revenue for a split and class, and their parts sum to it. Rounding is applied once per split and class over the statement’s period, never per impression, so it does not accumulate.

7.4 Citations

Each line cites the records it came from: for each impression attributed to its split in its class, the impression’s records (§5.2), the run that placed it (kind 10, its identity) when it has an instant, and the invoice that billed it (kind 11, its digest). cited is their number, each record counted once, and cites is SHA-256 over the bytes wai:share-cites (ASCII) and 0x01, followed by each cited record’s kind (u8) and identity (32), in ascending order of (kind, identity). A line that cites no record has no source (line_unsourced, §8.2 step 7). An owner shows one line to an auditor with the records it cites, the contract and the runs. Kinds 10 and 11, like every kind a citation digest names, are registered in receipt-ledger §5.2.

7.5 Digest and signature

The digest of a statement is SHA-256 over its bytes before sig. Its signature is Ed25519 by the contract’s operator over the bytes wai:share-statement (ASCII) and 0x01, followed by the bytes the digest covers, verified strictly (SPEC §7 Signatures).

7.6 Reading and rules

A reader reads exactly the bytes it is given as one statement, as §3.4 reads a contract with WRS1 for WRC1 (truncated, malformed, unknown_version), and refuses as share_statement_invalid one that breaks any of these:

Its signature is checked against the contract’s operator (§8.2 step 3).

8. Verification

8.1 Inputs

A verifier is given the contract; the other contracts it holds of the operator or of the channel, for the overlap check; the statement; the runs; and the ledger documents. Its verification document holds them as one JSON object with exactly the members contract and statement (hexadecimal), contracts (a list of hexadecimal strings), runs (a list of runs in their carriage, §4.1) and ledgers (a list of ledger documents, receipt-ledger §7.1). 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 (§3.4), at contract; a signature’s refusal at contract and operator, or contract, owner and the owner’s index: truncated, malformed, unknown_version, contract_invalid, split_not_whole, bad_key, bad_signature, not_countersigned.
  2. The other contracts, each at contracts and its index, a signature’s place after it: each is read and verified as step 1, with those reasons; one whose digest is the contract’s is the contract and is passed over; one that overlaps the contract (§3.5) is contract_overlap.
  3. The statement, at statement: it reads (§7.6: truncated, malformed, unknown_version, share_statement_invalid); its contract is the contract’s digest, its party the operator or an owner of the contract, and its currency the contract’s: otherwise contract_mismatch; its period lies within the contract’s: otherwise outside_contract; its signature verifies under the operator (§7.5): bad_key, bad_signature.
  4. The runs, each at run and its index: it reads (§4.1: malformed); it verifies (§4.3: run_invalid); its channel_id is the contract’s channel and its signer_pubkey the channel_signer: otherwise run_foreign; its window shares an instant with the window of a run before it: run_overlap.
  5. The ledger documents, each at ledger and its index: it is verified as receipt-ledger §7.2 verifies one, a refusal being that extension’s reason with its place after the index; its invoice’s seller is the operator and its currency the contract’s: otherwise invoice_foreign; its period lies within the statement’s: otherwise outside_period; and no record of kinds 6 to 9 in its invoice’s records is in the records of an invoice before it, and no ad receipt of its sources names the room, the cue and the session an ad receipt of a document before it names: otherwise impression_duplicate. A ledger refuses a record billed twice within one invoice and one chain (receipt-ledger §5.2, §6.9); this step refuses one billed by two invoices of a statement, under two tariffs among them.
  6. The impressions (§5.2), in the order the documents give them, each at ledger, its index, source and the source’s index: an impression with an instant (§5.3) whose instant does not lie within the statement’s period is outside_period; one that aired in no window (§6.1) is outside_window; one whose asset the contract does not map (§6.2) is asset_unmapped.
  7. The figures, at statement, recomputed from the impressions as §7.2 to §7.4 compute them for the statement’s party, and compared in this order:
    1. a line whose split and class give the party no impression, or that states more impressions than they give: line_unsourced, a line no signed record covers;
    2. a line whose cited or cites is not the records it came from: citation_mismatch;
    3. any other difference (the invoices, the runs, a line missing, fewer impressions, a revenue, a ppm, an amount, the total): share_statement_mismatch.

A figure that cannot be held in its field is a difference.

8.3 Outcome

The division is not signed: anyone holding the records computes it, and it shows that the parts of a split sum to its revenue.

9. The statement and the receipt ledger

A statement composes with the ledger and duplicates none of it:

Paying an owner its part, and an owner invoicing the operator for it, are not sources the ledger defines, and this revision defines none.

10. Operations

wai runs the operations of this extension, under the agent rules of the operation catalogue (the --json envelope, the exit codes, --dry-run on every operation that writes, --yes before a file is replaced). A refusal by this text has its reason as the operation’s code.

operationcommand linedoes
contract.createwai contract create <terms.json> --key <file> [--out F]the operator’s contract from its terms, signed by the operator with every countersignature zero (§3.3)
contract.updatewai contract accept <contract> --key <file> [--out F]an owner’s countersignature added, after the contract’s rules and the operator’s signature are checked
statement.createwai statement create <inputs.json> --party <key> --start µs --end µs --key <file> [--out F]the statement of a party and a period, computed from a verification document without its statement (§8.1), signed by the operator, written with its document
statement.verifywai statement verify <document.json>a verification document’s outcome (§8.3); exit 1 with the reason on a refusal

11. Reasons

The codes this extension adds, each with where it is raised:

reasonraised at
contract_invalid§3.4, §8.2 steps 1 and 2
split_not_whole§3.2, §3.4, §8.2 steps 1 and 2
not_countersigned§3.3, §8.2 steps 1 and 2
contract_overlap§3.5, §8.2 step 2
share_statement_invalid§7.6, §8.2 step 3
contract_mismatch§8.2 step 3
outside_contract§8.2 step 3
run_invalid§4.3, §8.2 step 4
run_foreign§8.2 step 4
run_overlap§6.1, §8.2 step 4
invoice_foreign§5.1, §8.2 step 5
impression_duplicate§8.2 step 5
outside_window§6.1, §8.2 step 6
asset_unmapped§6.2, §8.2 step 6
share_statement_mismatch§8.2 step 7

A verifier of this extension also raises, with the meaning this text gives them: truncated, malformed and unknown_version for a contract or a statement that does not read, and malformed for a document or a run that does not read; bad_key and bad_signature for their signatures; outside_period (receipt-ledger §10) for an invoice or an impression outside the statement’s period; line_unsourced (fleet §10) for a line no impression gives; citation_mismatch (receipt-ledger §10) for a line’s citations; and, at a ledger document, every code the receipt ledger raises (receipt-ledger §10 and the codes it lists).

12. Conformance

live-conformance/share/ holds the corpus, in one file, vectors.json, written by wai_revenue_share_vectors gen from fixed seeds and from the sources of the ledger corpus (live-conformance/ledger/): a room’s break-form receipts under a policy that requires claims and under one that does not, and three streams stitched by others, one of whose markers are DASH events. The ledger documents, the runs and the contracts are held once, by name, and each vector is a verification document (§8.1) naming them, with the outcome §8.3 gives it:

wai_revenue_share_vectors verify and the test tests/revenue_share.rs check that the file regenerates byte for byte, that wai-rs gives every vector its outcome, that every reason of §11 refuses a vector, and that §11 lists exactly the reference’s codes; the test also holds the worked example, three statements of one month computed and then recomputed from the public records alone, and the module’s unit tests the rounding rule’s ties. live-conformance/revenue_share_verify.py is the second implementation, written from this text: it shares no code with wai-rs, verifies signatures with WAI’s strict rule written from RFC 8032 (claim-conformance/_ed25519.py) and BLAKE3 with claim-conformance/_blake3.py, verifies each ledger document with that corpus’s own second implementation (receipt_ledger_verify.py), reads every contract, run, marker and statement itself, recomputes every attribution, line, division and citation, 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.

13. Security and privacy considerations