receipt-ledger: tariff version 6 prices an endpoint and a third party's usage, and bills an upscale and a tone mapping by what re-derives them #579

Closed
dcharlot wants to merge 1 commit from wai/tariff-v6 into main
12 changed files with 5707 additions and 1835 deletions

View file

@ -6,6 +6,32 @@ wire format may change and such changes are recorded here.
## Unreleased
### Tariff version 6: a job priced per endpoint, a third party's usage billed, and an upscale and a tone mapping billed by what re-derives them
- **Tariff and invoice version 6** ([extensions/receipt-ledger.md](extensions/receipt-ledger.md) §4.9). A job price names
an `endpoint`, or none for every endpoint, since two providers of one capability have different list prices; a price
of any endpoint beside a price of one is refused, since it would bill a job at both.
- **A third party's usage.** A new job unit, `usage`, in a unit the price names. A job source may give `third_party`,
the hop records of [route-record](extensions/route-record.md) §6.2, and a job is billed what its billed hop's record
states the provider bills. A failed hop's billed usage and a record's `charge` reach no line; the line cites the
record, which states both. Records are kind 17.
- **An upscale and a tone mapping** are jobs of their own forms, `media.upscale` and `media.tonemap`, billed in `subpixels`.
An upscale's record is proved by exact-upscale §7 from its input and its prior, and a tone mapping's by exact-tonemap
§8 from its envelope, and what the hop delivered is held to what they prove. Their records are kinds 18 and 19, and a
later invoice may cite them again, as an output receipt is.
- A tariff of versions 1 to 5, its invoices and its sources read, verify and bill as before. The corpus grows from 120
to 169 vectors.
- **Breaking:** `api:rust:wai::receipt_ledger::JobPrice::endpoint` and `api:rust:wai::receipt_ledger::JobPrice::usage`: new
members of a public struct.
- **Breaking:** `api:rust:wai::receipt_ledger::JobFacts::endpoint`, `api:rust:wai::receipt_ledger::JobFacts::usage`,
`api:rust:wai::receipt_ledger::JobFacts::third_party`, `api:rust:wai::receipt_ledger::JobFacts::upscales` and
`api:rust:wai::receipt_ledger::JobFacts::renditions`: new members of a public struct.
- **Breaking:** `api:rust:wai::receipt_ledger::JOB_UNITS`: holds a fourth name, an array of 4.
- **Breaking:** `record:corpus:wai/live-conformance/ledger/vectors.json`: the control `invoice_version` gives an
invoice of version 7, since version 6 is now defined, and is refused `unknown_version` as before; every other vector
keeps its outcome, and the new vectors are added.
### A third party's hop has a signed record: what was sent, what came back, and what the provider bills
- **The third-party hop record** ([extensions/route-record.md](extensions/route-record.md) §6.2, §7.2 step 9),

View file

@ -31,7 +31,10 @@
> receipt of version 2 states (live-metering §4.4), at a price per 10^9 bytes; a package's work
> once per run that made it, by the work units it states, its receipt the product each run
> cites; a package's storage by byte-days for each period it is held; and its invoices state
> every hop's energy, the hops not billed in a scope of their own (§6.6). Whatever the version,
> every hop's energy, the hops not billed in a scope of their own (§6.6). A tariff of version 6
> also prices a job **per endpoint**, and by the **usage** a third party's hop record states
> ([route-record](route-record.md) §6.2), and bills an upscale and a tone mapping by the subpixels
> their records state, each re-derived from what it read (§4.9). Whatever the version,
> a job's output receipt is of an object its served hop states it delivered, where its route
> record states it (§5.1), and a route record's routing figures are never charges. No new transport, no payment
> and no tax: an invoice is the recomputable figures a payment rests on. The reference implementation is the
@ -124,7 +127,7 @@ records recomputes all three.
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WLT1` (ASCII) |
| `version` | 1 | 1; 2 for a tariff that prices jobs (§4.5); 3 for one that prices them per operation and bills their work (§4.6, §6.9); 4 for one that also prices bytes out, a package's work and its storage by byte-time (§4.7) |
| `version` | 1 | 1; 2 for a tariff that prices jobs (§4.5); 3 for one that prices them per operation and bills their work (§4.6, §6.9); 4 for one that also prices bytes out, a package's work and its storage by byte-time (§4.7); 5 for one that holds a decode to its frames (§4.8); 6 for one that also prices a job per endpoint and by a third party's usage, and bills an upscale and a tone mapping (§4.9) |
| `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 |
@ -133,7 +136,7 @@ records recomputes all three.
| `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, in version 4 8 | three u64: millionths of a minor unit per package receipt, per entry, and per 10^9 stored bytes; in version 4 one, per 10^9 byte-days held (§4.7) |
| `ads` | 16 | two u64: millionths of a minor unit per presented impression, and per billable impression |
| `job` | 2 + Σ (18 + *c*), in versions 3 and 4 2 + Σ (19 + *c* + *o*) | versions 2 to 4, absent in version 1: a u16 count *j*, then each job price (§4.5, §4.6): `cap_len` (u8), `capability` (`cap_len` bytes), in versions 3 and 4 `op_len` (u8) and `operation` (`op_len` bytes), then `unit` (u8), `divisor` (u64) and `price` (u64) |
| `job` | 2 + Σ (18 + *c*), in versions 3 and 4 2 + Σ (19 + *c* + *o*), in version 6 2 + Σ (21 + *c* + *o* + *e* + *u*) | versions 2 to 4, absent in version 1: a u16 count *j*, then each job price (§4.5, §4.6, §4.9): `cap_len` (u8), `capability` (`cap_len` bytes), in versions 3 and 4 `op_len` (u8) and `operation` (`op_len` bytes), in version 6 `end_len` (u8) and `endpoint` (`end_len` bytes), then `unit` (u8), in version 6 `use_len` (u8) and `usage` (`use_len` bytes), then `divisor` (u64) and `price` (u64) |
| `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 |
@ -152,7 +155,7 @@ tariff the buyer did not sign bills nobody.
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 to 5, `unknown_version`; the
`malformed`; no fifth byte, `truncated`; a version other than 1 to 6, `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:
@ -163,7 +166,11 @@ It then refuses as `tariff_invalid` a tariff that breaks any of these:
- in version 2, `job` has at most 255 prices, each as §4.5 requires, in strictly ascending
order of (`capability`'s bytes, `unit`); in versions 3 and 4, at most 255, each as §4.6
requires, in strictly ascending order of (`capability`'s bytes, `operation`'s bytes, `unit`);
each `unit` 1 or 2, and in version 4 1 to 3.
each `unit` 1 or 2, and in version 4 1 to 3;
- in version 6, at most 255, each as §4.9 requires, in strictly ascending order of
(`capability`'s bytes, `operation`'s bytes, `endpoint`'s bytes, `unit`, `usage`'s bytes), each
`unit` 1 to 4; and no price of any endpoint beside a price of one for the same `capability`,
`operation`, `unit` and `usage` (§4.9).
Then the seller's signature, then the buyer's: `bad_key`, `bad_signature`.
@ -195,6 +202,7 @@ The units are the records' own figures, never converted:
| 1 | `jobs` | 1 |
| 2 | `subpixels` | the sum of its output receipts' `work_pixel_ops` (integer-payloads §15.1): the subpixels its output decodes to |
| 3 | `work_units` (version 4) | the sum of its output packages' `work_units` (packaging §9.1, §4.7) |
| 4 | `usage` (version 6) | what its billed hop's third-party record states the provider bills, in the price's own unit (§4.9) |
A job the tariff has no price for, by its serving capability, is billed nothing, and a buyer
pays for a capability only at a price it signed. A package job stays a package source (§5.1),
@ -335,6 +343,83 @@ a version-4 verdict on a decode must not change under it, so version 5 is a vers
A tariff of versions 1 to 4, its invoices and its sources read, verify and bill exactly as
before.
### 4.9 An endpoint, a third party's usage, and what re-derives an output (version 6)
A tariff of version 6 is version 5's in its layout but for its job prices (§4.1), in its rules
but for theirs (§4.3), and in its scope and prices (§4.4 to §4.8), and an invoice of version 6 is
version 5's (§6.1) but for the records it may list; each bills what version 5 bills. Where this
text says version 4 or 5, or a range of versions ending at either, version 6 is included, except
as this section says.
**A job price names an endpoint.** Its `endpoint` is the endpoint of the attempt it prices, a
token of route-record §2's form, or empty, which prices every endpoint. Two providers of one
capability have different list prices, so one price per capability overprices one or underprices
the other, and a buyer pays each at the price it signed for it. A job is priced as §4.6 prices it
and, where a price names an endpoint, by its billed hop's. A price of any endpoint beside a price
of one, for the same capability, operation, unit and usage, would bill one job at both, so a
tariff that has both is `tariff_invalid`: its seller states one price for every endpoint or a
price for each.
**A third party's usage.** A job price of unit 4 names `usage`, a unit of route-record §2's form,
and of any other unit none. A job source may give `third_party`, a list of third-party hop
records' texts, which route-record §7.2 step 9 verifies with the rest of its document, a refusal
there being the source's. Each record contributes itself (§5.2, kind 17). A job's quantity at a
price of usage is the `usage.quantity` of its billed hop's record, when that record's `usage.unit`
is the price's `usage`, and otherwise none:
- a job given no record bills nothing at a price of usage, and bills by its other units;
- a failed hop's record, and a late hop's, state what the provider billed for it, which is what
the router's operator owes the provider, and no line reads it: only the billed hop's usage
reaches a line, as §5.1 holds a failed hop's figures from a line;
- a record's `charge` reaches no line. It is the provider's own figure in its own terms, and a
line adds jobs, so it cannot be summed. The line cites the record, which states it signed, so a
buyer reads the two side by side.
A third party's output has no WAI receipt, and a job it served gives no output receipt for it: its
record names the objects, and the route record holds them to the hop (route-record §6.2).
**An upscale.** A job whose route record's `operation` is `media.upscale` gives each output as an
object of exactly `upscale` (the text of an upscale record, exact-upscale §4.2), `object` (the
input: a `WAI1` of capability `wai.image.png` holding a PNG of 8-bit RGB that packaging §5's reader
reads, hex), `prior` (the `WSR1` it names, hex) and `picture` (the object the hop delivered: a PNG
that reader reads, hex). The record is verified by exact-upscale §7 under `require-proof`, holding
the input, its samples and the prior, and the picture is of the size the record's output is, with
the samples whose digest is its `output`. So what is billed is what the derivation gives: the
record's subpixels are the output's width × height × 3, the record contributes itself by its link
hash (§5.2, kind 18), and the picture is its object (§5.1). No residual is given, so a record that
names one is not proved here. Otherwise the source is refused `output_invalid`: at `outputs`, the
output's index and `object` where the input does not read so; at `outputs`, the output's index and
`picture` where the picture does not read or is not the output; and at `outputs` and the output's
index where the record does not read or is not proved.
**A tone mapping.** A job whose route record's `operation` is `media.tonemap` gives each output as
an object of exactly `tonemap` (the text of a rendition record, exact-tonemap §7.1), `envelope`
(the `WAI2` it read, hex) and `renditions` (each picture's rendition, its `RGB16` bytes, exact-tonemap
§2, in the record's order, hex), and under a route record of version 3 `proofs`, in place of
`proof`: one inclusion proof for each rendition (route-record §6.1). The record is verified by
exact-tonemap §8 under `require-proof`, holding the envelope and no decoded frame, so what is proved
is a still's pictures, and a record of a video's frames is refused. There is one rendition for each
picture, whose digest is the picture's `output`. So the record's subpixels are its pictures × width
× height × 3, and it contributes itself by its link hash (§5.2, kind 19). Otherwise the source is
refused `output_invalid`: at `outputs`, the output's index, `renditions` and the rendition's index
where a rendition is not its picture's; at `outputs`, the output's index and `renditions` where
their number is not the record's; and at `outputs` and the output's index where the record does not
read or is not proved. Each rendition is an object the hop delivered, and none is another output's
(§5.1): otherwise `output_not_delivered`, at `outputs`, the output's index, `renditions` and the
rendition's index. Two pictures of one record may render one picture: the hop states that object
once (route-record §5.2), and both are billed, since the record states and proves both.
A job of either operation gives its outputs in that form alone, and any other form is `malformed`.
A job's `subpixels` (§4.5) sum its output receipts' `work_pixel_ops` and these records' subpixels.
These records are the evidence of what a job delivered, as an output receipt is (§6.9): the
derivations are exact, so a run of the same request delivers the same picture, and a later invoice
may cite a record again for a new hop's work. Each is a record of the signer that made it, and what
its figure rests on is the derivation, which any holder of the source recomputes.
A version-5 reader cannot read a job price that names an endpoint, a job source's `third_party`,
these outputs or kinds 17 to 19, so version 6 is a version of its own. A tariff of versions 1 to 5,
its invoices and its sources read, verify and bill exactly as before.
## 5. Sources
### 5.1 Kinds and their documents
@ -349,7 +434,7 @@ extension's verifier is given. Bytes are lowercase hexadecimal strings.
| `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) |
| `job` (versions 2 to 4) | `request`, `pricebook`, `policy`, `record`, `receipts`, `keys` (a route record's verification document, route-record §7.1, its `receipts` and `keys` not `null`) and `outputs`, a list of objects of exactly `receipt` (the text of a `wai.video.int_motion` decode receipt's JSON form, integer-payloads §15.2) and `codestream` (the `WIV1` payload it describes: the object the hop delivered, named by its content hash; in version 5 a decode's output also has `frames`, the animation of its frames it delivered, and its codestream is what it read, §4.8), or, in version 4, of exactly `package` (the text of a package receipt of version 2, `package.receipt.json`: the object the hop delivered) and `indexes` (its index files in its component order); under a route record of version 3, each output also has `proof`, its object's inclusion proof (route-record §6.1) | route-record §7, accepted with its hop receipts checked; then each output receipt read by integer-payloads §15.2 and re-derived from its `codestream` (§15.1 there), each package verified against its indexes by packaging §9, or `output_invalid` at `outputs` and its index; then each output held to what its hop delivered, or `output_not_delivered` there |
| `job` (versions 2 to 4) | `request`, `pricebook`, `policy`, `record`, `receipts`, `keys` (a route record's verification document, route-record §7.1, its `receipts` and `keys` not `null`) and `outputs`, a list of objects of exactly `receipt` (the text of a `wai.video.int_motion` decode receipt's JSON form, integer-payloads §15.2) and `codestream` (the `WIV1` payload it describes: the object the hop delivered, named by its content hash; in version 5 a decode's output also has `frames`, the animation of its frames it delivered, and its codestream is what it read, §4.8), or, in version 4, of exactly `package` (the text of a package receipt of version 2, `package.receipt.json`: the object the hop delivered) and `indexes` (its index files in its component order); under a route record of version 3, each output also has `proof`, its object's inclusion proof (route-record §6.1); in version 6, it may also have `third_party`, and an upscale's and a tone mapping's outputs have their own forms (§4.9) | route-record §7, accepted with its hop receipts checked; then each output receipt read by integer-payloads §15.2 and re-derived from its `codestream` (§15.1 there), each package verified against its indexes by packaging §9, or `output_invalid` at `outputs` and its index, an upscale's and a tone mapping's outputs as §4.9 says; then each output held to what its hop delivered, or `output_not_delivered` there |
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
@ -439,8 +524,11 @@ the others are the ledger's own:
| 14 | an OER/2 receipt | the 32 bytes its `receipt_id` names (oer-receipts §5) | a `job` source (versions 2 to 4): the receipt of each attempt of its record |
| 15 | a `wai.video.int_motion` decode receipt | its receipt hash (integer-payloads §15.1) | a `job` source (versions 2 to 4): each output receipt |
| 16 | a route record | its digest (route-record §5.3) | a `job` source (versions 2 to 4): its record |
| 17 | a third-party hop record | its digest (route-record §6.2) | a `job` source (version 6): each record it gives |
| 18 | an upscale record | its link hash (exact-upscale §2) | a `job` source (version 6): each upscale's record (§4.9) |
| 19 | a rendition record | its link hash (exact-tonemap §2) | a `job` source (version 6): each tone mapping's record (§4.9) |
Kinds 17 to 127 are unassigned. Kinds **128 to 255 are reserved for private and provisional
Kinds 20 to 127 are unassigned. Kinds **128 to 255 are reserved for private and provisional
use**: an implementation's own records, such as a hosted service's provisional records carried
beside WAI's until WAI defines them, may name records by them, and no kind WAI assigns later will
ever be one of them. A WAI record never states one, and a WAI verifier refuses one wherever a WAI
@ -537,7 +625,7 @@ each:
| 3 | 3 | `byte_days` (version 4, its only package unit) | ⌊Σ `stored_bytes` × (`end` − `start`) / 86 400 000 000⌋ over the package receipts the previous invoice lists, which are held (§4.7) | `package` 0 of version 4 | 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 |
| 5 | *k* | the *k*-th job price's unit | over the router's job sources with a billed hop whose serving capability is that price's `capability` (and, in versions 3 and 4, whose operation is its `operation`): one per job (`jobs`), each job's output receipts' `work_pixel_ops` (`subpixels`), or, in version 4, its output packages' `work_units` (`work_units`) | the *k*-th job price's `price` | its `divisor` |
| 5 | *k* | the *k*-th job price's unit | over the router's job sources with a billed hop whose serving capability is that price's `capability` (and, in versions 3 and 4, whose operation is its `operation`, and, in version 6, whose endpoint is its `endpoint`, when it names one): one per job (`jobs`), each job's output receipts' `work_pixel_ops` (`subpixels`), in version 6 with its upscale and rendition records' subpixels, or, in version 4, its output packages' `work_units` (`work_units`), or, in version 6, its billed hop's record's usage in the price's unit (`usage`, §4.9) | the *k*-th job price's `price` | its `divisor` |
In version 4 a package source bills only its `byte_days`, and its work is its run's job line.
There is one line for each source and unit whose
@ -571,7 +659,8 @@ Each line cites the records it came from:
and 4, an output receipt that two jobs of the line delivered is cited once and its
`work_pixel_ops` counted for each (§6.9); in version 4, for `work_units`, each of its output
packages whose `work_units` is above 0, a package that two runs of the line made cited once
and counted for each.
and counted for each; in version 6, for `subpixels`, each of its upscale and rendition records,
counted as an output receipt is, and for `usage`, its billed hop's third-party record.
`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`,
@ -630,7 +719,8 @@ invoice with a row of tag 0. It refuses as `private_kind` an invoice with a reco
- `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 (in
versions 2 to 4, also 14 to 16: kinds 10 to 13 are no ledger source's, §5.2);
versions 2 to 4, also 14 to 16: kinds 10 to 13 are no ledger source's, §5.2; in version 6, also
17 to 19);
- each line's `source_kind` is 1 to 4 (1 to 5 in versions 2 to 4), its `unit` a unit of its kind
in its version (§6.3; for kind 5, at least 1), its `quantity` and `cited` above 0;
- each row's `scope` is 1 to 4 (1 to 5 in versions 2 and 3, 1 to 6 in version 4), its `tag` 2 to 5
@ -643,7 +733,8 @@ Then the signature (§6.7): `bad_key`, `bad_signature`.
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, but, under a tariff of version 3 or 4, an output receipt
(kind 15), and under one of version 4 a package receipt (kind 5), which each run that made it
(kind 15), under one of version 6 an upscale record and a rendition record (kinds 18 and 19, §4.9),
and under one of version 4 a package receipt (kind 5), which each run that made it
cites and each invoice whose period it is held through lists again to bill its storage (§4.7): 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.
@ -654,7 +745,7 @@ run again delivers the same objects, of the same content hashes, under the same
tariff of version 3 or 4, therefore:
- the records a chain bills once for a job are its route record and its hops' OER/2 receipts,
the billed hop's among them;
the billed hop's among them, and in version 6 its hops' third-party records, each of one hop;
- an output receipt is cited, and carried in its lines' citation digests, but is not billed once:
a later invoice may list it again for another job's hop;
- a re-run whose route record and hop receipt are new is billed, in the invoice that holds it;
@ -703,7 +794,8 @@ It runs these steps and stops at the first refusal, which it reports with the re
(`record_duplicate`); a receipt, statement or ad receipt overlapping an earlier one, or, in
version 2, a route record of the router and job an earlier one names (§5.3,
`record_overlap`); and a record that is in the previous invoice's `records`, in versions 3 and
4 an output receipt excepted, and in version 4 a package receipt (§6.9, `record_rebilled`).
4 an output receipt excepted, in version 4 a package receipt, and in version 6 an upscale or
rendition record (§6.9, `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
@ -720,7 +812,8 @@ A figure that cannot be held in its field is a difference.
- **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 (and, for a job line, the job
price's `capability`, and in versions 3 and 4 its `operation`), quantity, price, amount and its
price's `capability`, in versions 3 and 4 its `operation`, and in version 6 its `endpoint` and,
for a usage, its `usage` as `usage_unit`), 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 (scopes 1 to 4, in versions 2 and 3 scope 5, and in
version 4 scopes 5 and 6); the
@ -815,9 +908,9 @@ The codes this extension adds, each with where it is raised:
| `citation_mismatch` | §6.5, §7.2 step 7 |
| `invoice_mismatch` | §7.2 step 7 |
| `private_kind` | §5.2, §6.8, §7.2 step 2 |
| `output_invalid` | §5.1, §7.2 step 5 |
| `output_invalid` | §4.8, §4.9, §5.1, §7.2 step 5 |
| `job_figure_invalid` | §5.1, §7.2 step 5 |
| `output_not_delivered` | §5.1, §7.2 step 5 |
| `output_not_delivered` | §4.8, §4.9, §5.1, §7.2 step 5 |
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;
@ -873,7 +966,15 @@ outcome §7.3 gives it:
held to its codestream; and jobs whose route records are of version 3, each output proved
among what its hop delivered: an encode whose clip is the second of two objects, a decode of
version 5 whose animation is its one object, and a package's run whose receipt is the first
of 1,501 objects.
of 1,501 objects; and, under a tariff of version 6, a third party's jobs billed per job and by
what each record states the provider bills, each endpoint at its own price: a served hop,
another provider's, a failover whose failed hop's billed usage reaches no line, a job with no
record, a job in another unit at that unit's price, and one in a unit no price names; an encode,
a decode held to its frames and a package's run, billed as version 5 bills them; an upscale of
two pictures and two tone mappings, one of whose records states two pictures of one rendering,
each re-derived from what it read; the next invoice of that chain, billing the upscale run again
and citing its records again; and an upscale and a tone mapping whose route records are of
version 2.
- **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; a refusal of each source kind by its own extension; and, for
@ -907,7 +1008,24 @@ outcome §7.3 gives it:
a proof under a record of version 2 and a proof whose index is text (`malformed`), a proof of
another place, one past the count, one a sibling short, an output its hop did not deliver, one
output twice, a decode's animation at another place, a decode with an object past its animation,
and an output of a job no hop served (`output_not_delivered`).
and an output of a job no hop served (`output_not_delivered`); and, for version 6, job prices out
of order by endpoint, a price of any endpoint beside one, a price of usage naming no unit, a
price per job naming one, an endpoint not of its form and a unit past `usage`
(`tariff_invalid`); a price of usage under version 5, and an invoice of version 5 listing a
third-party record (`invoice_invalid`), and an invoice of version 5 under a tariff of version 6;
a job given third-party records, and an upscale and a tone mapping in version 6's form, under
version 5 (`malformed`); another job's record (`third_party_mismatch`); a failed hop's usage added to a
line, a usage line for a job with no record, and subpixels above a record's
(`quantity_not_recorded`); a usage at another endpoint's price and a record left out of the
invoice's (`invoice_mismatch`), and a usage line not citing its records (`citation_mismatch`);
an upscale whose picture is altered, of another shape or not a PNG, under another prior, of
another input, whose input is not an envelope, of another capability or not RGB, whose record
states another output, names a residual or does not verify (`output_invalid`), whose picture its
hop did not deliver or given twice (`output_not_delivered`), in an encode's form (`malformed`);
and a tone mapping with a rendition altered, one missing, renditions out of order, of another
envelope, whose record does not verify (`output_invalid`), with a rendition its hop did not
deliver or given twice (`output_not_delivered`), a proof short, without its proofs, with proofs
under a list, or in an encode's form (`malformed`).
`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
@ -916,7 +1034,8 @@ refuses a vector, and that §10 lists exactly the reference's codes.
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`, `route_record_verify.py`, and
`ad_presentation_verify.py`, `stitched_presentation_verify.py`, `route_record_verify.py`,
`upscale_verify.py`, `tonemap_verify.py` and
`pack-conformance/pack_verify.py`) and each output receipt by `_int_motion_receipt.py`, written
from integer-payloads §15, computes each output's content hash with BLAKE3 written from its
specification (`claim-conformance/_blake3.py`), recomputes every line,
@ -951,6 +1070,17 @@ copy of the corpus to fail.
so it was written by the start of the period billed, and a buyer holding the chain sees each
period billed once. Whether the seller kept the bytes through the period is the seller's
statement, as a delivery is the acknowledging parties' (settlement §13).
- **A third party's usage.** Under a tariff of version 6 a job may be billed by what a third
party's record states the provider bills. That figure is the router's statement: no provider
signs anything, so the record holds what the router read, with the provider's response kept
where it was JSON and every byte named by digest (route-record §6.2), and a router can state a
usage the provider did not bill. A buyer holds the price it signed and the record the router
signed, each line cites that record, and the provider's own `charge` is stated beside it, so a
dispute is about a signed figure; it is not recomputable, as a re-derived subpixel is.
- **What re-derives an output.** Under a tariff of version 6 an upscale's and a tone mapping's
subpixels are recomputed by the verifier from the record, what it read and what the hop
delivered. That binds the picture to its input, and nothing binds the input to what the buyer
asked for: the hop's receipt names its inputs by digest, which a holder of them checks.
- **Every hop's energy.** Under a tariff of version 4 an invoice states the energy every hop of
its jobs spent, the hops the buyer was not billed for in a scope of their own, so a seller
whose router fails often shows it in the energy it states, and is not paid for it.

View file

@ -596,7 +596,8 @@ So the provider's work is tied to the hop that paid for it: the receipt says the
these objects, the record says which calls produced them and what the provider bills, and the
route record names both. The `charge` is the provider's own figure, in its own terms, beside the
usage and never added to it; a reader who compares what the provider says it charged with what
a job was billed reads both from signed documents. The record states no energy figure: the
a job was billed reads both from signed documents. A tariff of version 6 bills a billed hop by the
record's usage (receipt-ledger §4.9). The record states no energy figure: the
hop's receipt states it, or the unmetered marker where the provider gave none
(oer-receipts §3.1).

View file

@ -85,6 +85,21 @@ The accepted vectors:
(`job_v3r_decode`) whose animation is its one object, and a package's run
(`package_run_v3r`) whose receipt is the first of 1,501 objects, the rest standing for the
package's own; billed under the tariff of version 5.
- `version_6_a_third_partys_usage_per_endpoint` (version 6, §4.9): a third party's jobs, each
`third_party` record naming what the provider bills, billed per job and by that usage at its
endpoint's price: gen.alpha (`job_tp_alpha`), gen.beta (`job_tp_beta`), a failover whose failed
hop's billed usage reaches no line (`job_tp_failover`), a job given no record, billed by the job
alone (`job_tp_unrecorded`), one in another unit at that unit's price (`job_tp_images`), and one
in a unit no price names (`job_tp_a_unit_no_price_names`).
- `version_6_holds_what_version_5_holds`: the encode, decode and package run of version 5's route
records of version 3, at prices of any endpoint.
- `version_6_an_upscale_and_a_tone_mapping`: an upscale of two pictures (`job_upscale`) and two tone
mappings, one of whose records states two pictures of one rendering
(`job_tonemap_two_pictures_of_one_rendering`), each re-derived from what it read; and
`version_6_an_upscale_run_again`, the next invoice, billing the upscale's new hop and citing its
records again.
- `version_6_re_derived_jobs_under_a_list`: an upscale and a tone mapping whose route records are of
version 2, so that no output gives a proof.
Every other vector is refused, a control of one defect: each reason of §10, the reused codes, and a
refusal of each source kind by its own extension.
@ -108,7 +123,11 @@ manifests, responses and receipts they name, by their bytes), media jobs from
router under its price book and policy, their hop receipts sealed under its hop key, the replayed
and the claimed one being `served_first_try`'s own; the jobs of version 2 (`job_v2_*`) routed
here by that router under the route corpus's price book of version 2, and the runs of
`package.build` (`package_run*`) under a price book of version 2 of its own; and the
`package.build` (`package_run*`) under a price book of version 2 of its own; the third party's
jobs (`job_tp_*`), the upscales (`job_upscale*`) and the tone mappings (`job_tonemap*`) routed here by
that router, the upscales derived under the prior `generic.wsr` of
[`live-conformance/upscale/priors/`](../upscale/priors/) and the tone mappings' envelopes written
by exact-tonemap's corpus encoder; and the
packages `video-base-01` and `video-staged-01` of
[`pack-conformance/packs/`](../../pack-conformance/packs/). When one of those corpora is
regenerated, so is this one. Nothing here was fetched from outside the repository.
@ -116,8 +135,8 @@ regenerated, so is this one. Nothing here was fetched from outside the repositor
Regenerate and check:
cd wai/wai-rs
cargo run -q --no-default-features --features receipt_ledger --bin wai_receipt_ledger_vectors -- gen ../live-conformance/ledger
cargo run -q --no-default-features --features receipt_ledger --bin wai_receipt_ledger_vectors -- verify ../live-conformance/ledger
cargo run -q --release --no-default-features --features receipt_ledger --bin wai_receipt_ledger_vectors -- gen ../live-conformance/ledger
cargo run -q --release --no-default-features --features receipt_ledger --bin wai_receipt_ledger_vectors -- verify ../live-conformance/ledger
cargo test -q --no-default-features --features receipt_ledger --test receipt_ledger
python3 -I ../live-conformance/receipt_ledger_verify.py ../live-conformance/ledger

