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 #591
12 changed files with 5707 additions and 1835 deletions
|
|
@ -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 probe no longer panics on two adjacent start codes
|
||||
|
||||
- `wai.video.avs3` and `wai.audio.avs3`: a payload with a start code prefix directly after another (`00 00 01 00 00 01`)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, ¬_png), ¬_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, ¬_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());
|
||||
|
|
|
|||
|
|
@ -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'_')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue