A stitched source (extensions/stitched-presentation.md, PR #470, which this branch merges): the stitcher's pod-input records (WSI1) and the players' claims (WSC1), verified by that extension with the tariff's ad scope as its signers. Its presentations bill as presented impressions, each line citing the accepted records and the presented own claims; its report is that extension's §7.5 statement unchanged, a form-1 receipt's own quartiles and spend beside the presented ones. The corpus bills two vectors of live-conformance/stitched/ and refuses a stitched source given twice and a stitcher outside the scope. Co-Authored-By: Claude Opus 5.5 <[email protected]>
569 lines
36 KiB
Markdown
569 lines
36 KiB
Markdown
# WAI Extension: Receipt Ledger
|
||
|
||
> Status: Draft. Registers the record `wai.delivery.ledger`: invoice lines a customer can
|
||
> recompute from the signed records WAI already defines. A seller and a buyer both sign a
|
||
> **tariff**, `WLT1`: one currency, and one price for each unit those records state, the
|
||
> units of a room session ([live-metering](live-metering.md) §4.4), the acknowledged bytes of
|
||
> each object class ([settlement](settlement.md) §3.3), the packages, entries and stored bytes
|
||
> of a package receipt ([packaging](packaging.md) §9), the counted impressions of a
|
||
> `wai.video.ssai` receipt of the break form ([live-ads](live-ads.md) §8,
|
||
> [ad-presentation](ad-presentation.md) §9.2) and the presentations of ads another party
|
||
> stitched ([stitched-presentation](stitched-presentation.md) §7.4); and the **scope** it
|
||
> covers, the rooms, the delivering parties, the packagers, and the ad deciders and stitchers
|
||
> whose records are billed under it. The
|
||
> ledger verifies each **source** by the extension that defines it, and the seller signs an
|
||
> **invoice**, `WLI1`, for a period: the records it was computed from, one line per source,
|
||
> unit and price, each line citing the records it came from, and the joules those records
|
||
> state, per scope and per acquisition class with their largest declared uncertainty, never
|
||
> summed across classes or scopes. Invoices under one tariff form a chain, and no record is
|
||
> billed in two of them. A verifier given the tariff, the invoice, the invoice before it and
|
||
> the sources recomputes every line and refuses an invoice that does not recompute. Beside the
|
||
> invoice, the ledger states two **reports**: the ad decisions reconciled against the players'
|
||
> presentation claims (for a stitched stream, stitched-presentation §7.5's own statement), and
|
||
> the delivery of each party under the one tariff with the bytes two parties were acknowledged
|
||
> for twice. No new transport, no payment and no tax: an invoice is
|
||
> the recomputable figures a payment rests on. The reference implementation is the
|
||
> `receipt_ledger` module of `wai-rs` (feature `receipt_ledger`) and the service
|
||
> `wai/wai-ledger`; the corpus is [`live-conformance/ledger/`](../live-conformance/ledger/),
|
||
> and its second implementation is `live-conformance/receipt_ledger_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 8032; RFC 8259; FIPS 180-4.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: how one tariff prices rooms, packages, HTTP and relayed delivery and ads together
|
||
(§3); the tariff, its two signatures and its scope (§4); the sources a ledger is given and what
|
||
each contributes (§5); the invoice, its records, lines, citations and energy, its signature and
|
||
its chain (§6); verification (§7); the two reports (§8); and the reference service (§9).
|
||
|
||
Out of scope: a transport; payment, a payment rail, tax and any contact with a party outside the
|
||
records; how a seller chooses its prices; how a party measures its energy, which
|
||
[energy-measurement](energy-measurement.md) governs; and the records themselves, which their own
|
||
extensions define, verify and leave as they are. The live-metering tariff and the settlement
|
||
statement's tariff keep their meaning: this extension reads them, and changes neither.
|
||
|
||
Relationship to the core spec: `wai.delivery.ledger` 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
|
||
|
||
- **Seller**, **buyer**: Ed25519 keys (RFC 8032). The seller operates the ledger and signs its
|
||
invoices; the buyer is the customer billed.
|
||
- **Time**: microseconds since 1970-01-01T00:00:00Z, the unit of a room's `t0` (live-rooms §3.1).
|
||
A **period** is a span of time from its `start` (included) to its `end` (excluded).
|
||
- **Minor unit**: a currency's smallest unit (ISO 4217); an **amount** is an integer of
|
||
millionths of one, as settlement §7.3's amounts are.
|
||
- **Source**: one record a ledger bills from, with what its own verifier is given (§5).
|
||
- **Record identity**: the 32 bytes a record of a kind is named by (§5.2).
|
||
- *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers of that many bits. A **digest** is
|
||
SHA-256 (FIPS 180-4), 32 bytes; **zero** of a key or digest field is 32 zero bytes.
|
||
- **Class**, **tag**, **unmetered marker**: an acquisition class of energy-measurement §2.7, its
|
||
tag byte (2 `ModelBased`, 3 `Estimator`, 4 `OnChipCounter`, 5 `CalibratedInstrument`), and a
|
||
figure of 0 with no class.
|
||
|
||
## 3. One tariff across rooms, packages and HTTP delivery
|
||
|
||
The records a deployment bills from state their units in their own terms. A room-session receipt
|
||
states microseconds and events (live-metering §4.4), and may state a `tariff` of its own, which
|
||
prices the session's grants against their funds ceilings (§4.6 there). A settlement statement
|
||
states acknowledged bytes by object class, whatever carried them: a relay over MOQT, or a cache
|
||
over HTTP whose deliveries were acknowledged as settlement §9.1 maps them; it may state a tariff
|
||
too, which prices its own lines (§7.3 there). A package receipt states entries and bytes
|
||
(packaging §9) and no tariff. An ad decision's receipt states impressions (live-ads §8.2) and
|
||
no tariff of the seller's; so does a stream another party stitched, whose stitcher signs a
|
||
pod-input record, `WSI1`, of the ads it chose and whose players sign a claim, `WSC1`, of each ad
|
||
presented (stitched-presentation §4, §5).
|
||
|
||
A tariff of this extension prices every one of those units in one currency, so one invoice bills
|
||
a buyer's rooms, deliveries, packages and ads together. Three rules keep every figure in it
|
||
consistent with what the records say:
|
||
|
||
1. **A record that states a tariff states this one.** A room-session receipt of a source either
|
||
states no tariff (currency zero) or the tariff's currency and its thirteen room prices, in
|
||
live-metering §4.1's order; a settlement statement either states no tariff or the tariff's
|
||
currency and its four delivery prices, in settlement §7.1's order. A record under another
|
||
tariff is refused `tariff_conflict` (§7.2 step 5): the funds its grants were held to, or the
|
||
amount its party stated, were computed at other prices, and an invoice at these prices would
|
||
contradict it.
|
||
2. **One rounding, the same everywhere.** Every amount is a quantity times a price, divided by
|
||
the unit's divisor and rounded down once per invoice line (§6.4), as settlement §7.3 rounds a
|
||
statement's line. A statement's own amount rounds once per payer; the invoice rounds once per
|
||
party and class, so the two can differ by less than one millionth of a minor unit per payer
|
||
line, and the delivery report (§8.2) states both.
|
||
3. **Units are never converted.** A room is billed in its own units, a delivery in bytes, a
|
||
package in packages, entries and stored bytes, an ad in impressions. No line turns one unit
|
||
into another, and no line is priced from joules: the energy of an invoice is stated beside
|
||
its lines (§6.6), per class, and is not an amount.
|
||
|
||
So the tariff is the one place prices live: a room receipt's funds verdicts, a statement's
|
||
amount and an invoice's lines all follow from it, and a buyer who holds the tariff and the
|
||
records recomputes all three.
|
||
|
||
## 4. The tariff
|
||
|
||
### 4.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WLT1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `seller` | 32 | the seller's key, which signs the tariff and every invoice under it |
|
||
| `buyer` | 32 | the buyer's key, which accepts the tariff by signing it |
|
||
| `start`, `end` | 8 each | u64: the period the tariff is good for |
|
||
| `currency` | 3 | three capitals `A`–`Z` (ISO 4217) |
|
||
| `room` | 52 | thirteen u32: millionths of a minor unit per second of `publisher_us`, of `subscriber_us` for each layer class 0 to 8, of `composition_us` and of `recorded_us`, and per event sent (live-metering §4.1's order) |
|
||
| `delivery` | 32 | four u64: millionths of a minor unit per 10^9 acknowledged bytes of each object class, in class order (settlement §7.1) |
|
||
| `package` | 24 | three u64: millionths of a minor unit per package receipt, per entry, and per 10^9 stored bytes |
|
||
| `ads` | 16 | two u64: millionths of a minor unit per presented impression, and per billable impression |
|
||
| `scope` | 2 + 33 × *n* | a u16 count *n*, then each entry: `kind` (u8) and `id` (32), in strictly ascending order of (`kind`, `id`) |
|
||
| `seller_sig` | 64 | §4.2 |
|
||
| `buyer_sig` | 64 | §4.2 |
|
||
|
||
### 4.2 Digest and signatures
|
||
|
||
The **digest** of a tariff is SHA-256 over its bytes before `seller_sig`, the **body**. Its
|
||
`seller_sig` is Ed25519 by `seller` over the 18 bytes `wai:ledger-tariff` (ASCII) and 0x01,
|
||
followed by the body; its `buyer_sig` is Ed25519 by `buyer` over the 18 bytes `wai:ledger-accept`
|
||
(ASCII) and 0x01, followed by the body. Both are verified strictly, as [SPEC](../SPEC.md) §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`. The buyer's signature is what binds the buyer to the prices: a
|
||
tariff the buyer did not sign bills nobody.
|
||
|
||
### 4.3 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given as one tariff, in the order of §4.1, and refuses at
|
||
the first of these that holds: fewer than 4 bytes, `truncated`; a magic other than `WLT1`,
|
||
`malformed`; no fifth byte, `truncated`; a version other than 1, `unknown_version`; the bytes end
|
||
before a field the layout requires, `truncated`; bytes after `buyer_sig`, `malformed`. It then
|
||
refuses as `tariff_invalid` a tariff that breaks any of these:
|
||
|
||
- `start` is below `end`; `currency` is three capitals `A`–`Z`;
|
||
- `seller` and `buyer` are not zero, and not the same key;
|
||
- `scope` has at least one entry, in strictly ascending order of (`kind`, `id`), each `kind` 1 to
|
||
4 and each `id` not zero.
|
||
|
||
Then the seller's signature, then the buyer's: `bad_key`, `bad_signature`.
|
||
|
||
### 4.4 Scope
|
||
|
||
A scope entry names whose records the tariff bills:
|
||
|
||
| kind | `id` | a source is in scope when |
|
||
|---|---|---|
|
||
| 1 | a room id | a room-session receipt's `room` is the `id` |
|
||
| 2 | a party's key | a settlement statement's `party` is the `id` |
|
||
| 3 | a packager's key | a package receipt's `signer_pubkey` is the `id` |
|
||
| 4 | an ad decider's or a stitcher's key | every receipt of an ads source has `signer_pubkey` the `id` (live-ads §3.4); a stitched source's records are accepted only when their `signer` is such an `id`: the verifier's **signers** (stitched-presentation §7.1) are the scope's entries of kind 4 |
|
||
|
||
The scope is what the buyer agreed to pay for: a record signed by a party the tariff does not name
|
||
is refused `outside_scope` (§7.2 step 5), whoever submits it.
|
||
|
||
## 5. Sources
|
||
|
||
### 5.1 Kinds and their documents
|
||
|
||
A source is a JSON object (RFC 8259) whose `kind` names its extension, holding exactly what that
|
||
extension's verifier is given. Bytes are lowercase hexadecimal strings.
|
||
|
||
| `kind` | members | verified as |
|
||
|---|---|---|
|
||
| `room` | `room` (`descriptor`, `grants`, `key_log_head` (a string or `null`), `chains`, a list of lists of records), `receipt`, `hops` (each `record` and `ledger`), `descriptors`, `compositions`, `takes`, `logs` (a string or `null`) | live-metering §5.1, its receipt being `receipt` |
|
||
| `delivery` | `statement`, `records` (each `record` and its `items`, one item list per entry), `proofs`, `hops`, `rooms` (each `descriptor` and `grants`) | settlement §8.2 |
|
||
| `package` | `receipt` (the text of `package.receipt.json`), `indexes` (the index files in the receipt's component order) | packaging §9, a receipt of version 2 against its indexes |
|
||
| `ads` | `descriptor`, `grants`, `key_log`, `chains`, `policy`, `pt0_ms`, `receipts` (form-2 receipts in their JSON carriage), `pods`, `claims`, `creatives`, `issuers` | live-ads §9, with ad-presentation §9 under a policy that requires claims |
|
||
| `stitched` | `records` (pod-input records), `claims` (stitched claims), `manifests`, `responses` (VAST and VMAP documents), `receipts` (form-1 receipts in their JSON carriage), `creatives`, `issuers` | stitched-presentation §7, its signers the tariff's (§4.4) |
|
||
|
||
A source is accepted when its extension's verifier accepts it; for `ads`, when the outcome is
|
||
**accept** (live-ads §9.4), a **gap** being `source_gap`. A `stitched` source is always
|
||
accepted, since its verification refuses nothing as a whole (stitched-presentation §7.5): a
|
||
record it does not accept, of a signer outside the scope among them, bills nothing, and a claim
|
||
it does not find presented counts nothing. A source that is not such an object,
|
||
whose `kind` is none of these, or whose members are not exactly its kind's, each of its form, is
|
||
refused `malformed`.
|
||
|
||
### 5.2 What a source contributes
|
||
|
||
Each source contributes **records**, each named by a kind and an identity:
|
||
|
||
| record kind | record | identity | contributed by |
|
||
|---|---|---|---|
|
||
| 1 | a room-session receipt, `WMR1` | its digest (live-metering §4.2) | a `room` source: its receipt |
|
||
| 2 | a per-hop record, `WMH1` | its digest (live-metering §3.5) | a `room` source: each record its receipt names; a `delivery` source: each record its statement names in `hops` |
|
||
| 3 | a settlement statement, `WSS1` | its digest (settlement §7.4) | a `delivery` source: its statement |
|
||
| 4 | an acknowledgement or declaration, `WDA1` | its digest (settlement §3.5) | a `delivery` source: each record its statement names in `records` |
|
||
| 5 | a package receipt | its `receipt_hash` (packaging §9) | a `package` source |
|
||
| 6 | a `wai.video.ssai` receipt of the break form | its identity (live-ads §8.4) | an `ads` source: each receipt |
|
||
| 7 | a presentation claim, `WPC1` | its digest (ad-presentation §5.3) | an `ads` source: each claim that reads |
|
||
| 8 | a pod-input record, `WSI1` | its digest (stitched-presentation §4.4) | a `stitched` source: each record it accepts |
|
||
| 9 | a stitched claim, `WSC1` | its digest (stitched-presentation §5.3) | a `stitched` source: each claim that reads |
|
||
|
||
A source's **head** is its receipt, statement or package receipt; an `ads` source's heads are its
|
||
receipts, and a `stitched` source's its accepted records. A head of one source that another source of the invoice also contributes is refused
|
||
`record_duplicate`; a per-hop record, which a room receipt and a statement can both name, is
|
||
contributed once.
|
||
|
||
### 5.3 Placement in time
|
||
|
||
A source is **in** a period when:
|
||
|
||
- `room`: the receipt's session, placed by its room's `t0`, lies within it: `t0` + `start` ≥ the
|
||
period's `start` and `t0` + `end` ≤ its `end`;
|
||
- `delivery`: the statement's period lies within it;
|
||
- `ads`: the `t` of the break each receipt names, placed by the room's `t0`, is within it;
|
||
- `package` and `stitched`: always: a package receipt and a pod-input record state no time, and
|
||
the chain (§6.9) bills each once.
|
||
|
||
Two room receipts of one room whose sessions overlap, two statements of one party whose periods
|
||
overlap, and two receipts of the invoice's ads sources naming one break and one session, bill one
|
||
thing twice: the later is refused `record_overlap`.
|
||
|
||
## 6. The invoice
|
||
|
||
### 6.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WLI1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `seller`, `buyer` | 32 each | the tariff's |
|
||
| `tariff` | 32 | the tariff's digest |
|
||
| `prev` | 32 | the digest of the invoice before it in its chain (§6.9); zero for the first |
|
||
| `start`, `end` | 8 each | u64: the period it bills |
|
||
| `currency` | 3 | the tariff's |
|
||
| `records` | 4 + 33 × *r* | a u32 count, then each record (§5.2): `kind` (u8) and `id` (32), in strictly ascending order of (`kind`, `id`) |
|
||
| `lines` | 2 + 94 × *l* | a u16 count, then each line (§6.3), in strictly ascending order of (`source_kind`, `source`, `unit`) |
|
||
| `energy` | 1 + 18 × *e* | a u8 count, then each row (§6.6): `scope` (u8), `tag` (u8), `figures` (u32), `joules` (u64, µJ), `max_ppm` (u32), in strictly ascending order of (`scope`, `tag`) |
|
||
| `unmetered` | 16 | four u32: the unmetered figures of scopes 1 to 4 (§6.6) |
|
||
| `amount` | 8 | u64: the sum of the lines' amounts |
|
||
| `sig` | 64 | §6.7 |
|
||
|
||
### 6.2 Records
|
||
|
||
`records` lists every record the sources contribute (§5.2), each once. It is what the invoice was
|
||
computed from, and what the next invoice of the chain may not bill again.
|
||
|
||
### 6.3 Lines
|
||
|
||
A line is 94 bytes:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `source_kind` | 1 | 1 room, 2 delivery, 3 package, 4 ads |
|
||
| `source` | 32 | the room id, the delivering party, the packager or the ad decider |
|
||
| `unit` | 1 | the unit (below) |
|
||
| `quantity` | 8 | u64: the quantity of the unit the records state |
|
||
| `price` | 8 | u64: the tariff's price for the unit |
|
||
| `amount` | 8 | u64: §6.4 |
|
||
| `cited` | 4 | u32: the records the line cites (§6.5) |
|
||
| `cites` | 32 | their **citation digest** (§6.5) |
|
||
|
||
The units, the quantity each sums over the sources of its `source`, and the price and divisor of
|
||
each:
|
||
|
||
| `source_kind` | `unit` | name | quantity | price | divisor |
|
||
|---|---|---|---|---|---|
|
||
| 1 | 1 | `publisher_us` | the receipts' `publisher_us` | `room` 0 | 10^6 |
|
||
| 1 | 2–10 | `subscriber_us_0`–`subscriber_us_8` | their `subscriber_us` of layer class `unit` − 2 | `room` 1–9 | 10^6 |
|
||
| 1 | 11 | `composition_us` | their `composition_us` | `room` 10 | 10^6 |
|
||
| 1 | 12 | `recorded_us` | their `recorded_us` | `room` 11 | 10^6 |
|
||
| 1 | 13 | `events_sent` | their `events_sent` | `room` 12 | 1 |
|
||
| 2 | 1–4 | `live`, `fetch`, `fill`, `ancillary` | the bytes of the statement's delivery lines of that class that are not free (settlement §7.2) | `delivery` `unit` − 1 | 10^9 |
|
||
| 3 | 1 | `packages` | the number of package receipts | `package` 0 | 1 |
|
||
| 3 | 2 | `entries` | their `total_entries` | `package` 1 | 1 |
|
||
| 3 | 3 | `stored_bytes` | their `stored_bytes` | `package` 2 | 10^9 |
|
||
| 4 | 1 | `presented_impressions` | under a policy that requires claims, the impressions that count (ad-presentation §9.2); of a stitched source, the presentations of its accepted records' ads (stitched-presentation §7.4), one per session | `ads` 0 | 1 |
|
||
| 4 | 2 | `billable_impressions` | under a policy that does not, the billable impressions (quartile at least 1) | `ads` 1 | 1 |
|
||
|
||
There is one line for each source and unit whose quantity is above 0, a price of 0 included, and
|
||
none for a quantity of 0. A delivery line's bytes are those its party's downstream parties signed
|
||
for (settlement §3.7): a declared byte and a free retrieval are never billed, and the delivery
|
||
report states both (§8.2). An ad line counts impressions under one rule or the other, never both:
|
||
a policy that requires claims bills only what the players' own claims show presented, and one that
|
||
does not bills on the decider's beacons, under a unit of its own.
|
||
|
||
### 6.4 Amounts
|
||
|
||
A line's **amount** is ⌊`quantity` × `price` / divisor⌋, computed in integers wide enough not to
|
||
overflow, and the invoice's `amount` is the sum of its lines' amounts. A line amount or a sum past
|
||
what a u64 holds cannot be stated, and a ledger refuses to compute such an invoice.
|
||
|
||
### 6.5 Citations
|
||
|
||
Each line cites the records it came from:
|
||
|
||
- a room line: the receipts of its room whose figure of the unit is above 0;
|
||
- a delivery line: the statements of its party with a delivery line of the class that is not free,
|
||
and each acknowledgement (mode 1) they name with an entry of the class that is not free (a
|
||
retrieval class with a zero `grant` is free);
|
||
- a package line: the package receipts of its packager whose figure of the unit is above 0;
|
||
- an ad line: the receipts of its decider with at least one impression counted under the unit, and,
|
||
for `presented_impressions`, each counted impression's own claim; of a stitched source, its
|
||
stitcher's accepted records, and each presented own claim of their ads.
|
||
|
||
`cited` is their number, and `cites` is SHA-256 over the 17 bytes `wai:ledger-cites` (ASCII) and
|
||
0x01, followed by each cited record's `kind` and identity, in ascending order of (`kind`,
|
||
identity). Every cited record is in `records`. A buyer recomputes a line from the records it
|
||
cites alone, and shows one line to an auditor with its records and nothing else.
|
||
|
||
### 6.6 Energy
|
||
|
||
Each record that states an energy figure states it once, in one **scope**:
|
||
|
||
| scope | figures |
|
||
|---|---|
|
||
| 1 `room_hops` | the per-hop records named by the room receipts (each restated by its receipt, live-metering §4.5) |
|
||
| 2 `delivery_hops` | the per-hop records named by the statements and by no room receipt of the invoice |
|
||
| 3 `packaging` | the package receipts' `joules_micro` and class |
|
||
| 4 `ad_decisions` | the ad receipts' `joules_micro` and class; a pod-input record states no figure |
|
||
|
||
For each scope, `unmetered` counts its figures that are the unmetered marker, which are in no row
|
||
and **MUST NOT** be presented as zero joules. For each scope and each tag among its metered
|
||
figures there is one **row**: `figures`, their number; `joules`, their sum; and `max_ppm`, the
|
||
largest `relative_ppm` among them for tags 4 and 5, which carry a declared uncertainty, and 0 for
|
||
tags 2 and 3, which do not. When each figure's error is within its declared relative uncertainty,
|
||
the sum's is within `max_ppm` of the sum. A row never sums figures of two classes, and no row sums
|
||
two scopes: a relay's counter reading, a packager's model and a decider's estimate are three rows,
|
||
since a sum of a model and a counter reading is neither. A row whose `tag` is 0 is refused
|
||
`classes_summed`, and so is a row whose `joules` is the sum of the figures of its class and of at
|
||
least one other class of its scope, while not the sum of its own (live-metering §4.5's rule). A
|
||
scope's energy is **whole** when its `unmetered` is 0 and it has a row, **partial** when it has an
|
||
unmetered figure, and **none** when it has no figure; a partial total **MUST NOT** be presented as
|
||
the scope's energy.
|
||
|
||
### 6.7 Digest and signature
|
||
|
||
The **digest** of an invoice is SHA-256 over its bytes before `sig`. Its signature is Ed25519 by
|
||
`seller` over the 19 bytes `wai:ledger-invoice` (ASCII) and 0x01, followed by the bytes the digest
|
||
covers, verified strictly (SPEC §7 *Signatures*).
|
||
|
||
### 6.8 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given as one invoice, as §4.3 reads a tariff with `WLI1`
|
||
for `WLT1` (`truncated`, `malformed`, `unknown_version`), and refuses as `classes_summed` an
|
||
invoice with a row of tag 0. It refuses as `invoice_invalid` one that breaks any of these:
|
||
|
||
- `start` is below `end`; `currency` is three capitals `A`–`Z`;
|
||
- each list is in the strictly ascending order §6.1 gives it; each record's `kind` is 1 to 9;
|
||
- each line's `source_kind` is 1 to 4, its `unit` a unit of its kind (§6.3), its `quantity` and
|
||
`cited` above 0;
|
||
- each row's `scope` is 1 to 4, its `tag` 2 to 5 and its `figures` above 0.
|
||
|
||
Then the signature (§6.7): `bad_key`, `bad_signature`.
|
||
|
||
### 6.9 Chains
|
||
|
||
The invoices of one tariff form a **chain**: each names in `prev` the invoice before it, the first
|
||
names zero, and their periods do not overlap. A ledger keeps one chain per tariff. No record is in
|
||
the `records` of two invoices of a chain: a record billed once is billed never again, whatever
|
||
period a later invoice states, and a package receipt, which states no time, is billed in the
|
||
invoice that first holds it.
|
||
|
||
## 7. Verification
|
||
|
||
### 7.1 Inputs
|
||
|
||
A verifier is given the **tariff**, the **invoice**, the invoice its `prev` names (the
|
||
**previous**, or none), and the **sources**, a list. The ledger's verification document holds them
|
||
as one JSON object: `tariff` and `invoice` (hexadecimal), `previous` (hexadecimal or `null`) and
|
||
`sources` (§5.1).
|
||
|
||
### 7.2 Steps
|
||
|
||
It runs these steps and stops at the first refusal, which it reports with the reason and where:
|
||
|
||
1. **The tariff** (§4.3), at `tariff`, or `tariff` and `seller` or `buyer` for a signature:
|
||
`truncated`, `malformed`, `unknown_version`, `tariff_invalid`, `bad_key`, `bad_signature`.
|
||
2. **The invoice** (§6.8), at `invoice`: `truncated`, `malformed`, `unknown_version`,
|
||
`classes_summed`, `invoice_invalid`, `bad_key`, `bad_signature`.
|
||
3. **The binding**, at `invoice`: its `tariff` is the tariff's digest, and its `seller`, `buyer`
|
||
and `currency` the tariff's: otherwise `tariff_mismatch`; its period lies within the tariff's:
|
||
otherwise `outside_tariff`.
|
||
4. **The previous**, at `previous`, when `prev` is not zero: an invoice whose digest is `prev` is
|
||
given (`record_missing`); it reads and its signature verifies (§6.8, with those reasons); its
|
||
`tariff`, `seller` and `buyer` are the invoice's (`tariff_mismatch`); and its `end` is at most
|
||
the invoice's `start` (`invoice_overlap`).
|
||
5. **Each source**, in order, at `source` and its index: it is verified as §5.1 says, a refusal
|
||
being that extension's reason with that extension's place after the index (an `ads` gap is
|
||
`source_gap`, and a source that does not read `malformed`); then it is in the scope (§4.4, `outside_scope`); a room receipt or a statement
|
||
that states a tariff states this one (§3, `tariff_conflict`); and it is in the invoice's period
|
||
(§5.3, `outside_period`).
|
||
6. **Across sources**, at `source` and the index of the later: a head contributed twice
|
||
(`record_duplicate`); a receipt, statement or ad receipt overlapping an earlier one (§5.3,
|
||
`record_overlap`); and a record that is in the previous invoice's `records`
|
||
(`record_rebilled`).
|
||
7. **The figures**, at `invoice`, recomputed from the sources as §6.2 to §6.6 compute them and
|
||
compared in this order:
|
||
1. a line whose source and unit no source gives, or that states a larger quantity than the
|
||
sources give: `quantity_not_recorded`, a quantity no signed record covers;
|
||
2. a row that §6.6 calls summed across classes: `classes_summed`;
|
||
3. a line whose `cited` or `cites` is not the records it came from: `citation_mismatch`;
|
||
4. any other difference (the records, a line missing, a smaller quantity, a price, an amount, a
|
||
row, `unmetered`, the `amount`): `invoice_mismatch`.
|
||
|
||
A figure that cannot be held in its field is a difference.
|
||
|
||
### 7.3 Outcome
|
||
|
||
- **refuse**: the reason and where.
|
||
- **accept**: the invoice's digest; the tariff's digest; the currency and the period; the number of
|
||
records; each line with its source kind, source, unit name, quantity, price, amount and its cited
|
||
records; each energy row by scope and class label with its figures, joules and `max_ppm`; each
|
||
scope's unmetered figures and coverage; the amount; and the reports of §8.
|
||
|
||
## 8. Reports
|
||
|
||
The reports are computed from the verified sources, as the lines are, and are stated with every
|
||
accepted invoice. They are not signed: anyone holding the sources computes them, and a verifier
|
||
compares them.
|
||
|
||
### 8.1 Ad decisions against presentation claims
|
||
|
||
For each `ads` source, in order: its room, whether its policy requires claims, and each receipt in
|
||
its list's order, with its identity, decider, `cue` and `session`, and each impression's `slot`,
|
||
`ad_id`, `price_micros` and `quartiles_reached`, whether it is **billable** (the decider's beacons:
|
||
a quartile of at least 1), whether it **counts**, and its **verdict**: under a policy that requires
|
||
claims, `presented` or the reason its own claim did not count, `claim_absent` among them
|
||
(ad-presentation §9.2); under one that does not, `beacons`. Then the receipt's billable impressions
|
||
and their spend, `budget_spent_micros`, the seller's figure; and its counted impressions and their
|
||
spend, the presented spend (ad-presentation §9.3), the buyer's. Then, over the source: the
|
||
billable and the counted impressions, the billable and the counted spend, and, for each verdict
|
||
other than `presented`, the billable impressions that did not count with it.
|
||
|
||
So a seller's receipts and a buyer's claims are reconciled from the same signed bytes: what the
|
||
seller billed, what the players presented, and why each impression that was billed and did not
|
||
count did not.
|
||
|
||
For each `stitched` source, in order, the report is the statement of stitched-presentation §7.5 as
|
||
that extension's verifier gives it, unchanged: each record's verdict, each ad's presentations,
|
||
quartiles and presented spend, and, for a checked form-1 response, the receipt's own quartile and
|
||
spend beside them; each claim's verdict; and the spend by currency, never summed across currencies.
|
||
That statement is the seller-against-buyer reconciliation of a stitched stream, and the ledger
|
||
computes no second one.
|
||
|
||
### 8.2 Delivery by party
|
||
|
||
For the `delivery` sources together, each party in ascending order of key: the digests of its
|
||
statements; their `currency` (the stated one, or `null`); the acknowledged objects and bytes of
|
||
each class; the free retrieval bytes; the declared declarations, objects and bytes (settlement §4),
|
||
never billed; the sum of its statements' own `amount`s; the sum of the invoice's lines of the party;
|
||
and its energy rows and unmetered figures over its per-hop records.
|
||
|
||
Then the **overlaps**: for each pair of acknowledgements (mode 1) of two different parties, signed
|
||
by one downstream key, whose windows overlap, and each content hash that an item of each names with
|
||
byte ranges that share bytes, one overlap. It states the content hash, the downstream key, the bytes
|
||
shared, summed over every such pair of items, and the two parties with their acknowledgements'
|
||
digests, the lower party first. The overlaps are in ascending order of (content hash, downstream,
|
||
first party, first record, second party, second record). An overlap is billed by both parties, since each was acknowledged; it is what a buyer
|
||
using several delivering parties disputes with them, and the report names the signatures it rests
|
||
on.
|
||
|
||
## 9. The reference ledger service
|
||
|
||
`wai/wai-ledger` runs the ledger as an HTTP service over a directory it owns:
|
||
|
||
| request | does |
|
||
|---|---|
|
||
| `GET /v1` | the service, its seller key, its limits |
|
||
| `GET /v1/health` | `ok` |
|
||
| `POST /v1/tariffs` | `{"tariff": hex}`: a tariff whose seller is the service's key, both signatures verified (§4), kept by its digest |
|
||
| `POST /v1/sources` | `{"tariff": digest, "source": {...}}`: the source verified (§7.2 steps 5 and 6 against what the tariff's chain holds), kept by its heads |
|
||
| `POST /v1/invoices` | `{"tariff": digest, "start", "end"}`: the invoice of the period over the sources held and not yet billed, chained, signed and kept, with its outcome (§7.3) |
|
||
| `GET /v1/invoices/{digest}` | the invoice, its outcome and its verification document (§7.1) |
|
||
| `POST /v1/verify` | a verification document: its outcome |
|
||
|
||
Each refusal names its reason and place. The service writes every file whole (to a temporary name,
|
||
synced, then renamed), holds a lock on its directory so that one process writes it, and recovers
|
||
each tariff's chain and held sources from the directory when it starts. A buyer fetches an
|
||
invoice's verification document and checks it with any verifier of this extension, the service's
|
||
own not among them.
|
||
|
||
## 10. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `tariff_invalid` | §4.3, §7.2 step 1 |
|
||
| `invoice_invalid` | §6.8, §7.2 step 2 |
|
||
| `tariff_mismatch` | §7.2 steps 3 and 4 |
|
||
| `outside_tariff` | §7.2 step 3 |
|
||
| `invoice_overlap` | §6.9, §7.2 step 4 |
|
||
| `source_gap` | §5.1, §7.2 step 5 |
|
||
| `outside_scope` | §4.4, §7.2 step 5 |
|
||
| `tariff_conflict` | §3, §7.2 step 5 |
|
||
| `outside_period` | §5.3, §7.2 step 5 |
|
||
| `record_duplicate` | §5.2, §7.2 step 6 |
|
||
| `record_overlap` | §5.3, §7.2 step 6 |
|
||
| `record_rebilled` | §6.9, §7.2 step 6 |
|
||
| `quantity_not_recorded` | §7.2 step 7 |
|
||
| `citation_mismatch` | §6.5, §7.2 step 7 |
|
||
| `invoice_mismatch` | §7.2 step 7 |
|
||
|
||
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version` for a
|
||
tariff or an invoice that does not read, and `malformed` for a source that does not read (§5.1); `bad_key` and `bad_signature` for their signatures;
|
||
`record_missing` (live-metering §9) for a previous invoice not given; `classes_summed`
|
||
(live-metering §9) for a row over classes; and, at a source, every code its extension's verifier
|
||
raises: live-metering §9 and the codes it lists, settlement §11 and the codes it lists, packaging
|
||
§12, and live-ads §10 with ad-presentation §11. A `stitched` source raises none: the codes of
|
||
stitched-presentation §9 are the verdicts of its records and claims, stated in its report (§8.1).
|
||
|
||
## 11. Conformance
|
||
|
||
[`live-conformance/ledger/`](../live-conformance/ledger/) holds the corpus, in one file,
|
||
`vectors.json`, written by `wai_receipt_ledger_vectors gen` from fixed seeds and from sources taken
|
||
from the corpora of the extensions it reads (`live-conformance/metering/`,
|
||
`live-conformance/settlement/`, `live-conformance/ads/`, `live-conformance/presentation/`,
|
||
`live-conformance/stitched/` and `pack-conformance/packs/`), with delivery statements of two
|
||
parties signed for it. The sources are
|
||
held once, by name, and each vector is a verification document (§7.1) naming its sources, with the
|
||
outcome §7.3 gives it:
|
||
|
||
- **Accepted**: an invoice of one day billing a lesson room, two delivering parties and a viewer
|
||
that forwarded sealed objects under no tariff of its own, a package, ads under a policy that
|
||
requires claims and under one that does not, with both reports, one viewer acknowledging one
|
||
object from both parties among the overlaps; the next invoice of its chain, billing a relay's
|
||
session whose energy is unmetered, and a second package; and an invoice whose room hops are of
|
||
three classes, one row each; and an invoice of streams stitched by others, one with a form-1
|
||
receipt whose own figures the report states beside the presented ones.
|
||
- **Refused**: each reason of §10 at least once, each vector a control of one defect; a source of
|
||
each kind outside the scope, a room receipt and a statement under other prices, and a source of
|
||
each timed kind outside the period; and a refusal of each source kind by its own extension.
|
||
|
||
`wai_receipt_ledger_vectors verify` and the test `tests/receipt_ledger.rs` check that the file
|
||
regenerates byte for byte, that `wai-rs` gives every vector its outcome, that every reason of §10
|
||
refuses a vector, and that §10 lists exactly the reference's codes.
|
||
`live-conformance/receipt_ledger_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`), verifies each source with that corpus's own second
|
||
implementation (`live_metering_verify.py`, `settlement_verify.py`, `live_ads_verify.py` with
|
||
`ad_presentation_verify.py`, `stitched_presentation_verify.py`, and
|
||
`pack-conformance/pack_verify.py`), recomputes every line,
|
||
citation, row and report itself, gives every vector the same outcome, and requires every mutated
|
||
copy of the corpus to fail.
|
||
|
||
## 12. Security and privacy considerations
|
||
|
||
- **Who signs what.** The buyer signs the prices and the scope; each record's own party signs its
|
||
figures; the seller signs the invoice. A seller cannot bill a unit at a price the buyer did not
|
||
sign, a record of a party the buyer did not name, or a quantity no record states: the verifier
|
||
recomputes every line.
|
||
- **Double billing.** A head is billed once in an invoice (`record_duplicate`), one session, one
|
||
statement period and one viewer's break once (`record_overlap`), and a record once in a chain
|
||
(`record_rebilled`). A verifier given one invoice checks it against the one before it; a buyer
|
||
holding the chain checks every link.
|
||
- **What the figures rest on.** An invoice is as true as its records: a delivery line rests on
|
||
acknowledgements, so a party and a downstream key that collude can bill bytes that never moved,
|
||
as settlement §13 says; a room line rests on per-hop records and the room's events; an ad line
|
||
under claims rests on players' session keys. The reports name the signatures each figure rests
|
||
on, so a dispute is about records, not about logs.
|
||
- **Energy.** A row states figures as their parties signed them, with their class: it is
|
||
attributable and its sum recomputable, not true (energy-measurement §5). `max_ppm` bounds the
|
||
declared part of a sum's uncertainty and says nothing of a class's undeclared systematic error.
|
||
- **What an invoice reveals.** Its records list names every record by identity, and its lines name
|
||
rooms, parties, packagers and deciders. A verification document holds the records themselves:
|
||
acknowledgements name downstream keys, and ad receipts name sessions. They travel between the
|
||
seller, the buyer and an auditor they choose; the service serves an invoice's document by its
|
||
digest and lists nothing.
|