File diff suppressed because one or more lines are too long

View file

@ -10,14 +10,16 @@ settlement statement by settlement_verify.py (settlement §8), a package receipt
../pack-conformance/pack_verify.py (packaging §9), an ads room by live_ads_verify.py with
ad_presentation_verify.py (live-ads §9, ad-presentation §9), and a stream stitched by others by
stitched_presentation_verify.py (stitched-presentation §7), whose statement the ledger reports as
it is; and, under a tariff of version 2 or 3, a media job by route_record_verify.py (route-record
§7, its hop receipts by the OER/2 reference verifier) and its output receipts by
_int_motion_receipt.py (integer-payloads §15). Everything else is here:
it is; and, under a tariff of version 2 or later, a media job by route_record_verify.py (route-record
§7, its hop receipts by the OER/2 reference verifier, its third-party records in version 6) and its
output receipts by _int_motion_receipt.py (integer-payloads §15), and in version 6 an upscale's by
upscale_verify.py (exact-upscale §7) and a tone mapping's by tonemap_verify.py (exact-tonemap §8).
Everything else is here:
- the tariff (§4): reading, rules, both signatures, its scope, its job prices per capability
(version 2) or per capability and operation (versions 3 and 4), and in version 4 its price of
a room's bytes out, of the work units of a package a run made, and of a package's storage by
byte-days (§4.7);
byte-days (§4.7), and in version 6 its prices of an endpoint and of a usage (§4.9);
- the invoice (§6): reading, rules, signature, chain, and what a chain bills once (§6.9): in
versions 3 and 4 a job's work, its route record and hop receipts, and never its output's
receipt; in version 4 a package's work once per run that made it, its receipt cited by each,
@ -68,6 +70,8 @@ import stitched_presentation_verify as STV # noqa: E402
import route_record_verify as RRV # noqa: E402 (it puts ../oer-conformance on the path)
import oer2_verify as OER # noqa: E402
import _int_motion_receipt as IMR # noqa: E402
import upscale_verify as UV # noqa: E402
import tonemap_verify as TV # noqa: E402
from _blake3 import blake3 # noqa: E402
from decimal import Decimal, ROUND_HALF_EVEN, localcontext, MAX_PREC, MAX_EMAX, MIN_EMIN # noqa: E402
@ -83,18 +87,25 @@ U64 = 2 ** 64 - 1
ROOM, DELIVERY, PACKAGE, ADS, JOB = 1, 2, 3, 4, 5 # §6.3 source kinds (5: version 2)
SESSION, HOP, STATEMENT, ACK, PACK, AD_RECEIPT, CLAIM, POD_INPUT, STITCHED_CLAIM = 1, 2, 3, 4, 5, 6, 7, 8, 9 # §5.2
OER_RECEIPT, VIDEO_RECEIPT, ROUTE_RECORD = 14, 15, 16 # §5.2, version 2 (10 to 13 are other texts')
THIRD_PARTY, UPSCALE_RECORD, RENDITION_RECORD = 17, 18, 19 # §5.2, version 6
PRIVATE = 128 # §5.2: 128 to 255, private and provisional use
KIND_NAMES = {ROOM: "room", DELIVERY: "delivery", PACKAGE: "package", ADS: "ads", JOB: "job"}
STITCHED = "stitched" # §5.1: a source kind billed as ads
SCOPES = {1: "room_hops", 2: "delivery_hops", 3: "packaging", 4: "ad_decisions", 5: "jobs", 6: "unbilled_hops"} # §6.6
CONTENT_DOMAIN = b"moq-jwp:object\x01" # route-record §2, jwp-receipts §2.1
DAY_US = 86400 * 10 ** 6 # §4.7: a byte-day's day
JOB_UNITS = ["jobs", "subpixels", "work_units"] # §4.5; the third in version 4 (§4.7)
JOB_VERSIONS = (2, 3, 4, 5) # §4.5 to §4.8: the versions that price jobs
WORK_VERSIONS = (3, 4, 5) # §4.6, §6.9: priced per operation, billed by the work
JOB_UNITS = ["jobs", "subpixels", "work_units", "usage"] # §4.5; the third from version 4 (§4.7), the fourth in 6 (§4.9)
USAGE = 4 # §4.9: the job unit of a third party's usage
JOB_VERSIONS = (2, 3, 4, 5, 6) # §4.5 to §4.9: the versions that price jobs
WORK_VERSIONS = (3, 4, 5, 6) # §4.6, §6.9: priced per operation, billed by the work
V4 = 4 # §4.7: bytes out, work units, storage by byte-days
V5 = 5 # §4.8: version 4, and a decode held to its frames
SPENDING = (V4, V5) # §4.7, §4.8: the versions that bill what the work spends
V6 = 6 # §4.9: version 5, per endpoint, by a usage, and re-derived jobs
SPENDING = (V4, V5, V6) # §4.7 to §4.9: the versions that bill what the work spends
DECODE_FRAMES = (V5, V6) # §4.8, §4.9: a decode held to its frames
TOKEN = re.compile(r"\A[A-Za-z0-9._:/-]{1,128}\Z") # route-record §2: an endpoint's form
UNIT_NAME = re.compile(r"\A[a-z][a-z0-9_]{0,63}\Z") # route-record §2: a unit's form
UPSCALE_OP, TONEMAP_OP = "media.upscale", "media.tonemap" # §4.9
CAPABILITY = re.compile(r"\Awai\.[a-z0-9_.]{0,124}\Z")
OPERATION = re.compile(r"\A[a-z][a-z0-9]*\.[a-z][a-z0-9]*\Z") # §4.6, route-record §2
LABELS = {2: "ModelBased", 3: "Estimator", 4: "OnChipCounter", 5: "CalibratedInstrument"}
@ -161,7 +172,7 @@ def head(b, magic):
return "malformed"
if len(b) < 5:
return "truncated"
if b[4] not in (1, 2, 3, 4, 5):
if b[4] not in (1, 2, 3, 4, 5, 6):
return "unknown_version"
return None
@ -194,12 +205,15 @@ def read_tariff(b):
t["package"] = [0, 0, r.u(8)] if b[4] in SPENDING else [r.u(8) for _ in range(3)]
t["ads"] = [r.u(8) for _ in range(2)]
t["version"] = b[4]
t["job"] = [] # (capability, operation, unit, divisor, price)
t["job"] = [] # (capability, operation, unit, divisor, price, endpoint, usage)
if t["version"] in JOB_VERSIONS: # §4.5, §4.6
for _ in range(r.u(2)):
cap = r.take(r.u(1))
op = r.take(r.u(1)) if t["version"] in WORK_VERSIONS else b""
t["job"].append((cap, op, r.u(1), r.u(8), r.u(8)))
endpoint = r.take(r.u(1)) if t["version"] == V6 else b""
unit = r.u(1)
usage = r.take(r.u(1)) if t["version"] == V6 else b""
t["job"].append((cap, op, unit, r.u(8), r.u(8), endpoint, usage))
t["scope"] = [(r.u(1), r.take(32)) for _ in range(r.u(2))]
body_end = r.p
seller_sig, buyer_sig = r.take(64), r.take(64)
@ -209,17 +223,24 @@ def read_tariff(b):
raise Refuse("malformed", ["tariff"])
def job_price(j):
try:
cap, op = j[0].decode("utf-8"), j[1].decode("utf-8")
cap, op, endpoint, usage = j[0].decode("utf-8"), j[1].decode("utf-8"), j[5].decode("utf-8"), j[6].decode("utf-8")
except UnicodeDecodeError:
return False
named = OPERATION.match(op) is not None and len(j[1]) <= 128 if t["version"] in WORK_VERSIONS else op == ""
units_priced = 3 if t["version"] in SPENDING else 2
return CAPABILITY.match(cap) is not None and named and 1 <= j[2] <= units_priced and j[3] >= 1
units_priced = USAGE if t["version"] == V6 else 3 if t["version"] in SPENDING else 2
ends = endpoint == "" or TOKEN.match(endpoint) is not None or "price-endpoint" in SKIP
unit_named = (UNIT_NAME.match(usage) is not None if j[2] == USAGE else usage == "") or "price-usage" in SKIP
return CAPABILITY.match(cap) is not None and named and ends and unit_named and 1 <= j[2] <= units_priced and j[3] >= 1
def overlap_free():
"""§4.9: a price of any endpoint and a price of one, for the same work, would bill a job at both."""
anys = {(j[0], j[1], j[2], j[6]) for j in t["job"] if j[5] == b""}
return not any(j[5] != b"" and (j[0], j[1], j[2], j[6]) in anys for j in t["job"])
top = 5 if t["version"] in JOB_VERSIONS else 4
rules = (t["start"] < t["end"] and capitals(t["currency"]) and t["seller"] != ZERO and t["buyer"] != ZERO
and t["seller"] != t["buyer"] and t["scope"] and ascending(t["scope"])
and all(1 <= k <= top and i != ZERO for k, i in t["scope"])
and len(t["job"]) <= 255 and ascending([(j[0], j[1], j[2]) for j in t["job"]]) and all(job_price(j) for j in t["job"]))
and len(t["job"]) <= 255 and (ascending([(j[0], j[1], j[5], j[2], j[6]) for j in t["job"]]) or "price-order" in SKIP)
and all(job_price(j) for j in t["job"]) and (overlap_free() or "any-beside-one" in SKIP))
if not rules and "tariff-rules" not in SKIP:
raise Refuse("tariff_invalid", ["tariff"])
body = b[:body_end]
@ -280,7 +301,9 @@ def read_invoice(b, at):
u = units(i["version"])
return x["kind"] in u and 1 <= x["unit"] <= len(u[x["kind"]]) and u[x["kind"]][x["unit"] - 1] is not None
rules = (i["start"] < i["end"] and capitals(i["currency"]) and ascending(i["records"])
and all(1 <= k <= 9 or (v2 and OER_RECEIPT <= k <= ROUTE_RECORD) for k, _ in i["records"]) and ascending(keys)
and all(1 <= k <= 9 or (v2 and OER_RECEIPT <= k <= ROUTE_RECORD)
or ((i["version"] == V6 or "kind-version" in SKIP) and THIRD_PARTY <= k <= RENDITION_RECORD) for k, _ in i["records"])
and ascending(keys)
and all(unit_ok(x) and x["quantity"] > 0 and x["cited"] > 0 for x in i["lines"])
and ascending([(e[0], e[1]) for e in i["energy"]])
and all(1 <= e[0] <= scopes(i["version"]) and 2 <= e[1] <= 5 and e[2] > 0 for e in i["energy"]))
@ -595,20 +618,129 @@ def frame_samples(z, w, h):
return PV.png_frame(still)[2]
def job_source(s, version):
if set(s) != {"kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"} \
or s["receipts"] is None or s["keys"] is None or not isinstance(s["outputs"], list):
HEX_BYTES = re.compile(r"(?:[0-9a-f]{2})*")
PNG_FAILS = (ValueError, zlib.error, struct.error, IndexError)
def is_hex(v):
return isinstance(v, str) and HEX_BYTES.fullmatch(v) is not None
def upscale_output(x, i):
"""§4.9: an upscale's output: its record proved by exact-upscale §7 from the input, a WAI1 of one
8-bit RGB PNG, and the prior; and the picture's samples the output it states. The link hash, the
subpixels and the picture."""
if not isinstance(x, dict) or set(x) != {"upscale", "object", "prior", "picture"} or not isinstance(x["upscale"], str) \
or not all(is_hex(x[k]) for k in ("object", "prior", "picture")):
raise Refuse("malformed", [])
doc = {k: s[k] for k in ("request", "pricebook", "policy", "record", "receipts", "keys")}
def bad(*at):
return Refuse("output_invalid", ["outputs", i] + list(at))
try:
u = UV.read(x["upscale"], "wai-upscale", UV.UPSCALE, UV.upscale_fields)
except UV.Refuse:
raise bad()
obj, prior, picture = bytes.fromhex(x["object"]), bytes.fromhex(x["prior"]), bytes.fromhex(x["picture"])
if "upscale-input" in SKIP:
rgb = b""
else:
try:
m, _, payload = UV.STV.read_wai1(obj)
if m["model_requirement"]["capability"] != "wai.image.png":
raise ValueError("another capability")
rgb = TV.png_decode(payload, False)[3]
except (UV.STV.NotWai1,) + PNG_FAILS:
raise bad("object")
case = {"claims": [], "upscales": [x["upscale"]], "presentations": [], "objects": [x["object"]],
"decoded": [{"object": content_hash(obj).hex(), "samples": rgb.hex()}], "priors": ["given"], "residuals": [], "targets": [],
"policy": {"dual_layer": "when-given", "assistive": "require-proof", "second_layer": "accept-stated", "upscale": "require-proof"}}
if "upscale-proof" not in SKIP:
try:
UV.verify(case, {}, {"given": prior})
except UV.Refuse:
raise bad()
f = UV.SR.FACTOR[u["derivation"]]
ow, oh = f * u["w"], f * u["h"]
try:
pw, ph, _, samples = TV.png_decode(picture, False)
except PNG_FAILS:
raise bad("picture")
if "upscale-picture" not in SKIP and ((pw, ph) != (ow, oh) or hashlib.sha256(samples).digest() != u["output"]):
raise bad("picture")
return u["link"], ow * oh * 3, picture
def read_proof(pr):
"""§5.1: an inclusion proof's form, or None."""
if not (isinstance(pr, dict) and set(pr) == {"index", "siblings"} and isinstance(pr["index"], int) and not isinstance(pr["index"], bool)
and 0 <= pr["index"] <= 2 ** 53 - 1 and isinstance(pr["siblings"], list)
and all(isinstance(h, str) and re.fullmatch(r"[0-9a-f]{64}", h) for h in pr["siblings"])):
return None
return pr["index"], [bytes.fromhex(h) for h in pr["siblings"]]
def tonemap_output(x, i, committed):
"""§4.9: a tone mapping's output: its record proved by exact-tonemap §8 from the envelope, and each
rendition the bytes its picture states. The link hash, the subpixels, each rendition's content hash
and its proof."""
want = {"tonemap", "envelope", "renditions"} | ({"proofs"} if committed else set())
if not isinstance(x, dict) or set(x) != want or not isinstance(x["tonemap"], str) or not is_hex(x["envelope"]) \
or not isinstance(x["renditions"], list) or not all(is_hex(b) for b in x["renditions"]):
raise Refuse("malformed", [])
if committed:
if not isinstance(x["proofs"], list) or len(x["proofs"]) != len(x["renditions"]):
raise Refuse("malformed", [])
proofs = [read_proof(p) for p in x["proofs"]]
if any(p is None for p in proofs):
raise Refuse("malformed", [])
else:
proofs = [None] * len(x["renditions"])
def bad(*at):
return Refuse("output_invalid", ["outputs", i] + list(at))
try:
r = TV.read_record(x["tonemap"])
except TV.Refuse:
raise bad()
env = bytes.fromhex(x["envelope"])
if "tonemap-proof" not in SKIP:
held = {"envelopes": {content_hash(env).hex(): env}, "frames": {}}
if TV.run_case({"records": [x["tonemap"]], "policy": "require-proof"}, held)["outcome"] != "accept":
raise bad()
renditions = [bytes.fromhex(b) for b in x["renditions"]]
if len(renditions) != len(r["pictures"]) and "tonemap-count" not in SKIP:
raise bad("renditions")
if "tonemap-renditions" not in SKIP:
for k, (b, p) in enumerate(zip(renditions, r["pictures"])):
if hashlib.sha256(b).hexdigest() != p["output"]:
raise bad("renditions", k)
w, h = r["base"]["width"], r["base"]["height"]
return bytes.fromhex(r["_link"]), len(r["pictures"]) * w * h * 3, [content_hash(b) for b in renditions], proofs
def job_source(s, version):
members = {"kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"}
third = "third_party" in s and (version == V6 or "third-party-version" in SKIP) # §4.9
if third:
members.add("third_party")
if set(s) != members or s["receipts"] is None or s["keys"] is None or not isinstance(s["outputs"], list):
raise Refuse("malformed", [])
doc = {k: s[k] for k in members - {"kind", "outputs"}}
got = RRV.verify(doc)
if got["outcome"] != "accept":
raise Refuse(got["reason"], got["at"])
records = got.get("third_party", [])
rec = json.loads(s["record"])
texts = [OER.load(io.StringIO(t)) for t in s["receipts"]]
billed, fig = None, None
billed, fig, endpoint, usage = None, None, b"", None
for a in rec["attempts"]:
if a["billed"] is True:
cap = next(c["capability"] for c in rec["candidates"] if c["endpoint"] == a["endpoint"])
endpoint = a["endpoint"].encode("utf-8")
# §4.9: a failed or late hop's record states what the provider billed, and no line reads it.
mine = records[:1] if "billed-usage" in SKIP else [r for r in records if r["hop"] == a["hop"]]
if mine and mine[0]["usage"] is not None:
usage = (mine[0]["usage"]["unit"].encode("utf-8"), mine[0]["usage"]["quantity"], bytes.fromhex(mine[0]["digest"]))
j = next(i for i, t in enumerate(texts) if isinstance(t, dict) and t.get("receipt_id") == a["receipt"])
fig = job_figure(texts[j]) if "job-figure" not in SKIP else (None, 0, 0)
if fig is None:
@ -655,19 +787,34 @@ def job_source(s, version):
return place < len(stated) and stated[place] == h
return h in delivered
def is_index(v):
return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= 2 ** 53 - 1
hex64 = re.compile(r"[0-9a-f]{64}")
seen = set()
outputs, packages = [], []
outputs, packages, upscales, renditions = [], [], [], []
hexs = re.compile(r"(?:[0-9a-f]{2})*")
# §4.8, version 5: a decode delivers one animated PNG of its frames, and its codestream is
# what it read; the served hop states exactly those animations, in order, and nothing else.
decode = version == V5 and rec["operation"] == "media.decode" and "decode-frames" not in SKIP
decode = version in DECODE_FRAMES and rec["operation"] == "media.decode" and "decode-frames" not in SKIP
# §4.9, version 6: an upscale and a tone mapping are given in their own forms.
of_six = version == V6 or "form-version" in SKIP
upscale = of_six and rec["operation"] == UPSCALE_OP
tonemap = of_six and rec["operation"] == TONEMAP_OP
stated = [bytes.fromhex(h) for a in rec["attempts"] if a["outcome"] == "served" for h in a["outputs"]] if rec["version"] == 2 else []
nxt = 0
for i, x in enumerate(s["outputs"]):
# One record's pictures are many objects, each with its proof. A picture that repeats within
# its record is one object its hop states once, and is billed as a picture (§4.9).
if tonemap:
link, n, objects, proofs = tonemap_output(x, i, committed)
renditions.append((link, n))
own = set()
for k, h in enumerate(objects):
fresh = h not in own
own.add(h)
if "output-delivered" in SKIP:
continue
if not holds(h, proofs[k], None) or (fresh and h in seen):
raise Refuse("output_not_delivered", ["outputs", i, "renditions", k])
seen.add(h)
continue
# Under a record of version 3 each output gives `proof`, and under one of version 1 or 2
# none (§5.1).
proof = None
@ -675,11 +822,9 @@ def job_source(s, version):
if not isinstance(x, dict) or "proof" not in x:
raise Refuse("malformed", [])
x = dict(x)
pr = x.pop("proof")
if not (isinstance(pr, dict) and set(pr) == {"index", "siblings"} and is_index(pr["index"]) and isinstance(pr["siblings"], list)
and all(isinstance(h, str) and hex64.fullmatch(h) for h in pr["siblings"])):
proof = read_proof(x.pop("proof"))
if proof is None:
raise Refuse("malformed", [])
proof = (pr["index"], [bytes.fromhex(h) for h in pr["siblings"]])
if decode:
if not isinstance(x, dict) or set(x) != {"receipt", "codestream", "frames"} or not isinstance(x["receipt"], str) \
or not isinstance(x["codestream"], str) or not hexs.fullmatch(x["codestream"]) \
@ -711,7 +856,10 @@ def job_source(s, version):
raise Refuse("output_not_delivered", ["outputs", i, "frames"])
nxt += 1
continue
if version in SPENDING and isinstance(x, dict) and set(x) == {"package", "indexes"}:
if upscale:
link, n, obj = upscale_output(x, i)
upscales.append((link, n))
elif version in SPENDING and isinstance(x, dict) and set(x) == {"package", "indexes"}:
# §4.7: a package a run of `package.build` made, its object its receipt's text.
if not isinstance(x["package"], str) or not isinstance(x["indexes"], list) \
or not all(isinstance(h, str) and hexs.fullmatch(h) for h in x["indexes"]):
@ -744,7 +892,8 @@ def job_source(s, version):
raise Refuse("output_not_delivered", ["outputs"]) # §4.8, 3: an object past the animations
return {"kind": JOB, "digest": bytes.fromhex(RRV.digest(rec)), "router": bytes.fromhex(rec["signer"]), "job": rec["job"],
"operation": rec["operation"].encode("utf-8"), "accepted": rec["accepted"], "hops": [oer_identity(a["receipt"]) for a in rec["attempts"]], "billed": billed,
"figure": fig, "outputs": outputs, "packages": packages, "unbilled": unbilled}
"figure": fig, "outputs": outputs, "packages": packages, "unbilled": unbilled, "endpoint": endpoint, "usage": usage,
"third_party": [bytes.fromhex(r["digest"]) for r in records], "upscales": upscales, "renditions": renditions}
_SOURCES = {}
@ -754,7 +903,9 @@ def verify_source(s, signers, version=1):
"""§5.1: the source verified by its own extension, a stitched source with `signers`, a job
source under a tariff of version 2 or 3, or Refuse with that extension's place."""
key = json.dumps([s, sorted(k.hex() for k in signers), version if isinstance(s, dict) and s.get("kind") == "job" else version in JOB_VERSIONS,
sorted(SKIP & {"job-figure", "output", "output-delivered", "unbilled-hops", "run-work", "decode-frames", "output-proof"})], sort_keys=True)
sorted(SKIP & {"job-figure", "output", "output-delivered", "unbilled-hops", "run-work", "decode-frames", "output-proof",
"third-party-version", "form-version", "billed-usage", "upscale-input", "upscale-proof", "upscale-picture",
"tonemap-proof", "tonemap-count", "tonemap-renditions"})], sort_keys=True)
if key not in _SOURCES:
try:
_SOURCES[key] = read_source(s, signers, version)
@ -808,6 +959,8 @@ def contributed(f):
elif f["kind"] == JOB:
out |= {(OER_RECEIPT, h) for h in f["hops"]} | {(VIDEO_RECEIPT, h) for h, _ in f["outputs"]}
out |= {(PACK, h) for h, _ in f["packages"]}
out |= {(THIRD_PARTY, d) for d in f["third_party"]}
out |= {(UPSCALE_RECORD, h) for h, _ in f["upscales"]} | {(RENDITION_RECORD, h) for h, _ in f["renditions"]}
return out
@ -882,6 +1035,8 @@ def billed_once(version, kind):
each invoice that bills its storage (§4.7)."""
if kind == PACK and version in SPENDING and "package-again" not in SKIP:
return False
if kind in (UPSCALE_RECORD, RENDITION_RECORD) and "evidence-again" not in SKIP:
return False # §4.9: what re-derives an output is its evidence
return not (kind == VIDEO_RECEIPT and (version in WORK_VERSIONS or "output-once" in SKIP))
@ -970,18 +1125,29 @@ def compute(t, facts):
if f["billed"] is None:
continue
cap, hop = f["billed"]
for k, (pcap, pop, unit, _, _) in enumerate(t["job"]):
for k, (pcap, pop, unit, _, _, pend, pusage) in enumerate(t["job"]):
if pcap != cap or (t["version"] in WORK_VERSIONS and pop != f["operation"] and "operation" not in SKIP):
continue
if pend and pend != f["endpoint"] and "endpoint" not in SKIP: # §4.9
continue
cites = [(ROUTE_RECORD, f["digest"]), (OER_RECEIPT, hop)]
if unit == 1:
n = 1
elif unit == 2:
cites += [(VIDEO_RECEIPT, h) for h, w in f["outputs"] if w > 0]
n = sum(w for _, w in f["outputs"])
else: # §4.7: the work units of its packages
cites += [(UPSCALE_RECORD, h) for h, w in f["upscales"] if w > 0]
cites += [(RENDITION_RECORD, h) for h, w in f["renditions"] if w > 0]
n = sum(w for _, w in f["outputs"] + f["upscales"] + f["renditions"])
elif unit == 3: # §4.7: the work units of its packages
cites += [(PACK, h) for h, w in f["packages"] if w > 0]
n = sum(w for _, w in f["packages"])
else: # §4.9: what the billed hop's record states
u = f["usage"]
if u is None or (u[0] != pusage and "usage-unit" not in SKIP):
n = 0
else:
n = u[1]
cites.append((THIRD_PARTY, u[2]))
add(JOB, f["router"], k + 1, n, cites)
else:
unit = 1 if f["claims_required"] else 2
@ -1241,6 +1407,10 @@ def line_json(x, t):
out["capability"] = t["job"][x["unit"] - 1][0].decode("utf-8")
if t["version"] in WORK_VERSIONS:
out["operation"] = t["job"][x["unit"] - 1][1].decode("utf-8")
if t["version"] == V6: # §7.3: its endpoint, empty for any; a usage's unit
out["endpoint"] = t["job"][x["unit"] - 1][5].decode("utf-8")
if t["job"][x["unit"] - 1][2] == USAGE:
out["usage_unit"] = t["job"][x["unit"] - 1][6].decode("utf-8")
return out
@ -1400,6 +1570,34 @@ def mutations(doc):
m = copy.deepcopy(doc)
m["sources"]["job_v5_decode"]["outputs"][0]["frames"] = doc["sources"][other]["outputs"][0]["frames"]
out.append((f"job_v5_decode: {what}", m))
if "job_tp_alpha" in doc["sources"]: # §4.9: what a third party bills is signed
m = copy.deepcopy(doc)
m["sources"]["job_tp_alpha"]["third_party"] = doc["sources"]["job_tp_beta"]["third_party"]
out.append(("job_tp_alpha: another job's record", m))
m = copy.deepcopy(doc)
m["sources"]["job_tp_failover"]["third_party"] = doc["sources"]["job_tp_failover"]["third_party"][1:]
out.append(("job_tp_failover: its failed hop's record left out", m))
m = copy.deepcopy(doc)
m["sources"]["job_tp_alpha"]["third_party"][0] = doc["sources"]["job_tp_alpha"]["third_party"][0].replace('"quantity":8', '"quantity":80')
out.append(("job_tp_alpha: its record's usage raised, its signature unchanged", m))
if "job_upscale" in doc["sources"]: # §4.9: re-derived, and of what the hop delivered
m = copy.deepcopy(doc)
o = m["sources"]["job_upscale"]["outputs"]
o[0]["picture"], o[1]["picture"] = o[1]["picture"], o[0]["picture"]
out.append(("job_upscale: its two pictures swapped", m))
m = copy.deepcopy(doc)
m["sources"]["job_upscale"]["outputs"][0]["object"] = doc["sources"]["job_upscale"]["outputs"][1]["object"]
out.append(("job_upscale: its first record given the second's input", m))
m = copy.deepcopy(doc)
m["sources"]["job_upscale"]["outputs"][0]["proof"]["index"] = 1
out.append(("job_upscale: its first picture proved at the second place", m))
m = copy.deepcopy(doc)
m["sources"]["job_tonemap"]["outputs"][0]["proofs"][0]["index"] = 1
out.append(("job_tonemap: its first rendition proved at the second place", m))
m = copy.deepcopy(doc)
r = m["sources"]["job_tonemap"]["outputs"][0]["renditions"]
r[0], r[1] = r[1], r[0]
out.append(("job_tonemap: its first two renditions swapped", m))
if "job_v3r_encode" in doc["sources"]: # §5.1: an output proved at its place
m = copy.deepcopy(doc)
m["sources"]["job_v3r_encode"]["outputs"][0]["proof"]["index"] = 0
@ -1423,7 +1621,9 @@ CHECKS = ["signatures", "buyer-signature", "tariff-rules", "invoice-rules", "row
"previous-given", "previous-overlap", "scope", "conflict", "period", "duplicate", "overlap", "rebilled", "quantity",
"summed", "citations", "mismatch", "job-figure", "output", "job-duplicate", "private-kind", "operation", "output-once",
"output-delivered", "unbilled-hops", "storage-held", "package-again", "run-work", "bytes-out", "conflict-bytes",
"decode-frames", "output-proof"]
"decode-frames", "output-proof", "price-endpoint", "price-usage", "price-order", "any-beside-one",
"kind-version", "third-party-version", "form-version", "billed-usage", "endpoint", "usage-unit", "evidence-again",
"upscale-input", "upscale-proof", "upscale-picture", "tonemap-proof", "tonemap-count", "tonemap-renditions"]
def main(corpus_dir):

View file

@ -515,7 +515,7 @@ settlement = ["live_metering", "jwp_hash"]
# 14), and an `int_motion` job's decode receipt (video_receipt, kind 15); under a tariff of
# version 5 a decode's frames, each a PNG read by packaging §5.7's reader (png_still) and held to
# its receipt's frame (§4.8). SHA-256, Ed25519 and the sources' own readers; no new dependency.
receipt_ledger =["live_metering", "settlement", "live_ads", "video_package", "stitched_presentation", "route_record", "video_receipt", "png_still"]
receipt_ledger =["live_metering", "settlement", "live_ads", "video_package", "stitched_presentation", "route_record", "video_receipt", "png_still", "exact_upscale", "exact_tonemap"]
# Route record (extensions/route-record.md): `wai.route.record` — what a router considered for one
# media job and why it chose: the request, the price book and the routing policy as documents
# pinned by digest; the candidates, the limits that removed each, the ranking, the order and the

View file

@ -67,8 +67,16 @@ pub const VERSION_4: u8 = 4;
/// The version of a tariff that bills as version 4 does and holds a decode to what it delivered,
/// its frames, and of its invoices (§4.8).
pub const VERSION_5: u8 = 5;
/// The version of a tariff that bills as version 5 does, prices a job per endpoint and by a third
/// party's usage, and bills an upscale and a tone mapping by what re-derives them, and of its
/// invoices (§4.9).
pub const VERSION_6: u8 = 6;
/// The catalogue's operation whose hop delivers a reconstruction, not a codestream (§4.8).
pub const DECODE: &str = "media.decode";
/// The catalogue's operation whose output is an upscale record's picture (§4.9).
pub const UPSCALE: &str = "media.upscale";
/// The catalogue's operation whose outputs are a rendition record's pictures (§4.9).
pub const TONEMAP: &str = "media.tonemap";
pub const TARIFF_DOMAIN: &[u8] = b"wai:ledger-tariff\x01";
pub const ACCEPT_DOMAIN: &[u8] = b"wai:ledger-accept\x01";
pub const INVOICE_DOMAIN: &[u8] = b"wai:ledger-invoice\x01";
@ -120,6 +128,11 @@ pub mod record_kind {
pub const OER_RECEIPT: u8 = 14;
pub const VIDEO_RECEIPT: u8 = 15;
pub const ROUTE_RECORD: u8 = 16;
/// Version 6: a job source's third-party hop records, upscale records and rendition records
/// (§5.2, §4.9).
pub const THIRD_PARTY: u8 = 17;
pub const UPSCALE_RECORD: u8 = 18;
pub const RENDITION_RECORD: u8 = 19;
/// The first kind reserved for private and provisional use (§5.2): 128 to 255. A WAI record
/// never states one (`private_kind`).
pub const PRIVATE: u8 = 128;
@ -141,11 +154,11 @@ pub mod scope {
["", "room_hops", "delivery_hops", "packaging", "ad_decisions", "jobs", "unbilled_hops"].get(s as usize).copied().unwrap_or("")
}
/// The scopes an invoice of `version` states (§6.1): 4, 5 in versions 2 and 3, 6 in 4 and 5.
/// The scopes an invoice of `version` states (§6.1): 4, 5 in versions 2 and 3, 6 in 4 to 6.
pub fn count(version: u8) -> u8 {
match version {
super::VERSION_2 | super::VERSION_3 => JOBS,
super::VERSION_4 | super::VERSION_5 => UNBILLED_HOPS,
super::VERSION_4 | super::VERSION_5 | super::VERSION_6 => UNBILLED_HOPS,
_ => AD_DECISIONS,
}
}
@ -358,7 +371,7 @@ impl<'a> Rd<'a> {
}
}
/// The first steps of reading (§4.3, §6.8): the magic and the version, 1 to 4.
/// The first steps of reading (§4.3, §6.8): the magic and the version, 1 to 6.
fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<u8, &'static str> {
if b.len() < 4 {
return Err(TRUNCATED);
@ -368,60 +381,89 @@ fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<u8, &'static str> {
}
match b.get(4) {
None => Err(TRUNCATED),
Some(&v) if (VERSION..=VERSION_5).contains(&v) => Ok(v),
Some(&v) if (VERSION..=VERSION_6).contains(&v) => Ok(v),
Some(_) => Err(UNKNOWN_VERSION),
}
}
/// Whether a tariff or an invoice of `version` prices jobs (§4.5, §4.6): versions 2 to 5.
/// Whether a tariff or an invoice of `version` prices jobs (§4.5, §4.6): versions 2 to 6.
pub fn prices_jobs(version: u8) -> bool {
(VERSION_2..=VERSION_5).contains(&version)
(VERSION_2..=VERSION_6).contains(&version)
}
/// Whether a tariff of `version` prices a job by its operation and bills its work (§4.6, §6.9):
/// versions 3 to 5.
/// versions 3 to 6.
pub fn bills_work(version: u8) -> bool {
(VERSION_3..=VERSION_5).contains(&version)
(VERSION_3..=VERSION_6).contains(&version)
}
/// Whether a tariff of `version` bills what the work spends (§4.7, §6.6): a room's bytes out, a
/// package's work per run and its storage by byte-time, and every hop's energy. Versions 4 and 5.
/// package's work per run and its storage by byte-time, and every hop's energy. Versions 4 to 6.
pub fn bills_spending(version: u8) -> bool {
version == VERSION_4 || version == VERSION_5
(VERSION_4..=VERSION_6).contains(&version)
}
/// Whether a tariff of `version` holds a decode to the frames it delivered, the codestream it
/// read being its input (§4.8): version 5.
/// read being its input (§4.8): versions 5 and 6.
pub fn holds_decode_to_frames(version: u8) -> bool {
version == VERSION_5
version == VERSION_5 || version == VERSION_6
}
/// The room prices a tariff of `version` states (§4.1): fourteen in versions 4 and 5, else
/// Whether a tariff of `version` prices a job per endpoint and by a third party's usage, and
/// bills an upscale and a tone mapping by what re-derives them (§4.9): version 6.
pub fn prices_endpoints(version: u8) -> bool {
version == VERSION_6
}
/// The room prices a tariff of `version` states (§4.1): fourteen in versions 4 to 6, else
/// thirteen.
pub fn room_prices(version: u8) -> usize {
if bills_spending(version) { ROOM_PRICES } else { ROOM_PRICES_1 }
}
/// A job price (§4.5, §4.6): a capability, in version 3 an operation, a unit (1 `jobs`, 2
/// `subpixels`), a divisor and a price.
/// A job price (§4.5, §4.6, §4.9): a capability, in version 3 an operation, in version 6 an
/// endpoint, a unit (1 `jobs`, 2 `subpixels`, and from version 4 3 `work_units`, in version 6 4
/// `usage`, with the usage's unit), a divisor and a price.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct JobPrice {
pub capability: String,
/// Version 3: the catalogue operation it prices (§4.6). Empty in version 2, whose job
/// prices name none.
pub operation: String,
/// Version 6: the endpoint it prices (§4.9), or empty for any. Empty before version 6.
pub endpoint: String,
pub unit: u8,
/// Version 6, unit 4: the unit the usage is stated in (§4.9). Empty otherwise.
pub usage: String,
pub divisor: u64,
pub price: u64,
}
/// The job units, in unit order (§4.5): `work_units`, the work its output packages state, in
/// version 4 only (§4.7).
pub const JOB_UNITS: [&str; 3] = ["jobs", "subpixels", "work_units"];
/// The job units, in unit order (§4.5): `work_units`, the work its output packages state, from
/// version 4 (§4.7), and `usage`, what a third party bills for its hop, in version 6 (§4.9).
pub const JOB_UNITS: [&str; 4] = ["jobs", "subpixels", "work_units", "usage"];
/// The job units a tariff of `version` prices (§4.5, §4.7): two, and three in versions 4 and 5.
/// The job unit a third party's hop is billed in (§4.9).
const USAGE: u8 = 4;
/// The job units a tariff of `version` prices (§4.5, §4.7, §4.9): two, three in versions 4 and 5,
/// and four in version 6.
pub fn job_units(version: u8) -> u8 {
if bills_spending(version) { 3 } else { 2 }
if prices_endpoints(version) {
USAGE
} else if bills_spending(version) {
3
} else {
2
}
}
impl JobPrice {
/// What a tariff orders its job prices by (§4.3): each is empty before the version that
/// states it.
fn order(&self) -> (&[u8], &[u8], &[u8], u8, &[u8]) {
(self.capability.as_bytes(), self.operation.as_bytes(), self.endpoint.as_bytes(), self.unit, self.usage.as_bytes())
}
}
fn job_capability(c: &str) -> bool {
@ -449,25 +491,27 @@ pub struct Tariff {
/// 1; 2 for a tariff that prices jobs (§4.5); 3 for one that prices them per operation and
/// bills their work (§4.6, §6.9); 4 for one that also prices bytes out, a package's work and
/// its storage by byte-time (§4.7); 5 for one that bills as version 4 does and holds a decode
/// to the frames it delivered (§4.8).
/// to the frames it delivered (§4.8); 6 for one that also prices a job per endpoint and by a
/// third party's usage, and bills an upscale and a tone mapping (§4.9).
pub version: u8,
pub seller: [u8; 32],
pub buyer: [u8; 32],
pub start: u64,
pub end: u64,
pub currency: [u8; 3],
/// Live-metering §4.1's thirteen prices, in its order, and in versions 4 and 5 a fourteenth,
/// Live-metering §4.1's thirteen prices, in its order, and in versions 4 to 6 a fourteenth,
/// per 10^9 bytes out; versions 1 to 3 state no fourteenth, read as 0.
pub room: [u32; ROOM_PRICES],
/// Settlement §7.1's four prices, in class order.
pub delivery: [u64; 4],
/// Per package receipt, per entry, per 10^9 stored bytes; in versions 4 and 5 only the third,
/// Per package receipt, per entry, per 10^9 stored bytes; in versions 4 to 6 only the third,
/// per 10^9 byte-days held, the first two neither written nor read, 0 (§4.7).
pub package: [u64; 3],
/// Per presented impression, per billable impression.
pub ads: [u64; 2],
/// Versions 2 and 3: the job prices, in strictly ascending order of (capability, unit) in
/// version 2 (§4.5) and of (capability, operation, unit) in version 3 (§4.6).
/// Versions 2 to 6: the job prices, in strictly ascending order of (capability, unit) in
/// version 2 (§4.5), of (capability, operation, unit) in versions 3 to 5 (§4.6), and of
/// (capability, operation, endpoint, unit, usage) in version 6 (§4.9).
pub job: Vec<JobPrice>,
/// (kind, id), strictly ascending.
pub scope: Vec<(u8, [u8; 32])>,
@ -502,7 +546,15 @@ impl Tariff {
o.push(u8::try_from(j.operation.len()).expect("an operation of at most 255 bytes"));
o.extend_from_slice(j.operation.as_bytes());
}
if prices_endpoints(self.version) {
o.push(u8::try_from(j.endpoint.len()).expect("an endpoint of at most 255 bytes"));
o.extend_from_slice(j.endpoint.as_bytes());
}
o.push(j.unit);
if prices_endpoints(self.version) {
o.push(u8::try_from(j.usage.len()).expect("a usage unit of at most 255 bytes"));
o.extend_from_slice(j.usage.as_bytes());
}
o.extend_from_slice(&j.divisor.to_le_bytes());
o.extend_from_slice(&j.price.to_le_bytes());
}
@ -573,7 +625,20 @@ impl Tariff {
} else {
String::new()
};
job.push(JobPrice { capability, operation, unit: r.u8()?, divisor: r.u64()?, price: r.u64()? });
let endpoint = if prices_endpoints(version) {
let len = r.u8()? as usize;
text(r.take(len)?)
} else {
String::new()
};
let unit = r.u8()?;
let usage = if prices_endpoints(version) {
let len = r.u8()? as usize;
text(r.take(len)?)
} else {
String::new()
};
job.push(JobPrice { capability, operation, endpoint, unit, usage, divisor: r.u64()?, price: r.u64()? });
}
}
let n = r.u16()? as usize;
@ -597,10 +662,17 @@ impl Tariff {
&& ascending(&t.scope)
&& t.scope.iter().all(|(k, id)| (1..=if prices_jobs(t.version) { 5 } else { 4 }).contains(k) && *id != [0; 32])
&& t.job.len() <= 255
&& t.job.windows(2).all(|w| (w[0].capability.as_bytes(), w[0].operation.as_bytes(), w[0].unit) < (w[1].capability.as_bytes(), w[1].operation.as_bytes(), w[1].unit))
&& t.job.windows(2).all(|w| w[0].order() < w[1].order())
&& t.job.iter().all(|j| {
let operation = if bills_work(t.version) { job_operation(&j.operation) } else { j.operation.is_empty() };
job_capability(&j.capability) && operation && (1..=job_units(t.version)).contains(&j.unit) && j.divisor >= 1
let endpoint = j.endpoint.is_empty() || crate::route_record::token(&j.endpoint);
let usage = if j.unit == USAGE { crate::route_record::unit(&j.usage) } else { j.usage.is_empty() };
job_capability(&j.capability) && operation && endpoint && usage && (1..=job_units(t.version)).contains(&j.unit) && j.divisor >= 1
})
// One job is billed once for a unit: a price of an endpoint beside a price of any,
// for the same work, would bill it at both (§4.9).
&& t.job.iter().filter(|j| j.endpoint.is_empty()).all(|any| {
!t.job.iter().any(|j| !j.endpoint.is_empty() && (&j.capability, &j.operation, j.unit, &j.usage) == (&any.capability, &any.operation, any.unit, &any.usage))
});
if !rules {
return Err((Reason::TariffInvalid.code(), at()));
@ -802,7 +874,11 @@ impl Invoice {
let rules = i.start < i.end
&& capitals(&i.currency)
&& ascending(&i.records)
&& i.records.iter().all(|(k, _)| (1..=9).contains(k) || (v2 && (record_kind::OER_RECEIPT..=record_kind::ROUTE_RECORD).contains(k)))
&& i.records.iter().all(|(k, _)| {
(1..=9).contains(k)
|| (v2 && (record_kind::OER_RECEIPT..=record_kind::ROUTE_RECORD).contains(k))
|| (prices_endpoints(version) && (record_kind::THIRD_PARTY..=record_kind::RENDITION_RECORD).contains(k))
})
&& ascending(&keys)
&& i.lines.iter().all(|l| {
let unit_ok = if l.source_kind == source_kind::JOB { v2 && l.unit >= 1 } else { unit_name_in(version, l.source_kind, l.unit).is_some() };
@ -973,7 +1049,7 @@ pub struct StitchedFacts {
pub statement: Value,
}
/// A verified job source (§5.1, versions 2 to 5).
/// A verified job source (§5.1, versions 2 to 6).
#[derive(Clone, Debug)]
pub struct JobFacts {
/// The route record's digest, and its signer: the router.
@ -988,14 +1064,27 @@ pub struct JobFacts {
pub hops: Vec<[u8; 32]>,
/// The billed hop, if any: its serving capability and its receipt's identity.
pub billed: Option<(String, [u8; 32])>,
/// The billed hop's endpoint, empty when no hop is billed (§4.9).
pub endpoint: String,
/// Under a tariff of version 6, the billed hop's third-party record when it states a usage:
/// the usage's unit and quantity, and the record's digest (§4.9).
pub usage: Option<(String, u64, [u8; 32])>,
/// Under a tariff of version 6, every third-party record the source gives, by its digest.
pub third_party: Vec<[u8; 32]>,
/// Under a tariff of version 6, each upscale record (§4.9): its link hash and the subpixels
/// of the picture it derives.
pub upscales: Vec<([u8; 32], u64)>,
/// Under a tariff of version 6, each rendition record (§4.9): its link hash and the subpixels
/// of the pictures it derives.
pub renditions: Vec<([u8; 32], u64)>,
/// The billed hop's figure (§5.1).
pub figure: Option<Figure>,
/// Each output receipt: its receipt hash and its `work_pixel_ops`.
pub outputs: Vec<([u8; 32], u64)>,
/// Under a tariff of version 4 or 5, each output package (§4.7): its receipt hash and the
/// Under a tariff of versions 4 to 6, each output package (§4.7): its receipt hash and the
/// work units it states.
pub packages: Vec<([u8; 32], u64)>,
/// Under a tariff of version 4 or 5, the figure of each attempt that is not billed, a failed
/// Under a tariff of versions 4 to 6, the figure of each attempt that is not billed, a failed
/// hop or one that served past the deadline, in attempt order (§6.6); empty under versions 2
/// and 3, which state no such figure.
pub unbilled: Vec<Figure>,
@ -1041,6 +1130,9 @@ impl Facts {
s.extend(j.hops.iter().map(|h| (record_kind::OER_RECEIPT, *h)));
s.extend(j.outputs.iter().map(|(h, _)| (record_kind::VIDEO_RECEIPT, *h)));
s.extend(j.packages.iter().map(|(h, _)| (record_kind::PACKAGE, *h)));
s.extend(j.third_party.iter().map(|d| (record_kind::THIRD_PARTY, *d)));
s.extend(j.upscales.iter().map(|(h, _)| (record_kind::UPSCALE_RECORD, *h)));
s.extend(j.renditions.iter().map(|(h, _)| (record_kind::RENDITION_RECORD, *h)));
}
}
s
@ -1471,6 +1563,99 @@ fn decode_output(x: &Map<String, Value>, i: usize) -> Result<DecodeOutput, Refus
Ok(DecodeOutput { receipt: (r.receipt_hash(), r.work_pixel_ops), object: crate::route_record::content_hash(&frames) })
}
/// An upscale's output under a tariff of version 6 (§4.9): its record's link hash and the
/// subpixels of the picture it derives, and the picture, the object its hop delivered.
struct UpscaleOutput {
record: ([u8; 32], u64),
picture: Vec<u8>,
}
/// §4.9: an upscale's output of exactly `upscale`, `object`, `prior` and `picture`; the record
/// proved by exact-upscale §7 from the input and the prior, which a record naming a residual
/// never is here, and the picture's samples the output it states. A refusal is `output_invalid`
/// at `outputs`, `i`, and for the input `object`, for the picture `picture`.
fn upscale_output(x: &Map<String, Value>, i: usize) -> Result<UpscaleOutput, Refusal> {
use crate::exact_upscale as ex;
let bad = |at: Value| (Reason::OutputInvalid.code(), place(json!(["outputs", i]), at));
if !has_exactly(x, &["upscale", "object", "prior", "picture"]) {
return Err(bad_source());
}
let (Some(text), Some(object), Some(prior), Some(picture)) = (x["upscale"].as_str(), bytes_of(&x["object"]), bytes_of(&x["prior"]), bytes_of(&x["picture"])) else { return Err(bad_source()) };
let u = ex::read_upscale(text).map_err(|_| bad(json!([])))?;
// The input is a picture this reader holds in full: a `WAI1` of one 8-bit RGB PNG.
let env = crate::container::Wai::from_bytes(&object).map_err(|_| bad(json!(["object"])))?;
if env.capability() != crate::codecs::CAP_IMAGE_PNG {
return Err(bad(json!(["object"])));
}
let (_, _, rgb) = crate::png_still::decode(&env.payload).map_err(|_| bad(json!(["object"])))?;
let decoded = vec![(crate::route_record::content_hash(&object), rgb)];
let case = ex::Case {
upscales: vec![text.to_string()],
objects: vec![object],
decoded,
priors: vec![prior],
policy: ex::Policy { require_proof: true, ..Default::default() },
..Default::default()
};
if !matches!(ex::verify(&case), ex::Outcome::Accept(_)) {
return Err(bad(json!([])));
}
let (w, h) = u.output_size();
let (pw, ph, samples) = crate::png_still::decode(&picture).map_err(|_| bad(json!(["picture"])))?;
if (u64::from(pw), u64::from(ph)) != (w, h) || sha256(&[&samples]) != u.output {
return Err(bad(json!(["picture"])));
}
Ok(UpscaleOutput { record: (u.link, w * h * 3), picture })
}
/// A tone mapping's output under a tariff of version 6 (§4.9): its record's link hash and the
/// subpixels of its pictures, and each rendition's content hash, with its proof where the route
/// record states what its hop delivered by a root.
struct TonemapOutput {
record: ([u8; 32], u64),
objects: Vec<[u8; 32]>,
proofs: Vec<Option<(u64, Vec<[u8; 32]>)>>,
}
/// §4.9: a tone mapping's output of exactly `tonemap`, `envelope` and `renditions`, and under a
/// route record of version 3 `proofs`; the record proved by exact-tonemap §8 from the envelope,
/// and each rendition the bytes its picture states. A refusal is `output_invalid` at `outputs`,
/// `i`, and for a rendition `renditions` and its index.
fn tonemap_output(x: &Map<String, Value>, i: usize, committed: bool) -> Result<TonemapOutput, Refusal> {
use crate::exact_tonemap as tm;
let bad = |at: Value| (Reason::OutputInvalid.code(), place(json!(["outputs", i]), at));
let members: &[&str] = if committed { &["tonemap", "envelope", "renditions", "proofs"] } else { &["tonemap", "envelope", "renditions"] };
if !has_exactly(x, members) {
return Err(bad_source());
}
let (Some(text), Some(envelope), Some(renditions)) = (x["tonemap"].as_str(), bytes_of(&x["envelope"]), hexes(&x["renditions"])) else { return Err(bad_source()) };
let proofs: Vec<Option<(u64, Vec<[u8; 32]>)>> = if committed {
let given = x["proofs"].as_array().ok_or_else(bad_source)?;
if given.len() != renditions.len() {
return Err(bad_source());
}
given.iter().map(|p| read_proof(p).map(Some).ok_or_else(bad_source)).collect::<Result<_, _>>()?
} else {
vec![None; renditions.len()]
};
let r = tm::read_record(text).map_err(|_| bad(json!([])))?;
let mut held = tm::Held::default();
held.hold_envelope(envelope);
if !matches!(tm::verify(std::slice::from_ref(&text.to_string()), &held, tm::Policy::RequireProof), tm::Outcome::Accept(_)) {
return Err(bad(json!([])));
}
if renditions.len() != r.pictures.len() {
return Err(bad(json!(["renditions"])));
}
for (k, (b, p)) in renditions.iter().zip(&r.pictures).enumerate() {
if tm::digest(b) != p.output {
return Err(bad(json!(["renditions", k])));
}
}
let objects = renditions.iter().map(|b| crate::route_record::content_hash(b)).collect();
Ok(TonemapOutput { record: (r.link, r.pictures.len() as u64 * r.width * r.height * 3), objects, proofs })
}
/// What the hop that served delivered, as its route record states it (route-record §5.2).
enum Delivered {
/// The content hashes of its outputs, in their order (version 2), or none.
@ -1515,7 +1700,11 @@ fn read_proof(v: &Value) -> Option<(u64, Vec<[u8; 32]>)> {
fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
use crate::route_record as rr;
if !has_exactly(o, &["kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"]) {
const MEMBERS: [&str; 8] = ["kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"];
// Version 6 reads a third party's hop records too (§4.9); before it, the member is unknown.
let third_party = prices_endpoints(version) && o.contains_key("third_party");
let members: Vec<&str> = MEMBERS.into_iter().chain(third_party.then_some("third_party")).collect();
if !has_exactly(o, &members) {
return Err(bad_source());
}
let mut doc = o.clone();
@ -1525,15 +1714,20 @@ fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
if doc["receipts"].is_null() || doc["keys"].is_null() {
return Err(bad_source());
}
rr::verify(&doc).map_err(|f| (f.reason.code(), Value::Array(f.at)))?;
let verified = rr::verify(&doc).map_err(|f| (f.reason.code(), Value::Array(f.at)))?;
let records = verified.third_party.unwrap_or_default();
let record = crate::oer::load(doc["record"].as_str().expect("a text").as_bytes()).expect("an accepted record reads");
let texts: Vec<Value> = doc["receipts"].as_array().expect("a list").iter().map(|t| crate::oer::load(t.as_str().expect("a text").as_bytes()).expect("read")).collect();
let attempts = record["attempts"].as_array().expect("a list");
let hops: Vec<[u8; 32]> = attempts.iter().map(|a| oer_identity(a["receipt"].as_str().expect("an id")).expect("an id")).collect();
let receipt_of = |a: &Value| texts.iter().position(|r| r.get("receipt_id") == Some(&a["receipt"])).expect("a checked receipt");
let (mut billed, mut figure) = (None, None);
let (mut billed, mut figure, mut billed_endpoint, mut usage) = (None, None, String::new(), None);
if let Some(a) = attempts.iter().find(|a| a["billed"] == true) {
let endpoint = a["endpoint"].as_str().expect("a token");
billed_endpoint = endpoint.to_string();
// A failed hop's or a late hop's record states what the provider billed, and no line reads
// it: only the billed hop's usage is billed (§4.9).
usage = records.iter().find(|r| Some(r.hop) == a["hop"].as_u64()).and_then(|r| Some((r.usage.get("unit")?.as_str()?.to_string(), r.usage.get("quantity")?.as_u64()?, r.digest)));
let cap = record["candidates"].as_array().expect("a list").iter().find(|c| c["endpoint"] == endpoint).and_then(|c| c["capability"].as_str()).expect("a survivor");
let id = a["receipt"].as_str().expect("an id");
let j = receipt_of(a);
@ -1571,14 +1765,31 @@ fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
// Under a tariff of version 5 a decode delivers its reconstruction, one animation of its
// frames, and its codestream is what it read (§4.8).
let decode = holds_decode_to_frames(version) && record["operation"] == DECODE;
// Under a tariff of version 6 an upscale and a tone mapping are billed by what re-derives
// them, and given in their own forms (§4.9).
let upscale = prices_endpoints(version) && record["operation"] == UPSCALE;
let tonemap = prices_endpoints(version) && record["operation"] == TONEMAP;
// A decode's animations are held to what its hop delivered in order: `next` is the place in
// the served attempt's outputs of the next.
let mut next = 0usize;
let mut seen: BTreeSet<[u8; 32]> = BTreeSet::new();
let (mut outs, mut packages) = (Vec::with_capacity(outputs.len()), Vec::new());
let (mut outs, mut packages, mut upscales, mut renditions) = (Vec::with_capacity(outputs.len()), Vec::new(), Vec::new(), Vec::new());
for (i, x) in outputs.iter().enumerate() {
let bad = || (Reason::OutputInvalid.code(), json!(["outputs", i]));
let x = x.as_object().ok_or_else(bad_source)?;
// One record's pictures are many objects, each with its proof. A picture that repeats
// within its record is one object its hop states once, and is billed as a picture.
if tonemap {
let out = tonemap_output(x, i, committed)?;
renditions.push(out.record);
let mut own: BTreeSet<[u8; 32]> = BTreeSet::new();
for (k, h) in out.objects.iter().enumerate() {
if !delivered.as_ref().is_some_and(|d| d.holds(h, out.proofs[k].as_ref(), None)) || (own.insert(*h) && !seen.insert(*h)) {
return Err((Reason::OutputNotDelivered.code(), json!(["outputs", i, "renditions", k])));
}
}
continue;
}
// With a record of version 3 each output also gives its inclusion proof, and with one of
// version 1 or 2 none (§5.1).
let (x, proof) = if committed {
@ -1599,9 +1810,14 @@ fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
next += 1;
continue;
}
// Each output's object: an int_motion codestream, or in versions 4 and 5 a package's
// receipt, the package a run of `package.build` made (§4.7), verified against its indexes.
let object = if bills_spending(version) && has_exactly(x, &["package", "indexes"]) {
// Each output's object: an int_motion codestream, or from version 4 a package's receipt,
// the package a run of `package.build` made (§4.7), verified against its indexes, or in
// version 6 an upscale's picture (§4.9).
let object = if upscale {
let out = upscale_output(x, i)?;
upscales.push(out.record);
out.picture
} else if bills_spending(version) && has_exactly(x, &["package", "indexes"]) {
let (Some(text), Some(indexes)) = (x["package"].as_str(), hexes(&x["indexes"])) else { return Err(bad_source()) };
let r = PackageReceiptV2::from_json(text).map_err(|_| bad())?;
let ix: Vec<&[u8]> = indexes.iter().map(Vec::as_slice).collect();
@ -1639,6 +1855,11 @@ fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
accepted: record["accepted"].as_u64().expect("a time"),
hops,
billed,
endpoint: billed_endpoint,
usage,
third_party: records.iter().map(|r| r.digest).collect(),
upscales,
renditions,
figure,
outputs: outs,
packages,
@ -1752,18 +1973,18 @@ fn overlap(version: u8, f: &Facts, g: &Facts) -> bool {
}
/// Whether a chain under a tariff of `version` bills a record of `kind` once (§6.9): every
/// record but, in versions 3 to 5, a job's output receipt, and in versions 4 and 5 a package
/// receipt.
/// record but, in versions 3 to 6, a job's output receipt, in versions 4 to 6 a package receipt,
/// and in version 6 an upscale record and a rendition record.
/// A job's work, its route record and its hops' OER/2 receipts, is new on every run and billed
/// once; its output receipt is the evidence of what it delivered, which a deterministic re-run
/// delivers again. In versions 4 and 5 a package is such an output: the run that made it is
/// delivers again. In versions 4 to 6 a package is such an output: the run that made it is
/// billed once per run, its receipt cited by each, and its storage billed by every invoice whose
/// period it is held through (§4.7).
pub fn billed_once(version: u8, kind: u8) -> bool {
!((bills_work(version) && kind == record_kind::VIDEO_RECEIPT) || (bills_spending(version) && kind == record_kind::PACKAGE))
!((bills_work(version) && matches!(kind, record_kind::VIDEO_RECEIPT | record_kind::UPSCALE_RECORD | record_kind::RENDITION_RECORD)) || (bills_spending(version) && kind == record_kind::PACKAGE))
}
/// Under a tariff of version 4 or 5, mark each package the previous invoice holds as held
/// Under a tariff of versions 4 to 6, mark each package the previous invoice holds as held
/// through the period [`start`, `end`) (§4.7); under versions 1 to 3, nothing.
pub fn hold(t: &Tariff, facts: &mut [Facts], previous: Option<&Invoice>, start: u64, end: u64) {
if !bills_spending(t.version) {
@ -1904,11 +2125,12 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
}
}
// A job is billed at the prices of its serving capability, in version 3 of its
// operation too, from its billed hop (§6.3). An output it shares with another job is
// cited once and counted for each (§6.5).
// operation too, in version 6 of its endpoint where the price names one, from its
// billed hop (§6.3). An output it shares with another job is cited once and counted
// for each (§6.5).
Facts::Job(j) => {
let Some((cap, hop)) = &j.billed else { continue };
let priced = |p: &JobPrice| p.capability == *cap && (!bills_work(t.version) || p.operation == j.operation);
let priced = |p: &JobPrice| p.capability == *cap && (!bills_work(t.version) || p.operation == j.operation) && (p.endpoint.is_empty() || p.endpoint == j.endpoint);
for (k, p) in t.job.iter().enumerate().filter(|(_, p)| priced(p)) {
let unit = u8::try_from(k + 1).expect("at most 255 job prices");
let mut cites = vec![(record_kind::ROUTE_RECORD, j.record), (record_kind::OER_RECEIPT, *hop)];
@ -1916,13 +2138,24 @@ pub fn compute(t: &Tariff, facts: &[Facts]) -> Result<Computed, Overflow> {
1 => 1,
2 => {
cites.extend(j.outputs.iter().filter(|(_, w)| *w > 0).map(|(h, _)| (record_kind::VIDEO_RECEIPT, *h)));
j.outputs.iter().map(|(_, w)| *w as u128).sum()
cites.extend(j.upscales.iter().filter(|(_, w)| *w > 0).map(|(h, _)| (record_kind::UPSCALE_RECORD, *h)));
cites.extend(j.renditions.iter().filter(|(_, w)| *w > 0).map(|(h, _)| (record_kind::RENDITION_RECORD, *h)));
j.outputs.iter().chain(&j.upscales).chain(&j.renditions).map(|(_, w)| *w as u128).sum()
}
// Version 4: the work units its output packages state (§4.7).
_ => {
3 => {
cites.extend(j.packages.iter().filter(|(_, w)| *w > 0).map(|(h, _)| (record_kind::PACKAGE, *h)));
j.packages.iter().map(|(_, w)| *w as u128).sum()
}
// Version 6: what the billed hop's record states the provider bills, in
// the unit the price names; no such record bills nothing (§4.9).
_ => match &j.usage {
Some((unit, quantity, digest)) if *unit == p.usage => {
cites.push((record_kind::THIRD_PARTY, *digest));
*quantity as u128
}
_ => 0,
},
};
add(source_kind::JOB, j.router, unit, n, &cites);
}
@ -2353,6 +2586,13 @@ pub fn accepted_json(a: &Accepted) -> Value {
if bills_work(a.tariff.version) {
v["operation"] = json!(p.operation);
}
// In version 6 its endpoint, empty for any, and for a usage its unit (§7.3).
if prices_endpoints(a.tariff.version) {
v["endpoint"] = json!(p.endpoint);
if p.unit == USAGE {
v["usage_unit"] = json!(p.usage);
}
}
}
v
})
@ -2427,8 +2667,8 @@ mod tests {
let mut t = tariff();
t.version = VERSION_2;
t.job = vec![
JobPrice { capability: "wai.video.av1".into(), operation: String::new(), unit: 1, divisor: 1, price: 7 },
JobPrice { capability: "wai.video.int_motion".into(), operation: String::new(), unit: 2, divisor: 1_000_000, price: 9 },
JobPrice { capability: "wai.video.av1".into(), operation: String::new(), unit: 1, divisor: 1, price: 7, endpoint: String::new(), usage: String::new() },
JobPrice { capability: "wai.video.int_motion".into(), operation: String::new(), unit: 2, divisor: 1_000_000, price: 9, endpoint: String::new(), usage: String::new() },
];
t.scope.push((source_kind::JOB, [7; 32]));
t.scope.sort();
@ -2438,7 +2678,7 @@ mod tests {
fn tariff_v3() -> Tariff {
let mut t = tariff_v2();
t.version = VERSION_3;
let p = |op: &str, unit, divisor, price| JobPrice { capability: "wai.video.int_motion".into(), operation: op.into(), unit, divisor, price };
let p = |op: &str, unit, divisor, price| JobPrice { capability: "wai.video.int_motion".into(), operation: op.into(), unit, divisor, price, endpoint: String::new(), usage: String::new() };
t.job = vec![p("media.decode", 2, 1_000_000, 10_000), p("media.encode", 1, 1, 500_000), p("media.encode", 2, 1_000_000, 20_000)];
t.sign(&key(1), &key(2))
}
@ -2492,10 +2732,10 @@ mod tests {
let at = nb.windows(2).position(|w| w == [0xc3, 0xa9]).expect("the two bytes of é");
nb[at] = 0xff;
assert_eq!(Tariff::parse(&nb).unwrap_err().0, "tariff_invalid");
// A tariff of version 6 is not defined.
let mut v6 = b.clone();
v6[4] = 6;
assert_eq!(Tariff::parse(&v6).unwrap_err().0, "unknown_version");
// A tariff of version 7 is not defined.
let mut v7 = b.clone();
v7[4] = 7;
assert_eq!(Tariff::parse(&v7).unwrap_err().0, "unknown_version");
}
fn tariff_v4() -> Tariff {
@ -2503,7 +2743,7 @@ mod tests {
t.version = VERSION_4;
t.room[13] = 11;
t.package = [0, 0, 5];
t.job.push(JobPrice { capability: "wai.video.package".into(), operation: "package.build".into(), unit: 3, divisor: 1, price: 4 });
t.job.push(JobPrice { capability: "wai.video.package".into(), operation: "package.build".into(), unit: 3, divisor: 1, price: 4, endpoint: String::new(), usage: String::new() });
t.sign(&key(1), &key(2))
}
@ -2526,7 +2766,7 @@ mod tests {
assert_eq!(v3.body(), tariff_v3().body(), "version 3 states no price of bytes");
// A job price of unit 3, `work_units`, only in version 4.
let mut work = tariff_v3();
work.job.push(JobPrice { capability: "wai.video.package".into(), operation: "package.build".into(), unit: 3, divisor: 1, price: 4 });
work.job.push(JobPrice { capability: "wai.video.package".into(), operation: "package.build".into(), unit: 3, divisor: 1, price: 4, endpoint: String::new(), usage: String::new() });
assert_eq!(Tariff::parse(&work.sign(&key(1), &key(2)).to_bytes()).unwrap_err().0, "tariff_invalid");
let mut four = tariff_v4();
four.job[3].unit = 4;
@ -2554,7 +2794,7 @@ mod tests {
assert!(!billed_once(VERSION_4, record_kind::PACKAGE) && billed_once(VERSION_3, record_kind::PACKAGE));
assert!(!billed_once(VERSION_4, record_kind::VIDEO_RECEIPT));
assert!(billed_once(VERSION_4, record_kind::OER_RECEIPT) && billed_once(VERSION_4, record_kind::ROUTE_RECORD) && billed_once(VERSION_4, record_kind::SESSION));
assert!(bills_work(VERSION_4) && prices_jobs(VERSION_4) && !prices_jobs(6));
assert!(bills_work(VERSION_4) && prices_jobs(VERSION_4) && !prices_jobs(7));
assert_eq!((scope::count(VERSION), scope::count(VERSION_2), scope::count(VERSION_3), scope::count(VERSION_4)), (4, 5, 5, 6));
}
@ -2581,7 +2821,7 @@ mod tests {
for k in [record_kind::PACKAGE, record_kind::VIDEO_RECEIPT, record_kind::OER_RECEIPT, record_kind::ROUTE_RECORD, record_kind::SESSION] {
assert_eq!(billed_once(VERSION_5, k), billed_once(VERSION_4, k), "kind {k}");
}
assert!(bills_work(VERSION_5) && prices_jobs(VERSION_5) && bills_spending(VERSION_5) && !prices_jobs(6));
assert!(bills_work(VERSION_5) && prices_jobs(VERSION_5) && bills_spending(VERSION_5) && !prices_jobs(7));
assert_eq!((scope::count(VERSION_5), room_prices(VERSION_5), job_units(VERSION_5)), (6, ROOM_PRICES, 3));
assert_eq!(unit_name_in(VERSION_5, source_kind::PACKAGE, 3), Some("byte_days"));
assert!(holds_decode_to_frames(VERSION_5) && (VERSION..=VERSION_4).all(|v| !holds_decode_to_frames(v)), "only version 5 holds a decode to its frames");
@ -2589,6 +2829,40 @@ mod tests {
/// What a chain bills once (§6.9): in version 3, every record but an output receipt; in
/// versions 1 and 2, every record.
fn tariff_v6(job: Vec<JobPrice>) -> Tariff {
let mut t = tariff_v4();
t.version = VERSION_6;
t.job = job;
t.sign(&key(1), &key(2))
}
fn usage_price(endpoint: &str, unit: &str) -> JobPrice {
JobPrice { capability: "wai.video.generate".into(), operation: "media.generate".into(), endpoint: endpoint.into(), unit: 4, usage: unit.into(), divisor: 1, price: 3 }
}
#[test]
fn a_tariff_of_version_6_prices_an_endpoint_and_a_usage() {
let t = tariff_v6(vec![usage_price("gen.alpha", "seconds"), usage_price("gen.beta", "seconds")]);
let back = Tariff::parse(&t.to_bytes()).unwrap();
assert_eq!(back, t);
assert_eq!(back.job[1].endpoint, "gen.beta");
assert_eq!(back.job[1].usage, "seconds");
assert!(prices_endpoints(VERSION_6) && (VERSION..=VERSION_5).all(|v| !prices_endpoints(v)), "only version 6 prices an endpoint");
assert!(bills_work(VERSION_6) && bills_spending(VERSION_6) && holds_decode_to_frames(VERSION_6));
assert_eq!((scope::count(VERSION_6), room_prices(VERSION_6), job_units(VERSION_6)), (6, ROOM_PRICES, 4));
assert_eq!(job_units(VERSION_5), 3);
// A record a line reads a usage from is the work's, billed once; what re-derives an output
// is the output's evidence, which a re-run cites again.
assert!(billed_once(VERSION_6, record_kind::THIRD_PARTY));
assert!(!billed_once(VERSION_6, record_kind::UPSCALE_RECORD) && !billed_once(VERSION_6, record_kind::RENDITION_RECORD));
// One job is billed once for a unit: a price of any endpoint beside a price of one is refused.
let both = tariff_v6(vec![usage_price("", "seconds"), usage_price("gen.alpha", "seconds")]);
assert_eq!(Tariff::parse(&both.to_bytes()).unwrap_err().0, "tariff_invalid");
// The same endpoint in another unit, and any endpoint in another unit, are other prices.
let other = tariff_v6(vec![usage_price("", "images"), usage_price("gen.alpha", "seconds")]);
assert!(Tariff::parse(&other.to_bytes()).is_ok());
}
#[test]
fn a_chain_bills_a_jobs_work_once_and_cites_its_output_again() {
for k in [record_kind::SESSION, record_kind::PACKAGE, record_kind::OER_RECEIPT, record_kind::ROUTE_RECORD] {
@ -2596,7 +2870,7 @@ mod tests {
}
assert!(billed_once(VERSION_2, record_kind::VIDEO_RECEIPT), "version 2 bills an output once");
assert!(!billed_once(VERSION_3, record_kind::VIDEO_RECEIPT), "version 3 bills the work, and cites the output");
assert!(prices_jobs(VERSION_2) && prices_jobs(VERSION_3) && !prices_jobs(VERSION) && !prices_jobs(6));
assert!(prices_jobs(VERSION_2) && prices_jobs(VERSION_3) && !prices_jobs(VERSION) && !prices_jobs(7));
}
/// A tariff of version 2 reads back with its job prices, and every altered byte is refused;
@ -2748,8 +3022,8 @@ mod tests {
assert_eq!(Invoice::parse(&unit.sign(&key(1)).to_bytes()).unwrap_err(), "invoice_invalid");
assert_eq!(Invoice::parse(&[b.clone(), vec![0]].concat()).unwrap_err(), "malformed");
let mut v = b.clone();
v[4] = 6;
assert_eq!(Invoice::parse(&v).unwrap_err(), "unknown_version", "version 6 is not defined (2 to 5 are, §6.1)");
v[4] = 7;
assert_eq!(Invoice::parse(&v).unwrap_err(), "unknown_version", "version 7 is not defined (2 to 6 are, §6.1)");
let mut s = b;
let n = s.len();
s[n - 1] ^= 1;

View file

@ -828,7 +828,7 @@ pub fn generate(wai: &Path) -> Result<String, String> {
Vector { name: "tariff_period_empty", about: "a tariff whose period is empty, signed by both", tariff: tariff_period.to_bytes(), invoice: ab.clone(), previous: None, sources: day_a.to_vec() },
// The invoice.
Vector { name: "invoice_trailing_byte", about: "an invoice with a byte after its signature", tariff: tb.clone(), invoice: [ab.clone(), vec![0]].concat(), previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_version", about: "an invoice of version 6, which this revision does not define", tariff: tb.clone(), invoice: { let mut b = ab.clone(); b[4] = 6; b }, previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_version", about: "an invoice of version 7, which this revision does not define", tariff: tb.clone(), invoice: { let mut b = ab.clone(); b[4] = 7; b }, previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_signature", about: "an invoice whose signature does not verify", tariff: tb.clone(), invoice: flip(ab.clone(), ab.len() - 1), previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_lines_unordered", about: "an invoice whose first two lines are swapped", tariff: tb.clone(), invoice: sealed(&t, [0; 32], a0, a1, &refs(&day_a), |i| i.lines.swap(0, 1)).to_bytes(), previous: None, sources: day_a.to_vec() },
Vector { name: "invoice_row_of_no_class", about: "an energy row of tag 0: joules of no one class", tariff: tb.clone(), invoice: sealed(&t, [0; 32], a0, a1, &refs(&day_a), |i| i.energy.insert(0, Row { scope: 1, tag: 0, figures: 1, joules: 1, max_ppm: 0 })).to_bytes(), previous: None, sources: day_a.to_vec() },
@ -875,9 +875,9 @@ pub fn generate(wai: &Path) -> Result<String, String> {
// Version 2: a tariff that prices jobs (§4.5), and its invoices.
let router: [u8; 32] = routes["keys"]["router"].as_str().and_then(unhex).and_then(|b| b.try_into().ok()).ok_or("the route corpus's router")?;
let job_prices = vec![
JobPrice { capability: "wai.video.av1".into(), operation: String::new(), unit: 1, divisor: 1, price: 2_000_000 },
JobPrice { capability: "wai.video.int_motion".into(), operation: String::new(), unit: 1, divisor: 1, price: 500_000 },
JobPrice { capability: "wai.video.int_motion".into(), operation: String::new(), unit: 2, divisor: 1_000_000, price: 30_000 },
JobPrice { capability: "wai.video.av1".into(), operation: String::new(), unit: 1, divisor: 1, price: 2_000_000, endpoint: String::new(), usage: String::new() },
JobPrice { capability: "wai.video.int_motion".into(), operation: String::new(), unit: 1, divisor: 1, price: 500_000, endpoint: String::new(), usage: String::new() },
JobPrice { capability: "wai.video.int_motion".into(), operation: String::new(), unit: 2, divisor: 1_000_000, price: 30_000, endpoint: String::new(), usage: String::new() },
];
let v2 = |scope: Vec<(u8, [u8; 32])>, job: Vec<JobPrice>| -> Tariff {
let mut t2 = tariff_of("buyer-j", room_prices, scope);
@ -929,7 +929,7 @@ pub fn generate(wai: &Path) -> Result<String, String> {
return Err(format!("the route corpus accepts its jobs at {routed_at}, its seeds at {accepted}"));
}
const IM: &str = "wai.video.int_motion";
let jp = |capability: &str, operation: &str, unit: u8, divisor: u64, price: u64| JobPrice { capability: capability.into(), operation: operation.into(), unit, divisor, price };
let jp = |capability: &str, operation: &str, unit: u8, divisor: u64, price: u64| JobPrice { capability: capability.into(), operation: operation.into(), unit, divisor, price, endpoint: String::new(), usage: String::new() };
let job_prices_3 = vec![
jp("wai.video.av1", "media.encode", 1, 1, 2_000_000),
jp(IM, "media.decode", 1, 1, 250_000),
@ -1116,6 +1116,389 @@ pub fn generate(wai: &Path) -> Result<String, String> {
refused_r3("route_record_3_an_output_of_a_job_no_hop_served", "a job whose route record is of version 3 and whose every allowed hop failed, given an output, which no hop delivered", "job_v3r_no_hop_served"),
]);
// Version 6 (§4.9): version 5's rules, a job priced per endpoint and by what a third party's
// record states the provider bills, and an upscale and a tone mapping billed by what re-derives
// them. Everything a third party's hop states is in its record, and a failed hop's billed usage
// reaches no line.
use crate::container::{Conditioning, Manifest, ModelRequirement, Rendition, Wai, WaiMulti};
use crate::exact_tonemap as tm;
use crate::exact_upscale as ex;
const GEN: &str = "wai.video.generate";
const GENERATE: &str = "media.generate";
const UP: &str = "wai.media.upscale";
let jp6 = |capability: &str, operation: &str, endpoint: &str, unit: u8, usage: &str, divisor: u64, price: u64| JobPrice {
capability: capability.into(),
operation: operation.into(),
endpoint: endpoint.into(),
unit,
usage: usage.into(),
divisor,
price,
};
let mut job_prices_6 = job_prices_4.clone();
job_prices_6.extend([
jp6(GEN, GENERATE, "", 1, "", 1, 10_000),
// A usage of any endpoint, in a unit no endpoint's price names: another price, not a second
// price of the same work, so it stands beside them and bills nothing here.
jp6(GEN, GENERATE, "", 4, "minutes", 1, 1),
// And a price of one endpoint in another unit than a price of any: also another price.
jp6(GEN, GENERATE, "gen.beta", 2, "", 1, 1),
jp6(GEN, GENERATE, "gen.alpha", 4, "images", 1, 5_000_000),
jp6(GEN, GENERATE, "gen.alpha", 4, "seconds", 1, 40_000_000),
jp6(GEN, GENERATE, "gen.beta", 4, "seconds", 1, 25_000_000),
jp6(UP, UPSCALE, "", 2, "", 1_000_000, 20_000),
jp6(tm::CAPABILITY, TONEMAP, "", 2, "", 1_000_000, 15_000),
]);
job_prices_6.sort_by(|a, b| a.order().cmp(&b.order()));
let v6 = |version: u8, job: Vec<JobPrice>| -> Tariff {
let mut t = tariff_of("buyer-4", room_prices_4, vec![(source_kind::ROOM, lesson_id), package_scope, staged_scope, (source_kind::JOB, router)]);
t.version = version;
t.package = [0, 0, 2_000_000];
t.job = job;
t.sign(&seller, &key("buyer-4"))
};
let t_6 = v6(VERSION_6, job_prices_6.clone());
let t6b = t_6.to_bytes();
// A third party's jobs: its hops report no energy, and its records state what it bills.
let tp_object = |job: &str| [rr_hash(format!("wai receipt-ledger corpus: {job}, what the provider made").as_bytes())];
let tp_hop = |job: &str, endpoint: &'static str| rr::Hop { endpoint, error: None, start: 1_000, end: 4_000_000, receipt: rr::unmetered_hop_receipt(GENERATE, job, endpoint) };
let tp_doc = |job: &str, unit: &str, endpoints: &[(&str, &str)], hops: &[rr::Hop]| rr::job_document_of(job, GENERATE, GEN, unit, endpoints, accepted, hops, &tp_object(job), true);
let tp_source = |d: &Value, records: Option<Vec<String>>| {
let mut j = routed(d.clone(), vec![]);
if let Some(r) = records {
j["third_party"] = json!(r);
}
j
};
const ALPHA: (&str, &str) = ("gen.alpha", "prov-alpha");
const BETA: (&str, &str) = ("gen.beta", "prov-beta");
let alpha_doc = tp_doc("job-7001", "seconds", &[ALPHA], &[tp_hop("job-7001", "gen.alpha")]);
let alpha_record = rr::third_party_text(&alpha_doc, 1, &tp_object("job-7001"), Some(8));
pool.insert("job_tp_alpha", tp_source(&alpha_doc, Some(vec![alpha_record.clone()])));
let beta_doc = tp_doc("job-7002", "seconds", &[BETA], &[tp_hop("job-7002", "gen.beta")]);
let beta_record = rr::third_party_text(&beta_doc, 1, &tp_object("job-7002"), Some(3));
pool.insert("job_tp_beta", tp_source(&beta_doc, Some(vec![beta_record.clone()])));
let failover_doc = tp_doc(
"job-7003",
"seconds",
&[ALPHA, BETA],
&[
rr::Hop { endpoint: "gen.alpha", error: Some("server_error"), start: 1_000, end: 2_000_000, receipt: rr::failed_hop_receipt(GENERATE, "job-7003", "gen.alpha", None) },
rr::Hop { endpoint: "gen.beta", error: None, start: 2_000_100, end: 9_000_000, receipt: rr::unmetered_hop_receipt(GENERATE, "job-7003", "gen.beta") },
],
);
let failover_records = vec![rr::third_party_text(&failover_doc, 1, &[], Some(1)), rr::third_party_text(&failover_doc, 2, &tp_object("job-7003"), Some(5))];
pool.insert("job_tp_failover", tp_source(&failover_doc, Some(failover_records.clone())));
let unrecorded_doc = tp_doc("job-7004", "seconds", &[ALPHA], &[tp_hop("job-7004", "gen.alpha")]);
pool.insert("job_tp_unrecorded", tp_source(&unrecorded_doc, None));
let images_doc = tp_doc("job-7005", "images", &[ALPHA], &[tp_hop("job-7005", "gen.alpha")]);
pool.insert("job_tp_images", tp_source(&images_doc, Some(vec![rr::third_party_text(&images_doc, 1, &tp_object("job-7005"), Some(2))])));
let frames_doc = tp_doc("job-7006", "frames", &[ALPHA], &[tp_hop("job-7006", "gen.alpha")]);
pool.insert("job_tp_a_unit_no_price_names", tp_source(&frames_doc, Some(vec![rr::third_party_text(&frames_doc, 1, &tp_object("job-7006"), Some(240))])));
pool.insert("job_tp_another_jobs_record", tp_source(&alpha_doc, Some(vec![beta_record.clone()])));
let day_6 = ["job_tp_alpha", "job_tp_beta", "job_tp_failover", "job_tp_unrecorded", "job_tp_images", "job_tp_a_unit_no_price_names"];
// An upscale's fixtures: pictures of a pattern, each a `WAI1` of its PNG, derived under the
// generic prior, and a record of each.
let prior = std::fs::read(wai.join("live-conformance/upscale/priors/generic.wsr")).map_err(|e| format!("the generic prior: {e}"))?;
let other_prior = std::fs::read(wai.join("live-conformance/upscale/priors/title.wsr")).map_err(|e| format!("the title prior: {e}"))?;
let loaded = crate::int_sr::load(&prior).map_err(|e| format!("the generic prior loads: {e:?}"))?;
let pattern = |w: u32, h: u32, seed: u32| -> Vec<u8> { (0..w * h).flat_map(|i| [((i * 7 + seed) % 256) as u8, ((i / w * 9 + seed) % 256) as u8, (((i ^ 0x55) + seed) % 256) as u8]).collect() };
let wai1 = |capability: &str, tag: &str, png: Vec<u8>| Wai::new(Manifest::new("image", "replicate", ModelRequirement::new(capability), Conditioning::new(tag)), png).to_bytes().expect("an envelope");
let sha = |b: &[u8]| hex(&sha256(&[b]));
let upscale_of = |rgb: &[u8], w: u32, h: u32| -> (Value, Vec<u8>, Vec<u8>) {
let envelope = wai1(crate::codecs::CAP_IMAGE_PNG, "png", crate::png_still::encode(rgb, w, h));
let out = crate::int_sr::derive(crate::int_sr::SR2, &loaded, rgb, w as usize, h as usize).expect("the derivation");
let digest = sha(&out);
let record = ex::sign(
json!({
"kind": "wai-upscale",
"object": hex(&rr_hash(&envelope)),
"input": {"width": w, "height": h, "samples": sha(rgb)},
"derivation": crate::int_sr::SR2,
"prior": sha(&prior),
"upscaled": digest,
"residual": Value::Null,
"output": digest,
"target": Value::Null,
"issued_at": "2026-10-20T00:00:00Z",
}),
&key("output-sink"),
);
(record, envelope, out)
};
let upscale_output = |record: &Value, envelope: &[u8], prior: &[u8], picture: &[u8]| json!({"upscale": crate::jcs::jcs(record).expect("a form"), "object": hex(envelope), "prior": hex(prior), "picture": hex(picture)});
let up_doc = |job: &str, at: u64, joules: &str, objects: &[[u8; 32]]| rr::job_document_of(job, UPSCALE, UP, "subpixels", &[("sr.alpha", "prov-tx")], at, &[rr::Hop { endpoint: "sr.alpha", error: None, start: 1_000, end: 4_000_000, receipt: rr::hop_receipt(UPSCALE, job, "sr.alpha", Some(joules)) }], objects, true);
let (rgb_a, rgb_b) = (pattern(6, 4, 0), pattern(4, 3, 31));
let (rec_a, env_a, out_a) = upscale_of(&rgb_a, 6, 4);
let (rec_b, env_b, out_b) = upscale_of(&rgb_b, 4, 3);
let (png_a, png_b) = (crate::png_still::encode(&out_a, 12, 8), crate::png_still::encode(&out_b, 8, 6));
let out_up_a = upscale_output(&rec_a, &env_a, &prior, &png_a);
let out_up_b = upscale_output(&rec_b, &env_b, &prior, &png_b);
let up_objects = [rr_hash(&png_a), rr_hash(&png_b)];
pool.insert("job_upscale", routed(up_doc("job-7101", accepted, "6.5", &up_objects), vec![proved(&out_up_a, &up_objects, 0), proved(&out_up_b, &up_objects, 1)]));
pool.insert("job_upscale_run_again", routed(up_doc("job-7101", accepted + 2 * DAY_US, "6.25", &up_objects), vec![proved(&out_up_a, &up_objects, 0), proved(&out_up_b, &up_objects, 1)]));
// One upscale whose output's defect is the one its name says; the record states the object it
// is given, so what is refused is the defect and not its delivery.
let up_one = |job: &str, x: &Value, picture: &[u8]| {
let objects = [rr_hash(picture)];
routed(up_doc(job, accepted, "6.5", &objects), vec![proved(x, &objects, 0)])
};
let mut altered = out_a.clone();
altered[0] ^= 1;
let altered_png = crate::png_still::encode(&altered, 12, 8);
pool.insert("job_upscale_a_picture_altered", up_one("job-7110", &upscale_output(&rec_a, &env_a, &prior, &altered_png), &altered_png));
let wrong_shape = crate::png_still::encode(&out_a, 8, 12);
pool.insert("job_upscale_a_picture_of_another_shape", up_one("job-7111", &upscale_output(&rec_a, &env_a, &prior, &wrong_shape), &wrong_shape));
let not_png = b"not a PNG".to_vec();
pool.insert("job_upscale_a_picture_that_does_not_read", up_one("job-7112", &upscale_output(&rec_a, &env_a, &prior, &not_png), &not_png));
pool.insert("job_upscale_another_prior", up_one("job-7113", &upscale_output(&rec_a, &env_a, &other_prior, &png_a), &png_a));
pool.insert("job_upscale_another_input", up_one("job-7114", &upscale_output(&rec_a, &env_b, &prior, &png_a), &png_a));
let not_an_envelope = b"not an envelope".to_vec();
pool.insert("job_upscale_an_input_that_does_not_read", up_one("job-7115", &upscale_output(&rec_a, &not_an_envelope, &prior, &png_a), &png_a));
let jpeg_capability = wai1(crate::codecs::CAP_IMAGE_JPEG, "jpeg", crate::png_still::encode(&rgb_a, 6, 4));
pool.insert("job_upscale_an_input_of_another_capability", up_one("job-7116", &upscale_output(&rec_a, &jpeg_capability, &prior, &png_a), &png_a));
let grey_input = wai1(crate::codecs::CAP_IMAGE_PNG, "png", crate::png_still::encode_grey(&[9u8; 24], 6, 4));
pool.insert("job_upscale_an_input_not_rgb", up_one("job-7117", &upscale_output(&rec_a, &grey_input, &prior, &png_a), &png_a));
let lie = |f: &mut dyn FnMut(&mut Value)| {
let mut body = rec_a.clone();
f(&mut body);
body.as_object_mut().expect("an object").remove("sig");
body.as_object_mut().expect("an object").remove("signer_pubkey");
ex::sign(body, &key("output-sink"))
};
let record_other_output = lie(&mut |b| b["upscaled"] = json!(sha(&out_b)));
pool.insert("job_upscale_a_record_of_another_output", up_one("job-7118", &upscale_output(&record_other_output, &env_a, &prior, &png_a), &png_a));
let record_residual = lie(&mut |b| b["residual"] = json!({"sha256": sha(b"a residual"), "q": 1}));
pool.insert("job_upscale_a_record_naming_a_residual", up_one("job-7119", &upscale_output(&record_residual, &env_a, &prior, &png_a), &png_a));
let mut unsigned = out_up_a.clone();
unsigned["upscale"] = json!(unsigned["upscale"].as_str().expect("a text").replacen("issued_at\":\"2026", "issued_at\":\"2025", 1));
pool.insert("job_upscale_a_record_that_does_not_verify", up_one("job-7120", &unsigned, &png_a));
let up_elsewhere = [rr_hash(b"wai receipt-ledger corpus: another picture")];
let mut undelivered = out_up_a.clone();
undelivered["proof"] = json!({"index": 0, "siblings": []});
pool.insert("job_upscale_a_picture_its_hop_did_not_deliver", routed(up_doc("job-7121", accepted, "6.5", &up_elsewhere), vec![undelivered]));
pool.insert("job_upscale_one_output_twice", routed(up_doc("job-7122", accepted, "6.5", &up_objects), vec![proved(&out_up_a, &up_objects, 0), proved(&out_up_a, &up_objects, 0)]));
pool.insert("job_upscale_in_an_encodes_form", routed(up_doc("job-7123", accepted, "6.5", &[rr_hash(&clip)]), vec![proved(&enc_out, &[rr_hash(&clip)], 0)]));
let up_v2 = rr::job_document_of("job-7124", UPSCALE, UP, "subpixels", &[("sr.alpha", "prov-tx")], accepted, &[rr::Hop { endpoint: "sr.alpha", error: None, start: 1_000, end: 4_000_000, receipt: rr::hop_receipt(UPSCALE, "job-7124", "sr.alpha", Some("6.5")) }], &up_objects, false);
pool.insert("job_upscale_under_a_list", routed(up_v2, vec![out_up_a.clone(), out_up_b.clone()]));
// A tone mapping's fixtures: a `WAI2` of a PNG base and its gain map, and a record of three
// headrooms.
// A multiple of the map's scale, 4, on each side (exact-tonemap §4.3).
let (tw, th) = (8u32, 4u32);
let scene = tm::corpus::scene(0, tw, th);
let encoded = tm::corpus::encode(&scene, tw, th, 4, false, (1, 1), false);
let companion_of = |e: &tm::corpus::Encoded| tm::wgm1(&e.metadata.to_bytes(), &crate::png_still::encode_grey(&e.map, e.map_w, e.map_h));
let companion = companion_of(&encoded);
let envelope_of = |base: &[u8], companion: &[u8]| {
WaiMulti::new(
"image",
"replicate",
Value::Null,
vec![
Rendition::media(crate::codecs::CAP_IMAGE_PNG, "png", crate::png_still::encode(base, tw, th)).with_id("base").with_payload_digest(),
Rendition::companion(tm::CAP_GAINMAP, companion.to_vec(), crate::codecs::CAP_IMAGE_PNG).with_id("gainmap").with_companion_of_id("base").with_payload_digest(),
],
)
.to_bytes()
.expect("an envelope")
};
let tm_envelope = envelope_of(&encoded.base, &companion);
let tm_other = envelope_of(&pattern(tw, th, 77), &companion);
let tm_record_of = |base: &[u8], companion: &[u8], envelope: &[u8], headrooms: &[u64]| -> (Value, Vec<Vec<u8>>) {
let gain = tm::read_gainmap(companion).expect("the gain map");
let renditions: Vec<Vec<u8>> = headrooms.iter().map(|h| tm::rgb16_bytes(&gain.render(base, tw, th, *h).expect("a rendition"))).collect();
let input = tm::hex(&tm::digest(base));
let object = tm::hex(&rr_hash(envelope));
let pictures: Vec<Value> = headrooms.iter().zip(&renditions).map(|(h, b)| json!({"frame": 0, "headroom": h, "input": input, "output": tm::hex(&tm::digest(b))})).collect();
let record = tm::sign(
json!({
"kind": tm::KIND,
"derivation": tm::GAINMAP,
"base": {"object": object, "rendition": 0, "width": tw, "height": th},
"companion": {"index": 1, "sha256": tm::hex(&tm::digest(companion))},
"pictures": pictures,
"class": "reconstructed",
"inputs": [{"content_hash": object, "origin": Value::Null, "class": "captured"}],
"issued_at": "2026-10-20T00:00:00Z",
}),
&key("output-sink"),
);
(record, renditions)
};
let (tm_record, tm_renditions) = tm_record_of(&encoded.base, &companion, &tm_envelope, &[10_000, 20_000, 40_000]);
let hexed = |r: &[Vec<u8>]| r.iter().map(|b| hex(b)).collect::<Vec<_>>();
let tm_text = crate::jcs::jcs(&tm_record).expect("a form");
let tm_output = |text: &str, envelope: &[u8], renditions: &[Vec<u8>]| json!({"tonemap": text, "envelope": hex(envelope), "renditions": hexed(renditions)});
let tm_proved = |x: &Value, objects: &[[u8; 32]], places: &[usize]| -> Value {
let mut x = x.clone();
x["proofs"] = Value::Array(places.iter().map(|&k| json!({"index": k, "siblings": crate::route_record::output_proof(objects, k).expect("a place").iter().map(|h| hex(h)).collect::<Vec<_>>()})).collect());
x
};
let tm_doc = |job: &str, objects: &[[u8; 32]], committed: bool| rr::job_document_of(job, TONEMAP, tm::CAPABILITY, "subpixels", &[("tm.alpha", "prov-tx")], accepted, &[rr::Hop { endpoint: "tm.alpha", error: None, start: 1_000, end: 4_000_000, receipt: rr::hop_receipt(TONEMAP, job, "tm.alpha", Some("2.5")) }], objects, committed);
let tm_objects: Vec<[u8; 32]> = tm_renditions.iter().map(|b| rr_hash(b)).collect();
let tm_given = tm_output(&tm_text, &tm_envelope, &tm_renditions);
pool.insert("job_tonemap", routed(tm_doc("job-7201", &tm_objects, true), vec![tm_proved(&tm_given, &tm_objects, &[0, 1, 2])]));
pool.insert("job_tonemap_under_a_list", routed(tm_doc("job-7202", &tm_objects, false), vec![tm_given.clone()]));
// A black scene renders black at every headroom: the record states two pictures, the hop one
// object, and both are billed.
let dark = tm::corpus::encode(&vec![[0i128; 3]; (tw * th) as usize], tw, th, 4, false, (1, 1), false);
let dark_companion = companion_of(&dark);
let dark_envelope = envelope_of(&dark.base, &dark_companion);
let (rep_record, rep_renditions) = tm_record_of(&dark.base, &dark_companion, &dark_envelope, &[10_000, 20_000]);
if rep_renditions[0] != rep_renditions[1] {
return Err("a black scene must render one picture at every headroom".into());
}
let rep_objects = [rr_hash(&rep_renditions[0])];
let rep_given = tm_output(&crate::jcs::jcs(&rep_record).expect("a form"), &dark_envelope, &rep_renditions);
pool.insert("job_tonemap_two_pictures_of_one_rendering", routed(tm_doc("job-7203", &rep_objects, true), vec![tm_proved(&rep_given, &rep_objects, &[0, 0])]));
let tm_one = |job: &str, x: &Value, objects: &[[u8; 32]], places: &[usize]| routed(tm_doc(job, objects, true), vec![tm_proved(x, objects, places)]);
let mut rendition_altered = tm_renditions.clone();
rendition_altered[1][0] ^= 1;
let altered_objects: Vec<[u8; 32]> = rendition_altered.iter().map(|b| rr_hash(b)).collect();
pool.insert("job_tonemap_a_rendition_altered", tm_one("job-7210", &tm_output(&tm_text, &tm_envelope, &rendition_altered), &altered_objects, &[0, 1, 2]));
pool.insert("job_tonemap_a_rendition_missing", tm_one("job-7211", &tm_output(&tm_text, &tm_envelope, &tm_renditions[..2]), &tm_objects[..2], &[0, 1]));
pool.insert("job_tonemap_another_envelope", tm_one("job-7212", &tm_output(&tm_text, &tm_other, &tm_renditions), &tm_objects, &[0, 1, 2]));
let mut swapped = tm_renditions.clone();
swapped.swap(0, 2);
pool.insert("job_tonemap_renditions_out_of_order", tm_one("job-7213", &tm_output(&tm_text, &tm_envelope, &swapped), &tm_objects, &[2, 1, 0]));
let mut unsigned_tm = tm_given.clone();
unsigned_tm["tonemap"] = json!(tm_text.replacen("2026-10-20", "2025-10-20", 1));
pool.insert("job_tonemap_a_record_that_does_not_verify", tm_one("job-7214", &unsigned_tm, &tm_objects, &[0, 1, 2]));
let tm_short = &tm_objects[..2];
pool.insert("job_tonemap_a_rendition_its_hop_did_not_deliver", routed(tm_doc("job-7215", tm_short, true), vec![tm_proved(&tm_given, tm_short, &[0, 1, 1])]));
pool.insert("job_tonemap_one_output_twice", routed(tm_doc("job-7216", &tm_objects, true), vec![tm_proved(&tm_given, &tm_objects, &[0, 1, 2]), tm_proved(&tm_given, &tm_objects, &[0, 1, 2])]));
let mut proofs_short = tm_proved(&tm_given, &tm_objects, &[0, 1, 2]);
proofs_short["proofs"].as_array_mut().expect("a list").pop();
pool.insert("job_tonemap_a_proof_short", routed(tm_doc("job-7217", &tm_objects, true), vec![proofs_short]));
pool.insert("job_tonemap_without_its_proofs", routed(tm_doc("job-7218", &tm_objects, true), vec![tm_given.clone()]));
pool.insert("job_tonemap_proofs_under_a_list", routed(tm_doc("job-7219", &tm_objects, false), vec![tm_proved(&tm_given, &tm_objects, &[0, 1, 2])]));
pool.insert("job_tonemap_in_an_encodes_form", routed(tm_doc("job-7220", &[rr_hash(&clip)], true), vec![proved(&enc_out, &[rr_hash(&clip)], 0)]));
let src = |names: &[&'static str]| -> Vec<Value> { names.iter().map(|n| pool[n].clone()).collect() };
let refs = |names: &[&'static str]| -> Vec<&Value> { names.iter().map(|n| &pool[n]).collect() };
let inv_6 = make_invoice(&seller, &t_6, None, MONTH.0, mid, &src(&day_6)).map_err(|e| format!("invoice 6: {e:?}"))?;
let a6 = inv_6.to_bytes();
let day_6m = ["job_upscale", "job_tonemap", "job_tonemap_two_pictures_of_one_rendering"];
let inv_6m = make_invoice(&seller, &t_6, None, MONTH.0, mid, &src(&day_6m)).map_err(|e| format!("invoice 6, the re-derived jobs: {e:?}"))?;
let am = inv_6m.to_bytes();
let inv_6_again = make_invoice(&seller, &t_6, Some(&inv_6m), mid, MONTH.1, &src(&["job_upscale_run_again"])).map_err(|e| format!("invoice 6, the upscale again: {e:?}"))?;
let inv_6_lists = make_invoice(&seller, &t_6, None, MONTH.0, mid, &src(&["job_upscale_under_a_list", "job_tonemap_under_a_list"])).map_err(|e| format!("invoice 6, under lists: {e:?}"))?;
let inv_6_as_5 = make_invoice(&seller, &t_6, None, MONTH.0, mid, &src(&day_r3)).map_err(|e| format!("invoice 6, version 5's jobs: {e:?}"))?;
let refused_6 = |name: &'static str, about: &'static str, source: &'static str| Vector { name, about, tariff: t6b.clone(), invoice: am.clone(), previous: None, sources: vec![source] };
let price_of = |endpoint: &str, usage: &str| u8::try_from(job_prices_6.iter().position(|p| p.endpoint == endpoint && p.usage == usage && p.unit == 4).expect("a price") + 1).expect("a unit");
let tariff_with = |f: &dyn Fn(&mut Vec<JobPrice>)| {
let mut job = job_prices_6.clone();
f(&mut job);
v6(VERSION_6, job).to_bytes()
};
let usage_line = |i: &mut Invoice, endpoint: &str, usage: &str| i.lines.iter().position(|l| l.source_kind == source_kind::JOB && l.unit == price_of(endpoint, usage)).expect("the usage line");
let t_5_usage = {
let mut job = job_prices_4.clone();
job.push(jp6(GEN, GENERATE, "", 4, "", 1, 1));
job.sort_by(|a, b| a.order().cmp(&b.order()));
v6(VERSION_5, job)
};
vectors.extend([
Vector { name: "version_6_a_third_partys_usage_per_endpoint", about: "version 6: a third party's jobs billed per job and by what its record states the provider bills, each endpoint at its own price: a served hop, another provider's, a failover whose failed hop's billed usage reaches no line, a job with no record, which bills by the job alone, a job in another unit at that unit's price, and one in a unit no price names", tariff: t6b.clone(), invoice: a6.clone(), previous: None, sources: day_6.to_vec() },
Vector { name: "version_6_holds_what_version_5_holds", about: "version 6: an encode, a decode held to its frames and a package's run, billed as version 5 bills them at prices of any endpoint", tariff: t6b.clone(), invoice: inv_6_as_5.to_bytes(), previous: None, sources: day_r3.to_vec() },
Vector { name: "version_6_an_upscale_and_a_tone_mapping", about: "version 6: an upscale of two pictures and two tone mappings, one of whose records states two pictures of one rendering, billed by the subpixels their records state, each re-derived from what it read", tariff: t6b.clone(), invoice: am.clone(), previous: None, sources: day_6m.to_vec() },
Vector { name: "version_6_an_upscale_run_again", about: "version 6: the next invoice of that chain bills the upscale run again, a new route record and a new hop, its records cited again", tariff: t6b.clone(), invoice: inv_6_again.to_bytes(), previous: Some(am.clone()), sources: vec!["job_upscale_run_again"] },
Vector { name: "version_6_re_derived_jobs_under_a_list", about: "version 6: an upscale and a tone mapping whose route records are of version 2, listing what each hop delivered, so that no output gives a proof", tariff: t6b.clone(), invoice: inv_6_lists.to_bytes(), previous: None, sources: vec!["job_upscale_under_a_list", "job_tonemap_under_a_list"] },
Vector { name: "version_6_prices_out_of_order_by_endpoint", about: "a tariff of version 6 pricing gen.beta before gen.alpha", tariff: tariff_with(&|j| {
let (a, b) = (j.iter().position(|p| p.endpoint == "gen.alpha" && p.usage == "seconds").expect("a price"), j.iter().position(|p| p.endpoint == "gen.beta" && p.unit == 4).expect("a price"));
j.swap(a, b);
}), invoice: a6.clone(), previous: None, sources: vec![] },
Vector { name: "version_6_a_price_of_any_endpoint_beside_one", about: "a tariff of version 6 pricing a usage at any endpoint and at gen.alpha, which would bill a job at both", tariff: tariff_with(&|j| {
j.push(jp6(GEN, GENERATE, "", 4, "seconds", 1, 1));
j.sort_by(|a, b| a.order().cmp(&b.order()));
}), invoice: a6.clone(), previous: None, sources: vec![] },
Vector { name: "version_6_a_usage_price_naming_no_unit", about: "a tariff of version 6 pricing a usage in no unit", tariff: tariff_with(&|j| j.iter_mut().find(|p| p.endpoint == "gen.beta" && p.unit == 4).expect("a price").usage = String::new()), invoice: a6.clone(), previous: None, sources: vec![] },
Vector { name: "version_6_a_jobs_price_naming_a_unit", about: "a tariff of version 6 whose price per job names a usage's unit", tariff: tariff_with(&|j| j.iter_mut().find(|p| p.capability == GEN && p.unit == 1).expect("a price").usage = "seconds".into()), invoice: a6.clone(), previous: None, sources: vec![] },
Vector { name: "version_6_an_endpoint_not_a_token", about: "a tariff of version 6 pricing an endpoint whose name holds a space", tariff: tariff_with(&|j| j.iter_mut().find(|p| p.endpoint == "gen.beta" && p.unit == 2).expect("a price").endpoint = "gen.b eta".into()), invoice: a6.clone(), previous: None, sources: vec![] },
Vector { name: "version_6_a_unit_past_usage", about: "a tariff of version 6 with a job price of unit 5", tariff: tariff_with(&|j| {
let p = j.iter_mut().find(|p| p.endpoint == "gen.beta" && p.unit == 4).expect("a price");
p.unit = 5;
p.usage = String::new();
}), invoice: a6.clone(), previous: None, sources: vec![] },
Vector { name: "version_5_a_price_of_usage", about: "a tariff of version 5 with a job price of unit 4, a usage, which only version 6 prices", tariff: t_5_usage.to_bytes(), invoice: a5.clone(), previous: None, sources: vec![] },
Vector { name: "version_5_invoice_under_a_version_6_tariff", about: "an invoice of version 5 naming a tariff of version 6", tariff: t6b.clone(), invoice: sealed_after(&t_6, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_r3), |i| i.version = VERSION_5).to_bytes(), previous: None, sources: day_r3.to_vec() },
Vector { name: "version_5_an_invoice_listing_a_third_partys_record", about: "an invoice of version 5 listing a record of kind 17, which only version 6 lists", tariff: t5b.clone(), invoice: sealed_after(&t_5, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_5), |i| {
i.records.push((record_kind::THIRD_PARTY, [1; 32]));
i.records.sort();
}).to_bytes(), previous: None, sources: day_5.to_vec() },
Vector { name: "version_5_a_job_given_a_third_partys_records", about: "version 5: a job given the third-party records only version 6 reads", tariff: t5b.clone(), invoice: a5.clone(), previous: None, sources: vec!["job_tp_alpha"] },
Vector { name: "version_5_an_upscale_in_version_6s_form", about: "version 5: an upscale given in the form only version 6 reads", tariff: t5b.clone(), invoice: a5.clone(), previous: None, sources: vec!["job_upscale"] },
Vector { name: "version_5_a_tone_mapping_in_version_6s_form", about: "version 5: a tone mapping given in the form only version 6 reads", tariff: t5b.clone(), invoice: a5.clone(), previous: None, sources: vec!["job_tonemap"] },
Vector { name: "version_6_another_jobs_record", about: "version 6: a job given the third-party record of another job", tariff: t6b.clone(), invoice: a6.clone(), previous: None, sources: vec!["job_tp_another_jobs_record"] },
Vector { name: "version_6_a_failed_hops_usage_billed", about: "version 6: gen.alpha's usage line adding what the provider billed for the failover's failed hop", tariff: t6b.clone(), invoice: sealed_after(&t_6, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_6), |i| {
let k = usage_line(i, "gen.alpha", "seconds");
let l = &mut i.lines[k];
i.amount -= l.amount;
l.quantity += 1;
l.amount = l.quantity * l.price;
i.amount += l.amount;
}).to_bytes(), previous: None, sources: day_6.to_vec() },
Vector { name: "version_6_usage_at_another_endpoints_price", about: "version 6: gen.beta's usage line priced at gen.alpha's price", tariff: t6b.clone(), invoice: sealed_after(&t_6, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_6), |i| {
let k = usage_line(i, "gen.beta", "seconds");
let l = &mut i.lines[k];
i.amount -= l.amount;
l.price = 40_000_000;
l.amount = l.quantity * l.price;
i.amount += l.amount;
}).to_bytes(), previous: None, sources: day_6.to_vec() },
Vector { name: "version_6_a_usage_line_not_citing_its_record", about: "version 6: gen.beta's usage line citing the route records and the hop receipts and not the third-party records", tariff: t6b.clone(), invoice: sealed_after(&t_6, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_6), |i| {
let k = usage_line(i, "gen.beta", "seconds");
let facts: Vec<Facts> = refs(&day_6).iter().map(|s| verify_source_of(s, &signers(&t_6), VERSION_6).expect("a corpus source verifies")).collect();
let computed = compute(&t_6, &facts).expect("figures that fit");
let cited: Cited = computed.lines.iter().find(|(l, _)| l.unit == i.lines[k].unit).expect("the line").1.iter().copied().filter(|(kind, _)| *kind != record_kind::THIRD_PARTY).collect();
i.lines[k].cited = cited.len() as u32;
i.lines[k].cites = cites_digest(&cited);
}).to_bytes(), previous: None, sources: day_6.to_vec() },
Vector { name: "version_6_a_record_left_out", about: "version 6: an invoice whose records leave out a third-party record it cites", tariff: t6b.clone(), invoice: sealed_after(&t_6, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_6), |i| {
let k = i.records.iter().position(|r| r.0 == record_kind::THIRD_PARTY).expect("a record");
i.records.remove(k);
}).to_bytes(), previous: None, sources: day_6.to_vec() },
Vector { name: "version_6_usage_no_record_states", about: "version 6: a usage line for a job given with no third-party record", tariff: t6b.clone(), invoice: sealed_after(&t_6, None, (MONTH.0, mid), MONTH.0, mid, &refs(&["job_tp_unrecorded"]), |i| {
let cited: Cited = [(record_kind::ROUTE_RECORD, [1; 32])].into_iter().collect();
i.lines.push(Line { source_kind: source_kind::JOB, source: router, unit: price_of("gen.alpha", "seconds"), quantity: 4, price: 40_000_000, amount: 160_000_000, cited: 1, cites: cites_digest(&cited) });
i.lines.sort_by_key(|l| (l.source_kind, l.source, l.unit));
i.amount += 160_000_000;
}).to_bytes(), previous: None, sources: vec!["job_tp_unrecorded"] },
Vector { name: "version_6_a_records_subpixels_above_it", about: "version 6: the upscale's subpixels line one above what its records state", tariff: t6b.clone(), invoice: sealed_after(&t_6, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_6m), |i| {
let up = u8::try_from(job_prices_6.iter().position(|p| p.capability == UP).expect("a price") + 1).expect("a unit");
let k = i.lines.iter().position(|l| l.source_kind == source_kind::JOB && l.unit == up).expect("the line");
i.lines[k].quantity += 1;
}).to_bytes(), previous: None, sources: day_6m.to_vec() },
refused_6("version_6_a_picture_altered", "version 6: an upscale whose picture has one sample changed, its record's output unchanged", "job_upscale_a_picture_altered"),
refused_6("version_6_a_picture_of_another_shape", "version 6: an upscale whose picture holds the samples in rows of another length", "job_upscale_a_picture_of_another_shape"),
refused_6("version_6_a_picture_that_does_not_read", "version 6: an upscale whose picture is not a PNG", "job_upscale_a_picture_that_does_not_read"),
refused_6("version_6_an_upscale_under_another_prior", "version 6: an upscale given a prior other than the one its record names, so nothing proves it", "job_upscale_another_prior"),
refused_6("version_6_an_upscale_of_another_input", "version 6: an upscale given another picture than the one its record names", "job_upscale_another_input"),
refused_6("version_6_an_input_that_does_not_read", "version 6: an upscale whose input is not an envelope", "job_upscale_an_input_that_does_not_read"),
refused_6("version_6_an_input_of_another_capability", "version 6: an upscale whose input's envelope is of a capability other than a PNG's", "job_upscale_an_input_of_another_capability"),
refused_6("version_6_an_input_not_rgb", "version 6: an upscale whose input is a greyscale PNG", "job_upscale_an_input_not_rgb"),
refused_6("version_6_a_record_of_another_output", "version 6: an upscale whose record, signed, states another derivation's digest", "job_upscale_a_record_of_another_output"),
refused_6("version_6_a_record_naming_a_residual", "version 6: an upscale whose record names a residual, which this form does not hold, so nothing proves it", "job_upscale_a_record_naming_a_residual"),
refused_6("version_6_an_upscale_record_that_does_not_verify", "version 6: an upscale whose record was changed after it was signed", "job_upscale_a_record_that_does_not_verify"),
refused_6("version_6_a_picture_its_hop_did_not_deliver", "version 6: an upscale whose picture is not what its route record states its hop delivered", "job_upscale_a_picture_its_hop_did_not_deliver"),
refused_6("version_6_an_upscale_output_twice", "version 6: an upscale giving one picture twice, to bill it twice", "job_upscale_one_output_twice"),
refused_6("version_6_an_upscale_in_an_encodes_form", "version 6: an upscale given as an encode gives its output", "job_upscale_in_an_encodes_form"),
refused_6("version_6_a_rendition_altered", "version 6: a tone mapping whose second rendition has one byte changed", "job_tonemap_a_rendition_altered"),
refused_6("version_6_a_rendition_missing", "version 6: a tone mapping that gives two renditions of the three pictures its record states", "job_tonemap_a_rendition_missing"),
refused_6("version_6_a_tone_mapping_of_another_envelope", "version 6: a tone mapping given another envelope than the one its record names, so nothing proves it", "job_tonemap_another_envelope"),
refused_6("version_6_renditions_out_of_order", "version 6: a tone mapping whose renditions are not in its record's order", "job_tonemap_renditions_out_of_order"),
refused_6("version_6_a_tonemap_record_that_does_not_verify", "version 6: a tone mapping whose record was changed after it was signed", "job_tonemap_a_record_that_does_not_verify"),
refused_6("version_6_a_rendition_its_hop_did_not_deliver", "version 6: a tone mapping whose third rendition is not what its route record states its hop delivered", "job_tonemap_a_rendition_its_hop_did_not_deliver"),
refused_6("version_6_a_tone_mapping_output_twice", "version 6: a tone mapping giving its record twice, to bill it twice", "job_tonemap_one_output_twice"),
refused_6("version_6_a_proof_short", "version 6: a tone mapping with one proof fewer than its renditions", "job_tonemap_a_proof_short"),
refused_6("version_6_a_tone_mapping_without_its_proofs", "version 6: a tone mapping whose route record is of version 3, given without its proofs", "job_tonemap_without_its_proofs"),
refused_6("version_6_proofs_under_a_list", "version 6: a tone mapping whose route record is of version 2, given proofs", "job_tonemap_proofs_under_a_list"),
refused_6("version_6_a_tone_mapping_in_an_encodes_form", "version 6: a tone mapping given as an encode gives its output", "job_tonemap_in_an_encodes_form"),
]);
let mut sources = Map::new();
for (n, v) in &pool {
sources.insert((*n).into(), v.clone());

View file

@ -237,11 +237,11 @@ fn unhex<const N: usize>(s: &str) -> Option<[u8; N]> {
Some(out)
}
fn token(s: &str) -> bool {
pub(crate) fn token(s: &str) -> bool {
(1..=128).contains(&s.len()) && s.bytes().all(|c| c.is_ascii_alphanumeric() || b"._-:/".contains(&c))
}
fn unit(s: &str) -> bool {
pub(crate) fn unit(s: &str) -> bool {
(1..=64).contains(&s.len())
&& s.as_bytes()[0].is_ascii_lowercase()
&& s.bytes().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_')

View file

@ -591,6 +591,15 @@ pub(crate) fn hop_receipt(operation: &str, job: &str, endpoint: &str, joules: Op
r
}
/// [`hop_receipt`] of a hop that served and reports no energy, as a third party's does.
#[cfg(feature = "receipt_ledger")]
pub(crate) fn unmetered_hop_receipt(operation: &str, job: &str, endpoint: &str) -> Value {
let mut r = receipt(operation, endpoint, 1, None);
r["subject"]["inputs"] = json!([sha(&format!("{job}/{endpoint}"))]);
reseal_receipt(&mut r);
r
}
/// The verification document (§7.1) of this corpus's request under `job` and `operation`,
/// routed by its router under its price book and policy, accepted at `accepted`, with
/// `attempts` in their order and their receipts given as they are.
@ -675,6 +684,33 @@ pub(crate) fn job_document_3(job: &str, operation: &str, accepted: u64, attempts
routed(request_2(job, operation, no_preference()), pricebook_2(), accepted, attempts, outputs, true)
}
/// The verification document of a job of `operation` under `capability`, one quantity of `unit`,
/// routed under a price book of version 2 of `endpoints`, each an (endpoint, provider) that prices
/// it per that unit and never failed in 1,000 hops; its record of version 3 when `committed`, else
/// of version 2. `attempts` and `outputs` as [`job_document_2`] takes them.
#[cfg(feature = "receipt_ledger")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn job_document_of(job: &str, operation: &str, capability: &str, unit: &str, endpoints: &[(&str, &str)], accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]], committed: bool) -> Value {
let mut request = request_2(job, operation, no_preference());
request["capability"] = json!(capability);
request["fallback"] = Value::Null;
request["quantities"] = json!([{"unit": unit, "quantity": 1}]);
let entries = endpoints.iter().map(|(e, p)| entry_2(e, operation, p, capability, EU_C, (unit, 1, 400_000), Some(500), Some((4_500_000, "OnChipCounter")), Some(950), failures(1_000, 0, 0, 0))).collect();
routed(request, pricebook_2_of(entries), accepted, attempts, outputs, committed)
}
/// The text of a third party's record of hop `hop` of `document`'s route record (§6.2), naming
/// `outputs`, and stating that the provider bills `usage` in the candidate's unit, or nothing.
#[cfg(feature = "receipt_ledger")]
pub(crate) fn third_party_text(document: &Value, hop: usize, outputs: &[[u8; 32]], usage: Option<u64>) -> String {
let record: Value = serde_json::from_str(document["record"].as_str().expect("a record's text")).expect("a record reads");
let mut body = third_party_body_of(&record, hop, outputs);
let unit = body["usage"]["unit"].clone();
let unit = if unit.is_null() { record["candidates"][hop - 1]["unit"].clone() } else { unit };
body["usage"] = usage.map_or(Value::Null, |q| json!({"unit": unit, "quantity": q, "source": "the response's usage"}));
canonical(&super::third_party::sign(body, &router()))
}
/// [`package_job_document`], its record of version 3.
#[cfg(feature = "receipt_ledger")]
pub(crate) fn package_job_document_3(job: &str, accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]]) -> Value {
@ -1141,8 +1177,12 @@ fn vectors_3() -> Vec<Vector> {
/// What a router states of attempt `hop` of a job, sent to a third party, before it is signed.
fn third_party_body(j: &Job, hop: usize, outputs: &[[u8; 32]]) -> Value {
let a = &j.record["attempts"][hop - 1];
let c = j.record["candidates"].as_array().unwrap().iter().find(|c| c["endpoint"] == a["endpoint"]).unwrap();
third_party_body_of(&j.record, hop, outputs)
}
fn third_party_body_of(record: &Value, hop: usize, outputs: &[[u8; 32]]) -> Value {
let a = &record["attempts"][hop - 1];
let c = record["candidates"].as_array().unwrap().iter().find(|c| c["endpoint"] == a["endpoint"]).unwrap();
let (start, end) = (a["start"].as_u64().unwrap(), a["end"].as_u64().unwrap());
let served = a["outcome"] == "served";
let origin = "https://api.provider.example";
@ -1158,7 +1198,7 @@ fn third_party_body(j: &Job, hop: usize, outputs: &[[u8; 32]]) -> Value {
}));
}
json!({
"job": j.record["job"], "hop": hop, "endpoint": a["endpoint"], "provider": c["provider"], "receipt": a["receipt"], "origin": origin,
"job": record["job"], "hop": hop, "endpoint": a["endpoint"], "provider": c["provider"], "receipt": a["receipt"], "origin": origin,
"account": "operator", "provider_job": null, "outcome": a["outcome"],
"error": if served { Value::Null } else { json!({"class": a["error"], "code": "503"}) },
"exchanges": exchanges,

View file

@ -274,6 +274,73 @@ fn version_4_bills_the_bytes_the_work_and_the_storage() {
}
/// §10 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
/// §4.9: a third party's usage per endpoint, and what re-derives an upscale and a tone mapping.
#[test]
fn version_6_bills_a_usage_per_endpoint_and_what_it_re_derives() {
use serde_json::Value;
let text = committed();
let doc: Value = serde_json::from_str(&text).unwrap();
let vector = |name: &str| doc["vectors"].as_array().unwrap().iter().find(|v| v["name"] == name).unwrap_or_else(|| panic!("no vector {name}"));
let line = |o: &Value, endpoint: &str, usage: &str| -> Value { o["lines"].as_array().unwrap().iter().find(|l| l["endpoint"] == endpoint && l["usage_unit"] == usage).cloned().unwrap_or(Value::Null) };
let o = &vector("version_6_a_third_partys_usage_per_endpoint")["outcome"];
assert_eq!(o["outcome"], "accept");
// Each endpoint at its own price. gen.alpha's usage is the served hop's alone: the failover's
// failed hop's billed second, and the job given no record, reach no line.
assert_eq!((&line(o, "gen.alpha", "seconds")["quantity"], &line(o, "gen.alpha", "seconds")["price"]), (&Value::from(8), &Value::from(40_000_000)));
assert_eq!((&line(o, "gen.beta", "seconds")["quantity"], &line(o, "gen.beta", "seconds")["price"]), (&Value::from(8), &Value::from(25_000_000)));
// Another unit at that unit's price, and a unit no price names, which bills by the job alone.
assert_eq!(line(o, "gen.alpha", "images")["quantity"], 2);
assert_eq!(o["lines"].as_array().unwrap().iter().filter(|l| l["unit"] == "usage").count(), 3);
let jobs = o["lines"].as_array().unwrap().iter().find(|l| l["unit"] == "jobs").unwrap();
assert_eq!((&jobs["quantity"], &jobs["endpoint"]), (&Value::from(6), &Value::from("")));
// A usage line cites its record; a line per job does not need to.
let cites_kind = |l: &Value, k: u64| l["cites"].as_array().unwrap().iter().filter(|c| c[0] == k).count();
assert_eq!(cites_kind(&line(o, "gen.beta", "seconds"), 17), 2);
assert_eq!(cites_kind(jobs, 17), 0);
let o = &vector("version_6_an_upscale_and_a_tone_mapping")["outcome"];
assert_eq!(o["outcome"], "accept");
let by_capability = |c: &str| o["lines"].as_array().unwrap().iter().find(|l| l["capability"] == c).unwrap().clone();
// 12×8 and 8×6 pictures, three samples a pixel; three pictures and two pictures of 8×4.
assert_eq!(by_capability("wai.media.upscale")["quantity"], (12 * 8 + 8 * 6) * 3);
assert_eq!(by_capability("wai.media.tonemap")["quantity"], 5 * 8 * 4 * 3);
assert_eq!(cites_kind(&by_capability("wai.media.upscale"), 18), 2);
assert_eq!(cites_kind(&by_capability("wai.media.tonemap"), 19), 2);
// The chain cites an upscale's records again for a new hop's work.
assert_eq!(vector("version_6_an_upscale_run_again")["outcome"]["outcome"], "accept");
for (name, reason) in [
("version_6_a_price_of_any_endpoint_beside_one", "tariff_invalid"),
("version_6_prices_out_of_order_by_endpoint", "tariff_invalid"),
("version_6_a_usage_price_naming_no_unit", "tariff_invalid"),
("version_6_a_jobs_price_naming_a_unit", "tariff_invalid"),
("version_5_a_price_of_usage", "tariff_invalid"),
("version_5_an_invoice_listing_a_third_partys_record", "invoice_invalid"),
("version_5_invoice_under_a_version_6_tariff", "tariff_mismatch"),
("version_5_a_job_given_a_third_partys_records", "malformed"),
("version_6_another_jobs_record", "third_party_mismatch"),
("version_6_a_failed_hops_usage_billed", "quantity_not_recorded"),
("version_6_usage_no_record_states", "quantity_not_recorded"),
("version_6_usage_at_another_endpoints_price", "invoice_mismatch"),
("version_6_a_record_left_out", "invoice_mismatch"),
("version_6_a_usage_line_not_citing_its_record", "citation_mismatch"),
("version_6_a_picture_altered", "output_invalid"),
("version_6_an_upscale_under_another_prior", "output_invalid"),
("version_6_a_record_of_another_output", "output_invalid"),
("version_6_a_picture_its_hop_did_not_deliver", "output_not_delivered"),
("version_6_an_upscale_output_twice", "output_not_delivered"),
("version_6_a_rendition_altered", "output_invalid"),
("version_6_a_tone_mapping_of_another_envelope", "output_invalid"),
("version_6_a_rendition_its_hop_did_not_deliver", "output_not_delivered"),
("version_6_a_tone_mapping_output_twice", "output_not_delivered"),
("version_6_a_tone_mapping_without_its_proofs", "malformed"),
] {
assert_eq!(vector(name)["outcome"]["reason"], reason, "{name}");
}
}
#[test]
fn the_extension_lists_the_reasons() {
let text = read("extensions/receipt-ledger.md");