route-record: version 2 prices per operation, ranks by measured failure, binds a hop receipt to its output #552
12 changed files with 2258 additions and 133 deletions
22
.github/workflows/wai-conformance.yml
vendored
22
.github/workflows/wai-conformance.yml
vendored
|
|
@ -2127,18 +2127,20 @@ jobs:
|
|||
python3 -I live-conformance/fleet_verify.py live-conformance/fleet
|
||||
# wai.route.record (extensions/route-record.md): the reference module's unit tests (a
|
||||
# document read strictly and held to the subset, §2's encodings, an estimate past the subset
|
||||
# unknown, a null ranked last, §8 listing exactly the reference's codes) and
|
||||
# unknown, a null ranked last, the expected figures per successful job and when they are
|
||||
# unknown, the outputs' commitment, §8 listing exactly the reference's codes) and
|
||||
# tests/route_record.rs (vectors.json regenerates byte for byte from its seeds and no other
|
||||
# file is there; wai-rs gives every vector its stated outcome; every reason of §8 and every
|
||||
# reused code refuses a vector; the accepted vectors are §10's, every limit removing a
|
||||
# candidate; a record is bound to each of its documents; `wai route verify` prints the
|
||||
# outcome and exits by it); then the corpus's second implementation, route_record_verify.py,
|
||||
# written from the extension's text and sharing no code with wai-rs, which checks the
|
||||
# router's signature with WAI's strict Ed25519 rule and the hop receipts with the OER/2
|
||||
# reference verifier (the `cryptography` package, installed above), recomputes every
|
||||
# candidate, removal, rank, order, decision and attempt, requires every mutated copy of the
|
||||
# corpus to fail, and drops each of its own checks in turn and requires the corpus to catch
|
||||
# the weakened verifier; run isolated (-I).
|
||||
# reused code refuses a vector; the accepted vectors are §10's, of version 1 and of version 2,
|
||||
# every limit removing a candidate; a record is bound to each of its documents; `wai route
|
||||
# verify` prints the outcome and exits by it); then the corpus's second implementation,
|
||||
# route_record_verify.py, written from the extension's text and sharing no code with wai-rs,
|
||||
# which checks the router's signature with WAI's strict Ed25519 rule and the hop receipts
|
||||
# with the OER/2 reference verifier (the `cryptography` package, installed above), computes
|
||||
# the outputs' commitment with claim-conformance/_blake3.py, recomputes every candidate,
|
||||
# removal, expected figure, rank, order, decision and attempt, requires every mutated copy
|
||||
# of the corpus to fail, and drops each of its own checks in turn and requires the corpus to
|
||||
# catch the weakened verifier; run isolated (-I).
|
||||
- name: route record · wai-rs and route_record_verify.py
|
||||
working-directory: wai
|
||||
shell: bash
|
||||
|
|
|
|||
|
|
@ -56,6 +56,57 @@ wire format may change and such changes are recorded here.
|
|||
invoice of version 3, since version 2 is now defined, and is refused `unknown_version` as before; every other
|
||||
vector keeps its outcome, and the version-2 vectors are added.
|
||||
|
||||
### The route record prices per operation, ranks by measured failure, and binds a hop to its output
|
||||
|
||||
- **Version 2 of the route record's price book and record** ([extensions/route-record.md](extensions/route-record.md)
|
||||
§3.4): a price book that states `version` 2 prices each **operation** of an endpoint in its own entry, ordered by
|
||||
endpoint and then operation, and a job's candidates are the entries of its own operation. Within one capability an
|
||||
encode and a decode spend different energy and are priced differently, so one entry per endpoint misstated one of
|
||||
them to the estimates, the unit-price limit and the ranking. A record's version is its price book's
|
||||
(`pricebook_mismatch` at `record`, `version` otherwise). Documents and records of version 1 read, route and verify
|
||||
exactly as before, and a reader of version 1 refuses version 2 rather than misreading it.
|
||||
- **Measured failure counts in the ranking** (§3.2.1, §4.2, §4.4): an entry of version 2 states its `failures`, the
|
||||
hops of its operation sent in a period, how many failed, what the provider billed them per unit and how long they
|
||||
took, or `null`. A job sent to an endpoint until it is served meets `failed / (hops − failed)` failures on average,
|
||||
each billed to the router and costing the job its time, so a candidate carries its **expected cost** and **expected
|
||||
time to first byte per successful job**, which the `price`, `ttfb` and `targets` keys read before the list figures:
|
||||
ordering by them minimises the expected spend over the attempts (an exchange argument, §4.4). A basis under 100 hops
|
||||
is too thin (the 95% interval of a rate near one half is wider than ±0.1 below 97), and thin or `null` failures are
|
||||
unknown: the expected figures are `null` and rank after every measured one, by the existing rule for `null`, never
|
||||
as no failures. The record restates every basis and expected figure. The job never pays for failures: the
|
||||
unit-price and spend limits read the list price and cost, and a line bills the billed attempt. This is the failure
|
||||
cost waiTransport proposed, with its physics and its basis, except that an unknown failure is not counted as zero.
|
||||
- **A hop receipt is bound to its output** (§5.2, §6): in version 2 the attempt that served states `outputs`, the
|
||||
content hashes (jwp-receipts §2.1) of what it delivered in order, and its OER/2 receipt's `subject.output` must be
|
||||
their commitment, or the record is refused **`receipt_output_mismatch`**: a receipt cannot be moved to vouch for an
|
||||
output it did not produce. [extensions/oer-receipts.md](extensions/oer-receipts.md) §3 now defines a `job`
|
||||
receipt's `subject.output`: one object's content hash, or for several the jwp-receipts §2.2 Merkle root over their
|
||||
content hashes in order, unlabelled leaves, the objects distinct; jwp-receipts §2.4.5 points to it. No OER/2
|
||||
reason or vector changes.
|
||||
- **The corpus** (`live-conformance/routes/vectors.json`) keeps every vector of version 1 byte for byte but one, and
|
||||
adds 36 of version 2: ten accepted (the expected-cost ranking with a thin, a failed and an unmeasured basis, a decode
|
||||
at its own price, the expected time to first byte as sort and as target, the ceiling on the list cost, a book with
|
||||
no failure measured, a failover, three outputs, a late hop) and a control for each new rule. `route_record_verify.py`
|
||||
reads version 2 from the text, computes the commitment with `claim-conformance/_blake3.py`, requires the new mutated
|
||||
copies to fail and drops eleven more of its checks. `wai::route_record::seal_delivered` writes a record of its
|
||||
price book's version; `seal` is unchanged.
|
||||
- **Breaking:** `record:corpus:wai/live-conformance/routes/vectors.json`: the control `record_version_2` is now
|
||||
`record_version_3`, a record of version 3, since version 2 is defined, and is refused `unknown_version` as before;
|
||||
every other vector of version 1 is unchanged, and the version-2 vectors are added after them.
|
||||
- **Breaking:** `api:rust:wai::route_record::Entry::operation`: the operation an entry prices, `None` in version 1. A
|
||||
struct literal adds `operation: None` and `failures: None`.
|
||||
- **Breaking:** `api:rust:wai::route_record::Entry::failures`: the entry's measured failures (version 2), or `None`.
|
||||
- **Breaking:** `api:rust:wai::route_record::PriceBook::version`: a price book states its version, 1 or 2.
|
||||
- **Breaking:** `api:rust:wai::route_record::Candidate::expected_cost`: the expected cost per successful job
|
||||
(version 2), `None` in version 1. A struct literal adds `expected_cost: None, expected_ttfb_ms: None`.
|
||||
- **Breaking:** `api:rust:wai::route_record::Candidate::expected_ttfb_ms`: the expected time to first byte per
|
||||
successful job (version 2), `None` in version 1.
|
||||
- **Breaking:** `api:rust:wai::route_record::Accepted::outputs`: the served hop's outputs in a record of version 2,
|
||||
`None` in version 1.
|
||||
- **Breaking:** `api:rust:wai::route_record::Reason::ReceiptOutputMismatch`: a served hop's receipt that does not name
|
||||
the outputs its attempt states (§6); a match over the reasons adds it.
|
||||
- **Breaking:** `api:rust:wai::route_record::Reason::ALL`: the codes of §8, `receipt_output_mismatch` among them.
|
||||
|
||||
### Routed media jobs: the route record, OER/2 minor version 1, and the operations a job names
|
||||
|
||||
- **`wai.route.record`** ([extensions/route-record.md](extensions/route-record.md)): what a router considered for
|
||||
|
|
|
|||
21
wai/SPEC.md
21
wai/SPEC.md
|
|
@ -1582,15 +1582,18 @@ And for **a routed media job**, what a router considered and why it chose —
|
|||
the quantities it expects, hard limits (regions and data residency, no training on its inputs,
|
||||
zero retention, generally available endpoints only, consent on file, a price per unit and a
|
||||
spend ceiling) and soft preferences (a time to first byte, a quality floor, a sort by price,
|
||||
speed, energy or quality); a price book states what each endpoint offers and costs, and a
|
||||
named, versioned routing policy orders the survivors and bounds the attempts, the three pinned
|
||||
by SHA-256 over their RFC 8785 forms. From those three documents alone come the candidates,
|
||||
every limit that removes one (a value the price book does not know counted as the most
|
||||
restrictive), the ranking, the order and the first key on which the first two survivors
|
||||
differ; the router signs a route record of them and of every attempt, each with its hop's
|
||||
OER/2 job receipt and whether it was billed. The attempts follow the order and only the last
|
||||
may serve; a failed hop, and a hop that served after the deadline the job declared, is not
|
||||
billed. A verifier recomputes the record from the three documents, holds each attempt to its
|
||||
speed, energy or quality); a price book states what each endpoint offers and costs for each
|
||||
operation, and, in its version 2, how often its hops failed, what their failures were billed
|
||||
and how long they took, over a stated basis; and a named, versioned routing policy orders the
|
||||
survivors and bounds the attempts, the three pinned by SHA-256 over their RFC 8785 forms. From
|
||||
those three documents alone come the candidates, every limit that removes one (a value the
|
||||
price book does not know counted as the most restrictive), the ranking (by what one served job
|
||||
is expected to cost and take, failures included, the job paying the list price), the order and
|
||||
the first key on which the first two survivors differ; the router signs a route record of them
|
||||
and of every attempt, each with its hop's OER/2 job receipt and whether it was billed, the
|
||||
receipt of the hop that served naming the outputs it delivered. The attempts follow the order
|
||||
and only the last may serve; a failed hop, and a hop that served after the deadline the job
|
||||
declared, is not billed. A verifier recomputes the record from the three documents, holds each attempt to its
|
||||
receipt, and refuses a record whose choice they do not give. No transport and no payment:
|
||||
[`extensions/route-record.md`](extensions/route-record.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -547,9 +547,11 @@ verifier holding only such a text holds none.
|
|||
reason (`energy_oer_invalid`);
|
||||
3. that it is about the claim's subject — its `subject.output` equals
|
||||
the claim's `subject` exactly as the claim writes it, 64 lowercase
|
||||
hex digits (`context_mismatch`). oer-receipts does not fix what
|
||||
`subject.output` holds; an OER receipt an `energy` claim can name
|
||||
holds there the object's §2.1 content hash in this form. Without
|
||||
hex digits (`context_mismatch`). oer-receipts §3 *Output* fixes
|
||||
what a `job` receipt's `subject.output` holds: the commitment to
|
||||
what it delivered, which for one object is that object's §2.1
|
||||
content hash in this form; an OER receipt an `energy` claim can name
|
||||
is about one object and holds its content hash there. Without
|
||||
this check a claim about one object could name a valid OER receipt
|
||||
about another that happens to carry the same figure and grade;
|
||||
4. that its `provenance.how` equals `grade`, and that its
|
||||
|
|
|
|||
|
|
@ -158,6 +158,24 @@ not WAI payload capabilities, which name formats a sink reconstructs
|
|||
([`SPEC.md`](../SPEC.md) §5), and a receipt **MUST NOT** name them in the `wai.`
|
||||
namespace.
|
||||
|
||||
**Output.** A `job` receipt names the objects its unit of work delivered in `subject.output`,
|
||||
by their **commitment**, written as 64 lowercase hexadecimal digits:
|
||||
|
||||
- one object, by its content hash ([jwp-receipts](jwp-receipts.md) §2.1), the form an
|
||||
`energy` claim holds an OER receipt to (§2, jwp-receipts §2.4.5);
|
||||
- several, delivered in an order, by the Merkle root jwp-receipts §2.2 builds over their
|
||||
content hashes in that order, every leaf of the first, unlabelled form: the root a group of
|
||||
exactly those objects is signed over when none carries a class label. The objects are
|
||||
distinct: the tree pads an odd level with a copy of its last node, so a list that repeats an
|
||||
object could share a root with a shorter one, and distinct objects give each list its own
|
||||
root.
|
||||
|
||||
The root of one object is not its content hash, and a commitment of several never names one
|
||||
of them alone: one value names one list. No reason of §7 checks the commitment, since a
|
||||
receipt on its own does not show what it delivered; a record that states what a job delivered
|
||||
holds its hop's receipt to it, as a route record of version 2 does
|
||||
([route-record](route-record.md) §6).
|
||||
|
||||
**Status, escalation and abstention.** `work.status` is `1` for a unit of work that
|
||||
completed and `-1` for one that was wasted; a bundle's `efficiency` counts them as
|
||||
`completed_units` and `wasted_units`. Two outcomes are easy to confuse:
|
||||
|
|
|
|||
|
|
@ -19,22 +19,32 @@
|
|||
> is `live-conformance/route_record_verify.py`.
|
||||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||||
>
|
||||
> This revision adds **version 2** of the price book and of the record (§3.4): an entry
|
||||
> prices one operation of an endpoint, so an encode and a decode are estimated, capped and
|
||||
> ranked at their own prices; it states the endpoint's measured failures with their basis, and
|
||||
> the ranking reads the expected cost and the expected time to first byte per successful job
|
||||
> they give (§4.2, §4.4), while the job pays the list price; and the attempt that served states
|
||||
> its outputs, to which its hop's receipt is held (§6). Documents and records of version 1 are
|
||||
> read and verified exactly as before.
|
||||
>
|
||||
> Texts read for this revision: the WAI texts this one cites; RFC 8259; RFC 8785; RFC 8032;
|
||||
> ISO 4217 (the alphabetic currency code); FIPS 180-4.
|
||||
> ISO 4217 (the alphabetic currency code); FIPS 180-4; the BLAKE3 specification.
|
||||
|
||||
## 1. Scope
|
||||
|
||||
In scope: the three documents a routing decision rests on, the request, the price book and
|
||||
the policy (§3); how a router computes its candidates, their removals, their ranking and
|
||||
its decision from them (§4); the record, its attempts and its signature (§5); how an
|
||||
attempt is held to its hop's OER/2 receipt (§6); verification (§7); and the reference
|
||||
implementation and corpus (§10).
|
||||
attempt is held to its hop's OER/2 receipt, and in version 2 to the outputs it delivered
|
||||
(§6); verification (§7); and the reference implementation and corpus (§10).
|
||||
|
||||
Out of scope: a transport; how a router learns its prices, which the price book states and
|
||||
this extension takes as stated; accounts, credit and payment; the job's output and its own
|
||||
receipts, which their extensions define; and the energy of a hop, which its OER/2 receipt
|
||||
states ([oer-receipts](oer-receipts.md)). An invoice that bills a job from its route record
|
||||
is the receipt ledger's ([receipt-ledger](receipt-ledger.md) §5).
|
||||
Out of scope: a transport; how a router learns its prices and measures its endpoints'
|
||||
failures, which the price book states and this extension takes as stated; accounts, credit
|
||||
and payment; the contents of the job's output and its own receipts, which their extensions
|
||||
define (a record of version 2 names the output by its content hash and nothing more); and
|
||||
the energy of a hop, which its OER/2 receipt states ([oer-receipts](oer-receipts.md)). An
|
||||
invoice that bills a job from its route record is the receipt ledger's
|
||||
([receipt-ledger](receipt-ledger.md) §5).
|
||||
|
||||
Relationship to the core spec: `wai.route.record` is a **record** ([SPEC](../SPEC.md) §5): a
|
||||
signed record a WAI tool emits and verifies. It names no payload format and is not
|
||||
|
|
@ -69,6 +79,11 @@ dispatchable.
|
|||
words joined by `.`, each a lowercase ASCII letter and then lowercase letters and digits
|
||||
(`media.encode`).
|
||||
- **Key** and **signature**: 64 and 128 lowercase hexadecimal digits.
|
||||
- **Content hash**: an object's content hash as [jwp-receipts](jwp-receipts.md) §2.1 computes
|
||||
it, BLAKE3-256 over the domain `moq-jwp:object\x01` and the object's bytes (a WAI
|
||||
envelope's whole bytes, for an envelope), written as 64 lowercase hexadecimal digits.
|
||||
- **Basis**: the hops a measured figure was read from and the period they were sent in
|
||||
(§3.2.1).
|
||||
- **Class**: an acquisition class of [energy-measurement](energy-measurement.md) §2, by its
|
||||
label: `CalibratedInstrument`, `OnChipCounter`, `ModelBased` or `Estimator`.
|
||||
- ⌊x⌋ is x rounded down; products are computed in integers wide enough not to overflow.
|
||||
|
|
@ -110,20 +125,24 @@ What the job asked for, as the router received it.
|
|||
|
||||
### 3.2 The price book
|
||||
|
||||
What each endpoint offers and costs, as the router read it.
|
||||
What each endpoint offers and costs, as the router read it. A price book is of version 1 or
|
||||
version 2 (§3.4); one of version 1 has no `version` member and its entries neither
|
||||
`operation` nor `failures`.
|
||||
|
||||
| member | form |
|
||||
|---|---|
|
||||
| `kind` | `"wai-route-pricebook"` |
|
||||
| `version` | version 2: `2`; a price book of version 1 has no such member |
|
||||
| `currency` | three capitals `A`–`Z` |
|
||||
| `issued` | a time: when the prices were stated |
|
||||
| `entries` | 1 to 1024 objects of exactly the members below, in strictly ascending order of `endpoint` |
|
||||
| `entries` | 1 to 1024 objects of exactly the members below, in strictly ascending order of `endpoint` in version 1, and of `endpoint` and then `operation` in version 2 |
|
||||
|
||||
An entry:
|
||||
|
||||
| member | form |
|
||||
|---|---|
|
||||
| `endpoint` | a token: the endpoint's id |
|
||||
| `operation` | version 2: an operation: the one this entry prices |
|
||||
| `provider` | a token: who runs it |
|
||||
| `capability` | a capability: what it produces |
|
||||
| `region`, `residency` | a token, or `null` when not known |
|
||||
|
|
@ -138,9 +157,44 @@ An entry:
|
|||
| `energy_uj` | an integer, or `null` when not stated: the microjoules it states per `divisor` units |
|
||||
| `energy_class` | the class of that figure, or `null` exactly when `energy_uj` is `null` |
|
||||
| `quality` | an integer 0 to 1000, or `null` when not known |
|
||||
| `failures` | version 2: the failures measured at this endpoint and operation (§3.2.1), or `null` when none were measured |
|
||||
|
||||
A price book states each figure as its endpoint's provider gave it; a value that is not known
|
||||
is `null`, and §4.3 reads every `null` limit as the most restrictive value.
|
||||
A price book states each figure as its endpoint's provider gave it, and its failures as the
|
||||
router measured them; a value that is not known is `null`, and §4.3 reads every `null` limit
|
||||
as the most restrictive value.
|
||||
|
||||
In version 2 an entry prices one operation of one endpoint, and an endpoint has an entry for
|
||||
each operation it serves, each with its own price, figures and failures. Within one
|
||||
capability an encode (a motion search, then a codestream written) and a decode (a
|
||||
reconstruction) spend different energy and are priced differently: one entry for both would
|
||||
misstate one of them to the estimates (§4.2), to the unit-price limit (§4.3) and to the
|
||||
ranking (§4.4).
|
||||
|
||||
#### 3.2.1 Failures (version 2)
|
||||
|
||||
`failures` states how often the endpoint's hops of the entry's operation failed, what the
|
||||
provider billed for those failures, and how long they took, with the basis they were read
|
||||
from. It is an object of exactly:
|
||||
|
||||
| member | form |
|
||||
|---|---|
|
||||
| `hops` | an integer at least 1: the hops of the entry's operation the router sent the endpoint in the period |
|
||||
| `from`, `to` | times, `from` at most `to` and `to` at most the price book's `issued`: the period those hops were sent in |
|
||||
| `failed` | an integer at most `hops`: the hops among them that failed (§5.2) |
|
||||
| `billed_price` | an integer: what the provider billed the failed hops, per `divisor` units of the entry's `unit`: the sum it billed them, times `divisor`, over the sum of their quantities in that unit, rounded down; `0` when it billed none of them |
|
||||
| `failed_ms` | an integer: the time a failed hop took from being sent to its end, when the next survivor could run, on average over the failed hops, in milliseconds, rounded down |
|
||||
|
||||
`hops`, `from` and `to` are the **basis** of the three figures: the failure rate is `failed`
|
||||
over `hops`, and `billed_price` and `failed_ms` are read from the failed hops of the basis.
|
||||
When `failed` is 0 there is no failed hop to read them from, and both are `0`: nothing was
|
||||
billed and no time was spent. A `failures` object that breaks a rule of this table is
|
||||
`pricebook_invalid`.
|
||||
|
||||
**A thin basis.** A basis of fewer than **100 hops** is too thin to mean anything. A failure
|
||||
rate read from *n* hops has a 95% confidence interval of half-width up to 1.96 × ½ / √*n* (its
|
||||
widest, at a rate of one half): ±0.098 at 100 hops, and wider than ±0.1 below 97. A rate
|
||||
known no better than that cannot weigh one endpoint against another, and the rule rounds the
|
||||
bound to 100. Failures of a thin basis, and `null` failures, are **unknown** (§4.2).
|
||||
|
||||
### 3.3 The policy
|
||||
|
||||
|
|
@ -158,6 +212,19 @@ How survivors are ranked and how many are tried.
|
|||
A policy is named and versioned so that a deployment pins it: the record states its name,
|
||||
version and digest, and a policy whose rules change takes a new version.
|
||||
|
||||
### 3.4 Versions
|
||||
|
||||
The request and the policy have one form. The price book and the record have two: version 1,
|
||||
this extension's first, and version 2, which adds the members that price an operation, state
|
||||
an endpoint's failures and name a hop's outputs. A record's version is its price book's
|
||||
(§7.2 step 5).
|
||||
|
||||
A reader of version 1 reads every document of version 1 as before, and refuses one of
|
||||
version 2 rather than misreading it: a price book with a `version` member, or an entry with
|
||||
`operation` or `failures`, is not of version 1's members (`pricebook_invalid`), and a record
|
||||
of version 2 is `unknown_version`. A price book whose `version` is other than 2 is
|
||||
`pricebook_invalid`; a record whose `version` is other than 1 and 2 is `unknown_version`.
|
||||
|
||||
## 4. Routing
|
||||
|
||||
A router computes the following from the request, the price book and the policy, and a
|
||||
|
|
@ -170,7 +237,9 @@ The **candidates** are the price book's entries whose `capability` is the reques
|
|||
`capability`, which are **direct** (`via_fallback` false), and, when the request names a
|
||||
`fallback`, those whose `capability` is the fallback (`via_fallback` true), in the price
|
||||
book's order. An entry of any other capability is not a candidate, and the record does not
|
||||
list it.
|
||||
list it. In a price book of version 2 a candidate's entry is also of the request's
|
||||
`operation`: an entry of another operation is not a candidate either, and since an endpoint
|
||||
has at most one entry of an operation, an endpoint is at most one candidate.
|
||||
|
||||
### 4.2 Estimates
|
||||
|
||||
|
|
@ -185,6 +254,36 @@ state. Units are never converted: a candidate priced in `seconds` is estimated f
|
|||
request's `seconds` and from nothing else. An estimate is the router's, from the request's
|
||||
own quantities; what a job costs is what its invoice bills from its receipts.
|
||||
|
||||
In a price book of version 2 a candidate also has two **expected** figures, per successful
|
||||
job, which count its failures. An attempt at the endpoint fails with probability
|
||||
*f* = `failed` / `hops` and serves with *s* = 1 − *f*, attempts taken as independent; a job
|
||||
sent there until it is served meets *f* / *s* = `failed` / (`hops` − `failed`) failures on
|
||||
average before it is (a geometric count). Each failure costs the router what the provider
|
||||
bills for it, and costs the job the time it took before the next survivor ran. So, per
|
||||
successful job:
|
||||
|
||||
- its **expected cost** is `cost` + ⌊*F* × `failed` / (`hops` − `failed`)⌋, where
|
||||
*F* = ⌊`q` × `billed_price` / `divisor`⌋ is what the provider bills one failed hop of this
|
||||
job;
|
||||
- its **expected time to first byte** is `ttfb_ms` + ⌊`failed_ms` × `failed` / (`hops` −
|
||||
`failed`)⌋, in milliseconds.
|
||||
|
||||
An expected figure is `null` when the candidate's failures are unknown (§3.2.1), when the
|
||||
figure it adds to (`cost`, `ttfb_ms`) is `null`, when every hop of the basis failed (there is
|
||||
no finite expectation), and when *F* or the figure exceeds 2⁵³ − 1, as an estimate is.
|
||||
|
||||
**Unknown failures** are not read as no failures, which would rank an endpoint nobody
|
||||
measured ahead of one measured to fail rarely, nor as a worst case, which this text states
|
||||
nowhere: the expected figures are `null`, and the rules this text gives every `null` figure
|
||||
apply to them unchanged. §4.4 ranks a `null` after every stated figure and then, within the
|
||||
same key, by the list figure, as version 1 ranks every candidate; and since no limit reads
|
||||
failures, unknown failures remove nothing.
|
||||
|
||||
**The job pays the list price.** The expected cost is the router's figure for ranking, of
|
||||
what failures cost it; the job is never charged it. The limits of §4.3 read the list `price`
|
||||
and the `cost`, what the job pays, and an invoice bills a job from its billed attempt alone
|
||||
(§5.2, receipt-ledger §5), so no failure reaches the job's line.
|
||||
|
||||
### 4.3 Hard limits
|
||||
|
||||
Each candidate is held to every limit in this order, and its **removals** list each limit
|
||||
|
|
@ -207,7 +306,9 @@ price book does not know it):
|
|||
So a value the price book does not know counts as the most restrictive one: an endpoint that
|
||||
does not say it never trains on its inputs is removed by `no_training`, one with no stated
|
||||
retention by `zero_retention`, and a cost that cannot be estimated, or a ceiling in another
|
||||
currency, by `spend`. A candidate with no removal is a **survivor**.
|
||||
currency, by `spend`. A candidate with no removal is a **survivor**. In version 2 the limits
|
||||
read the candidate's own entry, its operation's, and `unit_price` and `spend` read the list
|
||||
`price` and the `cost`, never the expected cost (§4.2).
|
||||
|
||||
### 4.4 Ranking
|
||||
|
||||
|
|
@ -223,20 +324,32 @@ The survivors are ordered by a list of keys, the **key list**:
|
|||
Two survivors compare by the first key of the list on which they differ, and the one that
|
||||
comes first under that key ranks first:
|
||||
|
||||
| key | first |
|
||||
|---|---|
|
||||
| `targets` | the one that meets the targets |
|
||||
| `price` | the smaller cost |
|
||||
| `ttfb` | the smaller `ttfb_ms` |
|
||||
| `energy` | the smaller energy |
|
||||
| `quality` | the larger `quality` |
|
||||
| `endpoint` | the smaller `endpoint`, by its bytes |
|
||||
| key | first | in version 2 |
|
||||
|---|---|---|
|
||||
| `targets` | the one that meets the targets | the same, the time to first byte read being the expected one |
|
||||
| `price` | the smaller cost | the smaller expected cost, then the smaller cost |
|
||||
| `ttfb` | the smaller `ttfb_ms` | the smaller expected time to first byte, then the smaller `ttfb_ms` |
|
||||
| `energy` | the smaller energy | the same |
|
||||
| `quality` | the larger `quality` | the same |
|
||||
| `endpoint` | the smaller `endpoint`, by its bytes | the same |
|
||||
|
||||
A `null` figure comes after every stated one, and two `null`s do not differ. Endpoints are
|
||||
distinct, so the order is total. The **order** is the survivors' endpoints in it, and each
|
||||
survivor's **rank** is its position there, from 1. The energy compared is each figure as its
|
||||
provider states it, whatever its class: the record states every class beside its figure, so
|
||||
a reader sees what was compared.
|
||||
A `null` figure comes after every stated one, and two `null`s do not differ; a key of two
|
||||
figures compares the second only when the first does not differ. Endpoints are distinct, so
|
||||
the order is total. The **order** is the survivors' endpoints in it, and each survivor's
|
||||
**rank** is its position there, from 1. The energy compared is each figure as its provider
|
||||
states it, whatever its class: the record states every class beside its figure, so a reader
|
||||
sees what was compared.
|
||||
|
||||
**Why per successful job.** A router tries survivors in turn until one serves. For one
|
||||
quantity, cost to the router or time to the job, let an attempt at survivor *i* spend *Cᵢ* on
|
||||
average (*sᵢ* × its list figure + *fᵢ* × its failure's) and serve with probability *sᵢ*. The
|
||||
attempts of an order spend *C*₁ + *f*₁*C*₂ + *f*₁*f*₂*C*₃ + … on average, each attempt
|
||||
weighted by the chance that every one before it failed. Exchanging two neighbours *i*, *j*
|
||||
changes that sum by a term whose sign is that of *Cᵢ* / *sᵢ* − *Cⱼ* / *sⱼ*, so the order that
|
||||
minimises it ranks by *C* / *s* = the list figure + (*f* / *s*) × the failure's: the expected
|
||||
figure per successful job (§4.2). An endpoint that fails often is tried first only where it is
|
||||
cheaper, or faster, per job served. The policy is unchanged: it names its keys, and a price
|
||||
book of version 2 gives `price` and `ttfb` these figures.
|
||||
|
||||
### 4.5 The decision
|
||||
|
||||
|
|
@ -257,7 +370,7 @@ A route record is a document of exactly these members:
|
|||
| member | form |
|
||||
|---|---|
|
||||
| `kind` | `"wai-route-record"` |
|
||||
| `version` | `1` in this revision |
|
||||
| `version` | `1` or `2`: its price book's version (§3.4) |
|
||||
| `job`, `operation`, `capability`, `fallback`, `quantities` | the request's |
|
||||
| `request` | the request's digest |
|
||||
| `policy` | an object of exactly `name`, `version` and `digest`: the policy's |
|
||||
|
|
@ -276,11 +389,16 @@ A candidate:
|
|||
| member | form |
|
||||
|---|---|
|
||||
| `endpoint`, `provider`, `capability`, `region`, `residency`, `trains_on_inputs`, `retention_days`, `ga`, `needs_consent`, `unit`, `divisor`, `price`, `ttfb_ms`, `energy_uj`, `energy_class`, `quality` | the price book's entry's, as it stated them |
|
||||
| `operation`, `failures` | version 2: the entry's, as it stated them |
|
||||
| `via_fallback` | a boolean (§4.1) |
|
||||
| `cost`, `energy` | its estimates (§4.2) |
|
||||
| `expected_cost`, `expected_ttfb_ms` | version 2: its expected cost and expected time to first byte per successful job (§4.2), each an integer or `null` |
|
||||
| `removed` | its removals (§4.3): objects of exactly `limit` and `value`, in §4.3's order; empty for a survivor |
|
||||
| `rank` | its rank (§4.4), or `null` for a candidate a limit removed |
|
||||
|
||||
So a record of version 2 states, for every candidate, the failures it was ranked by with
|
||||
their basis, and the terms the ranking derived from them.
|
||||
|
||||
### 5.2 Attempts
|
||||
|
||||
An attempt is one hop the router sent the job to:
|
||||
|
|
@ -295,6 +413,7 @@ An attempt is one hop the router sent the job to:
|
|||
| `billed` | a boolean: whether the job is billed for it |
|
||||
| `not_billed` | `null` when billed; otherwise `failed` or `deadline_missed` |
|
||||
| `receipt` | the `receipt_id` of the hop's OER/2 receipt (oer-receipts §5): `sha256:` or `blake3:` and 64 lowercase hexadecimal digits |
|
||||
| `outputs` | version 2: the content hashes (§2) of the objects the hop delivered, in the order it delivered them: when served, 1 to 1024, no two the same; when failed, none (an empty list) |
|
||||
|
||||
The attempts **MUST** follow the order: the attempt at position *i* names the *i*-th survivor
|
||||
of the order, and there are at most the allowed attempts (§4.5). So no attempt names a
|
||||
|
|
@ -337,7 +456,20 @@ holds each attempt to its receipt:
|
|||
- it verifies under oer-receipts on its own, with no reason (§7 there): otherwise
|
||||
`receipt_invalid`;
|
||||
- it is a `job` receipt of the record's operation whose `work.status` is the attempt's
|
||||
outcome's: otherwise `receipt_mismatch`.
|
||||
outcome's: otherwise `receipt_mismatch`;
|
||||
- in a record of version 2, the receipt of the attempt that served names the outputs the
|
||||
attempt states: its `subject.output` is their commitment, as oer-receipts §3 *Output*
|
||||
defines it, the content hash of the one output, or the jwp-receipts §2.2 root over their
|
||||
content hashes in their order, every leaf of the unlabelled form, as 64 lowercase
|
||||
hexadecimal digits: otherwise `receipt_output_mismatch`.
|
||||
|
||||
So a receipt cannot be moved to vouch for an output it did not produce. The hop signs what it
|
||||
produced, in its receipt; the router signs what the job delivered and which receipt is the
|
||||
hop's, in its record; and a record that pairs a hop's receipt with outputs the receipt does
|
||||
not name is refused. A holder of the job's output checks the remaining step, that its
|
||||
objects' content hashes are the record's outputs (§7.3). The attempt that served is held to
|
||||
its outputs whether or not it is billed: one that served past the deadline delivered them
|
||||
too. A failed attempt delivered nothing, and its receipt is not held to an output.
|
||||
|
||||
A receipt states the hop's energy with its grade, and, in minor version 1, its class or the
|
||||
unmetered marker; the record states no figure of its own. A reader of the hops' energy reads
|
||||
|
|
@ -363,18 +495,21 @@ and where (`at`, a list: the member, then indexes or members within it):
|
|||
2. **The price book** (§3.2), at `pricebook`: `malformed`, `pricebook_invalid`.
|
||||
3. **The policy** (§3.3), at `policy`: `malformed`, `policy_invalid`.
|
||||
4. **The record** (§5), at `record`: a text that is not a document, or whose `kind` is not
|
||||
`wai-route-record`, is `malformed`; a `version` that is an integer other than `1` is
|
||||
`unknown_version`, at `record`, `version`; one that is not of §5.1's and §5.2's members,
|
||||
each of the form its table gives it (a restated member of a candidate, the form §3.2 gives
|
||||
it), is `record_invalid` (an attempt whose `hop` is not its position, or whose `error` is
|
||||
not as its `outcome` requires, among them; the relations between a candidate's members,
|
||||
such as an energy figure and its class, are step 6's); then its signature (§5.3):
|
||||
`bad_key`, `bad_signature`, at `record`, `sig`.
|
||||
`wai-route-record`, is `malformed`; a `version` that is an integer other than `1` and `2`
|
||||
is `unknown_version`, at `record`, `version`; one that is not of §5.1's and §5.2's members
|
||||
for its version, each of the form its table gives it (a restated member of a candidate, the
|
||||
form §3.2 gives it, and of `failures`, each member an integer and `hops` at least 1), is
|
||||
`record_invalid` (an attempt whose `hop` is not its position, or whose `error` or
|
||||
`outputs` is not as its `outcome` requires, among them; the relations between a
|
||||
candidate's members, such as an energy figure and its class, or a failure count and its
|
||||
hops, are step 6's); then its signature (§5.3): `bad_key`, `bad_signature`, at `record`,
|
||||
`sig`.
|
||||
5. **The binding**, at `record` and the member: its `job`, `operation`, `capability`,
|
||||
`fallback`, `quantities`, `request` and `deadline` are not the request's
|
||||
(`request_mismatch`); its `policy` is not the policy's name, version and digest
|
||||
(`policy_mismatch`); its `pricebook` is not the price book's digest, currency and `issued`
|
||||
(`pricebook_mismatch`).
|
||||
(`policy_mismatch`); its `version` is not the price book's (`pricebook_mismatch`, at
|
||||
`record`, `version`), or its `pricebook` is not the price book's digest, currency and
|
||||
`issued` (`pricebook_mismatch`).
|
||||
6. **The candidates**, at `record`, `candidates` and the index, recomputed by §4, each in
|
||||
turn and for each these three in this order: a candidate missing, added or out of order,
|
||||
or with a member other than `removed` and `rank` not what §4 gives it
|
||||
|
|
@ -393,14 +528,16 @@ and where (`at`, a list: the member, then indexes or members within it):
|
|||
8. **The hop receipts** (§6), when `receipts` is not `null`: first each receipt's text, at
|
||||
`receipts` and its index, which a receipt that is not a document under oer-receipts §4
|
||||
fails (`receipt_invalid`); then each attempt in turn, at `record`, `attempts`, its index and
|
||||
`receipt`: `receipt_missing`, `receipt_invalid`, `receipt_mismatch`, the first receipt of the
|
||||
list whose `receipt_id` is the attempt's being its receipt.
|
||||
`receipt`: `receipt_missing`, `receipt_invalid`, `receipt_mismatch`,
|
||||
`receipt_output_mismatch`, the first receipt of the list whose `receipt_id` is the
|
||||
attempt's being its receipt.
|
||||
|
||||
### 7.3 Outcome
|
||||
|
||||
- **refuse**: the reason and where.
|
||||
- **accept**: the record's digest; the job and its operation; the served endpoint and the hop
|
||||
that served it, or `null`s for a job no hop served; the billed hop, or `null`; the deadline,
|
||||
that served it, and in a record of version 2 the content hashes of its outputs, in their
|
||||
order, or `null`s for a job no hop served; the billed hop, or `null`; the deadline,
|
||||
`none` when none was declared and otherwise `met` or `missed` by the served hop, or `none`
|
||||
too when no hop served; the order; the decision; each removed candidate's endpoint with the
|
||||
limits that removed it; whether the hop receipts were `checked` or `not_checked`; and, when
|
||||
|
|
@ -434,6 +571,7 @@ The codes this extension adds, each with where it is raised:
|
|||
| `receipt_missing` | §6, §7.2 step 8 |
|
||||
| `receipt_invalid` | §6, §7.2 step 8 |
|
||||
| `receipt_mismatch` | §6, §7.2 step 8 |
|
||||
| `receipt_output_mismatch` | §6, §7.2 step 8 |
|
||||
|
||||
A verifier of this extension also raises `malformed` for a text that is not a document (§2)
|
||||
or a verification document that is not §7.1's, `unknown_version` for a record of another
|
||||
|
|
@ -460,9 +598,28 @@ verification document with the outcome §7.3 gives it:
|
|||
every limit of §4.3 removing a candidate, a `null` value among them, and a candidate removed
|
||||
by several; a ceiling in another currency; the fallback's candidates; and a record verified
|
||||
without its receipts.
|
||||
- **Accepted, version 2**, over one price book whose endpoint prices its encode and its decode
|
||||
apart and whose encodes' failures run from none in 1,000 hops to all of 500: a ranking by the
|
||||
expected cost, which puts an endpoint whose billed failures make it dearer after one of the
|
||||
same list price, a cheaper one that fails more after both, and the unmeasured endpoint, the
|
||||
basis of 99 hops and the basis whose every hop failed after every measured one, a basis of
|
||||
exactly 100 hops counting; a decode at its own price, under a unit-price cap its endpoint's
|
||||
encode exceeds; a ranking and a target by the expected time to first byte, and one by the
|
||||
list cost where no failure was measured; a spend ceiling that reads the list cost; a failover
|
||||
whose failed hop delivered nothing; a hop that served three outputs, one that served past the
|
||||
deadline, its outputs bound all the same; and a record verified without its receipts.
|
||||
- **Refused**: each reason of §8 at least once, each vector a control of one defect, its
|
||||
record signed again over the defect where the defect is not in the signature; and each code
|
||||
§8 reuses.
|
||||
§8 reuses. In version 2, each rule of §3.2 and §3.2.1 broken once (an entry out of order by
|
||||
its operation, without its operation or its failures; more hops failed than were sent; a
|
||||
period ending after the book was issued, or reversed; a billed price or a time with no
|
||||
failed hop; a basis of no hop), an operation in a price book of version 1 and a price book
|
||||
of version 3; a record of either version on a price book of the other; an expected figure
|
||||
and a basis restated, and the other operation's entry listed; a served hop with no output,
|
||||
a failed one with one, an output twice and one in capitals; and a served hop's receipt moved
|
||||
from another job, a late hop's too, outputs restated in another order, one output named by
|
||||
its leaf and three by the first of them (`receipt_output_mismatch`). A record of version 3
|
||||
is `unknown_version`.
|
||||
|
||||
`wai_route_record_vectors verify` and the test `tests/route_record.rs` check that the file
|
||||
regenerates byte for byte, that `wai-rs` gives every vector its outcome, that every reason of
|
||||
|
|
@ -470,10 +627,11 @@ regenerates byte for byte, that `wai-rs` gives every vector its outcome, that ev
|
|||
`live-conformance/route_record_verify.py` is the second implementation, written from this
|
||||
text: it shares no code with `wai-rs`, verifies the router's signature with WAI's strict rule
|
||||
written from RFC 8032 (`claim-conformance/_ed25519.py`), verifies the hop receipts with the
|
||||
OER/2 reference verifier (`oer-conformance/oer2_verify.py`), recomputes every candidate,
|
||||
removal, rank, order, decision and attempt itself, gives every vector the same outcome,
|
||||
requires every mutated copy of the corpus to fail, and requires the corpus to catch the
|
||||
verifier with each of its checks dropped.
|
||||
OER/2 reference verifier (`oer-conformance/oer2_verify.py`), computes the outputs'
|
||||
commitment with BLAKE3 written from its specification (`claim-conformance/_blake3.py`),
|
||||
recomputes every candidate, removal, expected figure, rank, order, decision and attempt
|
||||
itself, gives every vector the same outcome, requires every mutated copy of the corpus to
|
||||
fail, and requires the corpus to catch the verifier with each of its checks dropped.
|
||||
|
||||
## 11. Security and privacy considerations
|
||||
|
||||
|
|
@ -494,6 +652,21 @@ verifier with each of its checks dropped.
|
|||
- **Unknown is restrictive.** Every `null` a limit reads removes the candidate (§4.3), so a
|
||||
price book that omits what a buyer cares about removes the endpoint rather than admitting
|
||||
it.
|
||||
- **Failures are measured, and stated with their basis.** A price book of version 2 states
|
||||
each endpoint's failures as the router measured them, with the hops and the period they
|
||||
were read from, and the record restates them beside the expected figures the ranking read.
|
||||
The record does not prove the measurement, as it does not prove a price: a reader sees what
|
||||
the order rests on, and a basis too thin to mean anything is read as no measurement at all
|
||||
(§3.2.1). Leaving an endpoint unmeasured gains it nothing: its expected figures are `null`,
|
||||
ranked after every measured one (§4.2).
|
||||
- **The job pays the list price.** What failures cost is the router's: it orders the
|
||||
survivors and is never billed to the job, whose limits and line read the list price
|
||||
(§4.2).
|
||||
- **A receipt is bound to its output.** In version 2, a served hop's receipt names the outputs
|
||||
the record states it delivered (§6), so a receipt cannot vouch, in another record, for an
|
||||
output its hop did not produce; and a holder of the output who finds its content hashes in
|
||||
the record knows which hop's receipt is about it.
|
||||
- **What a record reveals.** Its job id, its operation and its quantities, every endpoint and
|
||||
provider that could have served the job with their prices, and when each hop ran. It
|
||||
travels between the router, the job's owner and an auditor they choose.
|
||||
provider that could have served the job with their prices, and when each hop ran; in
|
||||
version 2, every candidate's measured failures and the content hashes of the job's
|
||||
outputs. It travels between the router, the job's owner and an auditor they choose.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,13 @@ else is here:
|
|||
- the record (§5): its members and forms, its signature, its binding to the documents, its
|
||||
candidates, order and decision recomputed, and its attempts held to the order and the
|
||||
billing rules;
|
||||
- the hop receipts (§6), and the outcome of §7.3.
|
||||
- the hop receipts (§6), and the outcome of §7.3;
|
||||
- version 2 (§3.4): a price book that prices each operation of an endpoint and states its
|
||||
failures over their basis (§3.2.1), the candidates of the request's operation, the expected
|
||||
cost and time to first byte per successful job and the ranking that reads them (§4.2, §4.4),
|
||||
a record's version held to its price book's, and the served hop's receipt held to the
|
||||
outputs the record states, their commitment computed with ../claim-conformance/_blake3.py
|
||||
(BLAKE3 written from its specification) as oer-receipts §3 *Output* defines it.
|
||||
|
||||
For every vector it requires the outcome the corpus states; of the corpus, that every reason of
|
||||
§8 and every code it reuses appears in it, that §8 names every reason it raises, and that no
|
||||
|
|
@ -43,6 +49,7 @@ sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
|
|||
sys.path.insert(0, os.path.join(HERE, "..", "oer-conformance"))
|
||||
import _ed25519 as ED # noqa: E402
|
||||
import oer2_verify as OER # noqa: E402
|
||||
from _blake3 import blake3 # noqa: E402
|
||||
|
||||
EXTENSION = os.path.join(HERE, "..", "extensions", "route-record.md")
|
||||
|
||||
|
|
@ -55,10 +62,19 @@ DECIDED_BY = ["none", "only_survivor", "targets", "price", "ttfb", "energy", "qu
|
|||
MAX = 2 ** 53 - 1
|
||||
ENTRY = ["endpoint", "provider", "capability", "region", "residency", "trains_on_inputs", "retention_days", "ga",
|
||||
"needs_consent", "unit", "divisor", "price", "ttfb_ms", "energy_uj", "energy_class", "quality"] # §3.2
|
||||
CANDIDATE = ENTRY + ["via_fallback", "cost", "energy", "removed", "rank"] # §5.1
|
||||
ENTRY_2 = ENTRY + ["operation", "failures"] # §3.2, version 2
|
||||
FAILURES = ["hops", "from", "to", "failed", "billed_price", "failed_ms"] # §3.2.1
|
||||
MIN_BASIS = 100 # §3.2.1
|
||||
RECORD = ["kind", "version", "job", "operation", "capability", "fallback", "quantities", "request", "policy", "pricebook",
|
||||
"candidates", "order", "decision", "accepted", "deadline", "attempts", "signer", "sig"] # §5.1
|
||||
ATTEMPT = ["hop", "endpoint", "outcome", "error", "start", "end", "billed", "not_billed", "receipt"] # §5.2
|
||||
LEAF, NODE = b"moq-jwp:merkle-leaf\x01", b"moq-jwp:merkle-node\x01" # jwp-receipts §2.2
|
||||
|
||||
|
||||
def candidate_members(version):
|
||||
"""§5.1: an entry's members, the five a candidate adds, and in version 2 the expected two."""
|
||||
return (ENTRY_2 if version == 2 else ENTRY) + ["via_fallback", "cost", "energy", "removed", "rank"] \
|
||||
+ (["expected_cost", "expected_ttfb_ms"] if version == 2 else [])
|
||||
|
||||
SKIP = set()
|
||||
|
||||
|
|
@ -252,8 +268,27 @@ def request_ok(r):
|
|||
return null_or(r["deadline_us"], lambda d: is_int(d, 1))
|
||||
|
||||
|
||||
def entry_ok(e):
|
||||
if not exactly(e, ENTRY):
|
||||
def failures_form(f):
|
||||
"""§3.2.1's table, each member of its form."""
|
||||
return exactly(f, FAILURES) and is_int(f["hops"], 1) and all(is_int(f[k]) for k in FAILURES)
|
||||
|
||||
|
||||
def failures_ok(f, issued):
|
||||
"""§3.2.1: the form, and its relations: no more failed than sent, a period that ends by the
|
||||
book's issue, and no billed price or time without a failed hop."""
|
||||
if not failures_form(f):
|
||||
return False
|
||||
if not on("failures-relations"):
|
||||
return True
|
||||
return f["failed"] <= f["hops"] and f["from"] <= f["to"] <= issued \
|
||||
and (f["failed"] > 0 or (f["billed_price"] == 0 and f["failed_ms"] == 0))
|
||||
|
||||
|
||||
def entry_ok(e, version=1, issued=0):
|
||||
if not exactly(e, ENTRY_2 if version == 2 else ENTRY):
|
||||
return False
|
||||
if version == 2 and not (isinstance(e["operation"], str) and OPERATION.match(e["operation"])
|
||||
and null_or(e["failures"], lambda f: failures_ok(f, issued))):
|
||||
return False
|
||||
return (is_token(e["endpoint"]) and is_token(e["provider"]) and is_cap(e["capability"])
|
||||
and null_or(e["region"], is_token) and null_or(e["residency"], is_token)
|
||||
|
|
@ -265,14 +300,30 @@ def entry_ok(e):
|
|||
and null_or(e["quality"], lambda q: is_int(q, 0, 1000)))
|
||||
|
||||
|
||||
def book_version(b):
|
||||
"""§3.4: a price book with no `version` member is of version 1."""
|
||||
return b.get("version", 1) if isinstance(b, dict) else 1
|
||||
|
||||
|
||||
def pricebook_ok(b):
|
||||
if not exactly(b, ["kind", "currency", "issued", "entries"]) or b["kind"] != "wai-route-pricebook":
|
||||
if not isinstance(b, dict):
|
||||
return False
|
||||
# A `version` member gives version 2's members; its value must be 2.
|
||||
shape = 2 if "version" in b else 1
|
||||
if shape == 2 and on("pricebook-version") and not (is_int(b["version"]) and b["version"] == 2):
|
||||
return False
|
||||
members = ["kind", "version", "currency", "issued", "entries"] if shape == 2 else ["kind", "currency", "issued", "entries"]
|
||||
if not exactly(b, members) or b["kind"] != "wai-route-pricebook":
|
||||
return False
|
||||
if not (isinstance(b["currency"], str) and CURRENCY.match(b["currency"]) and is_int(b["issued"])):
|
||||
return False
|
||||
es = b["entries"]
|
||||
if not (isinstance(es, list) and 1 <= len(es) <= 1024 and all(entry_ok(e) for e in es)):
|
||||
if not (isinstance(es, list) and 1 <= len(es) <= 1024 and all(entry_ok(e, shape, b["issued"]) for e in es)):
|
||||
return False
|
||||
if shape == 2:
|
||||
# By endpoint, then operation: compared as a pair of byte strings, never joined.
|
||||
keys = [(e["endpoint"].encode(), e["operation"].encode()) for e in es]
|
||||
return all(keys[i] < keys[i + 1] for i in range(len(keys) - 1))
|
||||
return ascending([e["endpoint"] for e in es])
|
||||
|
||||
|
||||
|
|
@ -341,9 +392,38 @@ def removals(req, book, pol, e, cost):
|
|||
return out
|
||||
|
||||
|
||||
def expected(q, e, cost):
|
||||
"""§4.2, version 2: the expected cost and time to first byte per successful job. A job sent
|
||||
to the endpoint until served meets failed / (hops - failed) failures on average; each costs
|
||||
the router its billed price for the job's quantity and costs the job its time. Unknown
|
||||
failures (none stated, or fewer than 100 hops), an unknown list figure, a basis every hop
|
||||
of which failed, and a figure past 2^53 - 1 give None."""
|
||||
f = e["failures"]
|
||||
least = MIN_BASIS if on("thin-basis") else 1
|
||||
if f is None or f["hops"] < least:
|
||||
return None, None
|
||||
ok = f["hops"] - f["failed"]
|
||||
if ok == 0:
|
||||
if on("unbounded"):
|
||||
return None, None
|
||||
ok = None
|
||||
|
||||
def plus(lst, per_failure):
|
||||
if lst is None or per_failure is None:
|
||||
return None
|
||||
t = lst + (0 if ok is None else per_failure * f["failed"] // ok)
|
||||
return t if t <= MAX else None
|
||||
return plus(cost, estimate(q, f["billed_price"], e["divisor"])), plus(e["ttfb_ms"], f["failed_ms"])
|
||||
|
||||
|
||||
def ttfb_read(c):
|
||||
"""The time to first byte §4.4 reads: in version 2, the expected one."""
|
||||
return c["expected_ttfb_ms"] if "expected_ttfb_ms" in c and on("expected-targets") else c["ttfb_ms"]
|
||||
|
||||
|
||||
def meets(req, c):
|
||||
t, q = req["prefer"]["ttfb_ms"], req["prefer"]["quality"]
|
||||
return (t is None or (c["ttfb_ms"] is not None and c["ttfb_ms"] <= t)) and \
|
||||
return (t is None or (ttfb_read(c) is not None and ttfb_read(c) <= t)) and \
|
||||
(q is None or (c["quality"] is not None and c["quality"] >= q))
|
||||
|
||||
|
||||
|
|
@ -370,13 +450,17 @@ def compare(req, key, a, b):
|
|||
if x == y:
|
||||
return 0
|
||||
return (-1 if x > y else 1) if high_first else (-1 if x < y else 1)
|
||||
v2 = "expected_cost" in a
|
||||
if key == "targets":
|
||||
ma, mb = meets(req, a), meets(req, b)
|
||||
return 0 if ma == mb else (-1 if ma else 1)
|
||||
if key == "price":
|
||||
return nulls_last(a["cost"], b["cost"])
|
||||
# Version 2: the expected cost, then the list cost (§4.4).
|
||||
first = nulls_last(a["expected_cost"], b["expected_cost"]) if v2 and on("expected-price") else 0
|
||||
return first or nulls_last(a["cost"], b["cost"])
|
||||
if key == "ttfb":
|
||||
return nulls_last(a["ttfb_ms"], b["ttfb_ms"])
|
||||
first = nulls_last(a["expected_ttfb_ms"], b["expected_ttfb_ms"]) if v2 and on("expected-ttfb") else 0
|
||||
return first or nulls_last(a["ttfb_ms"], b["ttfb_ms"])
|
||||
if key == "energy":
|
||||
return nulls_last(a["energy"], b["energy"])
|
||||
if key == "quality":
|
||||
|
|
@ -388,7 +472,10 @@ def compare(req, key, a, b):
|
|||
def route(req, book, pol):
|
||||
"""§4: the candidates as the record states them, the order, the decision, the allowed."""
|
||||
cands = []
|
||||
v2 = book_version(book) != 1
|
||||
for e in book["entries"]:
|
||||
if v2 and on("operation-candidates") and e["operation"] != req["operation"]:
|
||||
continue
|
||||
if e["capability"] == req["capability"]:
|
||||
via = False
|
||||
elif req["fallback"] is not None and e["capability"] == req["fallback"]:
|
||||
|
|
@ -401,6 +488,8 @@ def route(req, book, pol):
|
|||
c = dict(e)
|
||||
c.update(via_fallback=via, cost=cost, energy=energy,
|
||||
removed=[{"limit": l, "value": v} for l, v in removals(req, book, pol, e, cost)], rank=None)
|
||||
if v2:
|
||||
c["expected_cost"], c["expected_ttfb_ms"] = expected(q, e, cost)
|
||||
cands.append(c)
|
||||
keys = key_list(req, pol)
|
||||
survivors = [c for c in cands if not c["removed"]]
|
||||
|
|
@ -436,10 +525,15 @@ def scalar(v):
|
|||
return not isinstance(v, (dict, list))
|
||||
|
||||
|
||||
def candidate_form(c):
|
||||
def candidate_form(c, version=1):
|
||||
"""§5.1: each member of its own form. Relations between members (an energy figure and its
|
||||
class, a cost and its price) are the recomputation's, at §7.2 step 6."""
|
||||
if not exactly(c, CANDIDATE):
|
||||
class, a cost and its price, a failure count and its hops) are the recomputation's, at §7.2
|
||||
step 6."""
|
||||
if not exactly(c, candidate_members(version)):
|
||||
return False
|
||||
if version == 2 and not (isinstance(c["operation"], str) and OPERATION.match(c["operation"])
|
||||
and null_or(c["failures"], failures_form)
|
||||
and null_or(c["expected_cost"], is_int) and null_or(c["expected_ttfb_ms"], is_int)):
|
||||
return False
|
||||
is_bool = lambda b: isinstance(b, bool) # noqa: E731
|
||||
return (is_token(c["endpoint"]) and is_token(c["provider"]) and is_cap(c["capability"])
|
||||
|
|
@ -455,8 +549,19 @@ def candidate_form(c):
|
|||
and null_or(c["rank"], lambda n: is_int(n, 1)))
|
||||
|
||||
|
||||
def attempt_form(i, a):
|
||||
if not exactly(a, ATTEMPT) or a["outcome"] not in ("served", "failed"):
|
||||
def outputs_form(a):
|
||||
"""§5.2, version 2: a served attempt delivered 1 to 1024 objects, each named once by its
|
||||
content hash; a failed one, none."""
|
||||
xs = a["outputs"]
|
||||
if not (isinstance(xs, list) and all(is_hex(x, 64) for x in xs) and len(set(xs)) == len(xs)):
|
||||
return False
|
||||
return 1 <= len(xs) <= 1024 if a["outcome"] == "served" else not xs
|
||||
|
||||
|
||||
def attempt_form(i, a, version=1):
|
||||
if not exactly(a, ATTEMPT + (["outputs"] if version == 2 else [])) or a["outcome"] not in ("served", "failed"):
|
||||
return False
|
||||
if version == 2 and on("outputs-form") and not outputs_form(a):
|
||||
return False
|
||||
served = a["outcome"] == "served"
|
||||
error_ok = a["error"] is None if served else a["error"] in ERRORS
|
||||
|
|
@ -468,7 +573,8 @@ def attempt_form(i, a):
|
|||
def record_form(r):
|
||||
if not exactly(r, RECORD):
|
||||
return False
|
||||
return (is_int(r["version"]) and r["version"] == 1 and is_token(r["job"]) and isinstance(r["operation"], str)
|
||||
v = r["version"]
|
||||
return (is_int(v) and v in (1, 2) and is_token(r["job"]) and isinstance(r["operation"], str)
|
||||
and OPERATION.match(r["operation"]) is not None and is_cap(r["capability"]) and null_or(r["fallback"], is_cap)
|
||||
and quantities_ok(r["quantities"]) and is_hex(r["request"], 64)
|
||||
and exactly(r["policy"], ["name", "version", "digest"]) and is_token(r["policy"]["name"])
|
||||
|
|
@ -476,13 +582,13 @@ def record_form(r):
|
|||
and exactly(r["pricebook"], ["digest", "currency", "issued"]) and is_hex(r["pricebook"]["digest"], 64)
|
||||
and isinstance(r["pricebook"]["currency"], str) and CURRENCY.match(r["pricebook"]["currency"]) is not None
|
||||
and is_int(r["pricebook"]["issued"])
|
||||
and isinstance(r["candidates"], list) and len(r["candidates"]) <= 1024 and all(candidate_form(c) for c in r["candidates"])
|
||||
and isinstance(r["candidates"], list) and len(r["candidates"]) <= 1024 and all(candidate_form(c, v) for c in r["candidates"])
|
||||
and isinstance(r["order"], list) and len(r["order"]) <= 1024 and all(is_token(x) for x in r["order"])
|
||||
and exactly(r["decision"], ["decided_by", "runner_up"]) and r["decision"]["decided_by"] in DECIDED_BY
|
||||
and null_or(r["decision"]["runner_up"], is_token)
|
||||
and is_int(r["accepted"]) and null_or(r["deadline"], is_int)
|
||||
and isinstance(r["attempts"], list) and len(r["attempts"]) <= 16
|
||||
and all(attempt_form(i, a) for i, a in enumerate(r["attempts"]))
|
||||
and all(attempt_form(i, a, v) for i, a in enumerate(r["attempts"]))
|
||||
and is_hex(r["signer"], 64) and is_hex(r["sig"], 128))
|
||||
|
||||
|
||||
|
|
@ -534,7 +640,7 @@ def verify_steps(doc):
|
|||
rec = read_document(doc["record"])
|
||||
if not isinstance(rec, dict) or rec.get("kind") != "wai-route-record":
|
||||
raise Refuse("malformed", ["record"])
|
||||
if on("version") and is_int(rec.get("version")) and rec["version"] != 1:
|
||||
if on("version") and is_int(rec.get("version")) and rec["version"] not in (1, 2):
|
||||
raise Refuse("unknown_version", ["record", "version"])
|
||||
if on("record-form") and not record_form(rec):
|
||||
raise Refuse("record_invalid", ["record"])
|
||||
|
|
@ -552,6 +658,8 @@ def verify_steps(doc):
|
|||
raise Refuse("request_mismatch", ["record", m])
|
||||
if on("binding-policy") and not same(rec["policy"], {"name": pol["name"], "version": pol["version"], "digest": digest(pol)}):
|
||||
raise Refuse("policy_mismatch", ["record", "policy"])
|
||||
if on("record-version") and not same(rec["version"], book_version(book)):
|
||||
raise Refuse("pricebook_mismatch", ["record", "version"])
|
||||
if on("binding-pricebook") and not same(rec["pricebook"], {"digest": digest(book), "currency": book["currency"], "issued": book["issued"]}):
|
||||
raise Refuse("pricebook_mismatch", ["record", "pricebook"])
|
||||
# 6. The candidates, the order, the decision.
|
||||
|
|
@ -610,7 +718,9 @@ def verify_steps(doc):
|
|||
"digest": digest(rec),
|
||||
"job": req["job"],
|
||||
"operation": req["operation"],
|
||||
"served": None if served_attempt is None else {"endpoint": served_attempt["endpoint"], "hop": served_attempt["hop"]},
|
||||
"served": None if served_attempt is None else dict(
|
||||
{"endpoint": served_attempt["endpoint"], "hop": served_attempt["hop"]},
|
||||
**({"outputs": served_attempt["outputs"]} if "outputs" in served_attempt else {})),
|
||||
"billed": billed_hop,
|
||||
"deadline": dl,
|
||||
"order": order,
|
||||
|
|
@ -621,6 +731,20 @@ def verify_steps(doc):
|
|||
}
|
||||
|
||||
|
||||
def commitment(outputs):
|
||||
"""oer-receipts §3 *Output*: one output's content hash, or the jwp-receipts §2.2 root over
|
||||
several in their order, every leaf unlabelled, an odd level padded with its last node."""
|
||||
hs = [bytes.fromhex(x) for x in outputs]
|
||||
if len(hs) == 1:
|
||||
return hs[0].hex()
|
||||
level = [blake3(LEAF + h) for h in hs]
|
||||
while len(level) > 1:
|
||||
if len(level) % 2:
|
||||
level.append(level[-1])
|
||||
level = [blake3(NODE + level[i] + level[i + 1]) for i in range(0, len(level), 2)]
|
||||
return level[0].hex() if level else "00" * 32
|
||||
|
||||
|
||||
def hop_receipts(rec, texts, keys):
|
||||
"""§6: each attempt held to its hop's OER/2 receipt. The hops whose receipts state the
|
||||
unmetered marker."""
|
||||
|
|
@ -646,6 +770,11 @@ def hop_receipts(rec, texts, keys):
|
|||
status_ok = isinstance(got, int) and not isinstance(got, bool) and got == status
|
||||
if on("receipt-matches") and (r.get("kind") != "job" or r.get("capability") != "oer.job." + rec["operation"] or not status_ok):
|
||||
raise Refuse("receipt_mismatch", at)
|
||||
# Version 2 (§6): the served hop's receipt names the outputs the attempt states.
|
||||
if on("receipt-output") and a["outcome"] == "served" and "outputs" in a:
|
||||
subject = r.get("subject") if isinstance(r.get("subject"), dict) else {}
|
||||
if subject.get("output") != commitment(a["outputs"]):
|
||||
raise Refuse("receipt_output_mismatch", at)
|
||||
if OER.is_unmetered(r):
|
||||
unmetered.append(a["hop"])
|
||||
return unmetered
|
||||
|
|
@ -718,6 +847,10 @@ def mutations(doc):
|
|||
"keys", {n: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" for n in x["document"]["keys"]}))
|
||||
copy_with("its stated served hop changed", k, lambda x: x["outcome"].__setitem__("served", None if o["served"] else {"endpoint": "im.alpha", "hop": 1}))
|
||||
copy_with("its stated billed hop changed", k, lambda x: x["outcome"].__setitem__("billed", None if o["billed"] else 1))
|
||||
if o["served"] and "outputs" in o["served"]:
|
||||
copy_with("its stated outputs reversed and one more", k, lambda x: x["outcome"]["served"].__setitem__(
|
||||
"outputs", list(reversed(o["served"]["outputs"])) + ["00" * 32]))
|
||||
copy_with("its served hop's outputs dropped from the outcome", k, lambda x: x["outcome"]["served"].pop("outputs"))
|
||||
copy_with("its stated deadline changed", k, lambda x: x["outcome"].__setitem__("deadline", "met" if o["deadline"] != "met" else "missed"))
|
||||
copy_with("its stated order reversed", k, lambda x: x["outcome"].__setitem__("order", list(reversed(o["order"])) + ["x"]))
|
||||
copy_with("its stated decision changed", k, lambda x: x["outcome"]["decision"].__setitem__("decided_by", "price" if o["decision"]["decided_by"] != "price" else "ttfb"))
|
||||
|
|
@ -731,6 +864,8 @@ CHECKS = ["request_invalid", "pricebook_invalid", "policy_invalid", "version", "
|
|||
"binding-request", "binding-policy", "binding-pricebook", "candidates", "removals", "ranks", "order", "decision",
|
||||
"allowed", "removed", "in-order", "served-last", "times", "billing", "complete",
|
||||
"receipt-given", "receipt-verifies", "receipt-matches",
|
||||
"pricebook-version", "failures-relations", "operation-candidates", "thin-basis", "unbounded", "expected-price",
|
||||
"expected-ttfb", "expected-targets", "record-version", "outputs-form", "receipt-output",
|
||||
"limit-policy", "limit-unit", "limit-region", "limit-residency", "limit-training", "limit-retention", "limit-ga",
|
||||
"limit-consent", "limit-unit-price", "limit-spend"]
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,14 @@ signed again over the defect where the defect is not in the signature, and the g
|
|||
refuses to write a corpus whose control is refused for any reason or at any place but its
|
||||
own.
|
||||
|
||||
The vectors of version 1 come first, then those of version 2 (extension §3.4), whose names
|
||||
begin `v2_` but for the controls that pair a record with a price book of the other version:
|
||||
a price book that prices each operation of an endpoint and states its measured failures, the
|
||||
ranking by the expected cost and time to first byte per successful job, and the served hop's
|
||||
outputs, to which its receipt is held. The outputs are the content hashes of fixed byte
|
||||
strings standing for the envelopes a hop delivered; the verifier reads only their content
|
||||
hashes.
|
||||
|
||||
```sh
|
||||
# wai-rs: regenerates byte for byte, and every vector gets its outcome
|
||||
cargo run --no-default-features --features route_record --bin wai_route_record_vectors -- verify ../live-conformance/routes
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -10,6 +10,13 @@
|
|||
//! holds the attempts to the order and the billing rules, and, given the hops' OER/2
|
||||
//! receipts, holds each attempt to its receipt through [`crate::oer`] (§6).
|
||||
//!
|
||||
//! Version 2 of the price book and the record (§3.4): an entry prices one operation of an
|
||||
//! endpoint and states its measured failures with their basis (§3.2.1); a candidate carries
|
||||
//! the expected cost and time to first byte per successful job that the ranking reads (§4.2,
|
||||
//! §4.4); and the attempt that served states its outputs, to which its hop's receipt is held
|
||||
//! by [`output_commitment`] (§6). [`seal_delivered`] writes a record of its price book's
|
||||
//! version. Version 1 reads, routes and verifies exactly as before.
|
||||
//!
|
||||
//! The corpus `live-conformance/routes/` is written by [`corpus::generate`] and checked by
|
||||
//! [`corpus::verify`] here, and by `live-conformance/route_record_verify.py`, which shares no
|
||||
//! code with this module.
|
||||
|
|
@ -26,8 +33,19 @@ pub mod corpus;
|
|||
|
||||
/// The record this extension registers.
|
||||
pub const CAPABILITY: &str = "wai.route.record";
|
||||
/// The record's version in this revision (§5.1).
|
||||
/// The record's first version (§5.1), a price book of version 1's.
|
||||
pub const VERSION: u64 = 1;
|
||||
/// The second version of the price book and the record (§3.4).
|
||||
pub const VERSION_2: u64 = 2;
|
||||
/// The fewest hops a failure basis may count and still mean anything (§3.2.1).
|
||||
pub const MIN_BASIS_HOPS: u64 = 100;
|
||||
/// The most outputs an attempt states (§5.2).
|
||||
pub const MAX_OUTPUTS: usize = 1024;
|
||||
/// The content hash's domain (jwp-receipts §2.1).
|
||||
pub const CONTENT_DOMAIN: &[u8] = b"moq-jwp:object\x01";
|
||||
/// The Merkle tree's leaf and node domains (jwp-receipts §2.2), the leaf of the unlabelled form.
|
||||
pub const LEAF_DOMAIN: &[u8] = b"moq-jwp:merkle-leaf\x01";
|
||||
pub const NODE_DOMAIN: &[u8] = b"moq-jwp:merkle-node\x01";
|
||||
/// The domain the record's signature covers, before its canonical form (§5.3).
|
||||
pub const SIGN_DOMAIN: &[u8] = b"wai:route-record\x01";
|
||||
pub const KIND_REQUEST: &str = "wai-route-request";
|
||||
|
|
@ -71,6 +89,7 @@ pub enum Reason {
|
|||
ReceiptMissing,
|
||||
ReceiptInvalid,
|
||||
ReceiptMismatch,
|
||||
ReceiptOutputMismatch,
|
||||
// Reused (§8).
|
||||
Malformed,
|
||||
UnknownVersion,
|
||||
|
|
@ -80,7 +99,7 @@ pub enum Reason {
|
|||
|
||||
impl Reason {
|
||||
/// The codes this extension adds, in §8's order.
|
||||
pub const ALL: [Reason; 21] = [
|
||||
pub const ALL: [Reason; 22] = [
|
||||
Reason::RequestInvalid,
|
||||
Reason::PricebookInvalid,
|
||||
Reason::PolicyInvalid,
|
||||
|
|
@ -102,6 +121,7 @@ impl Reason {
|
|||
Reason::ReceiptMissing,
|
||||
Reason::ReceiptInvalid,
|
||||
Reason::ReceiptMismatch,
|
||||
Reason::ReceiptOutputMismatch,
|
||||
];
|
||||
|
||||
/// The codes it reuses (§8).
|
||||
|
|
@ -130,6 +150,7 @@ impl Reason {
|
|||
Reason::ReceiptMissing => "receipt_missing",
|
||||
Reason::ReceiptInvalid => "receipt_invalid",
|
||||
Reason::ReceiptMismatch => "receipt_mismatch",
|
||||
Reason::ReceiptOutputMismatch => "receipt_output_mismatch",
|
||||
Reason::Malformed => "malformed",
|
||||
Reason::UnknownVersion => "unknown_version",
|
||||
Reason::BadKey => "bad_key",
|
||||
|
|
@ -388,10 +409,54 @@ impl Request {
|
|||
}
|
||||
}
|
||||
|
||||
/// An entry's measured failures, over their basis (§3.2.1).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Failures {
|
||||
pub hops: u64,
|
||||
pub from: u64,
|
||||
pub to: u64,
|
||||
pub failed: u64,
|
||||
pub billed_price: u64,
|
||||
pub failed_ms: u64,
|
||||
}
|
||||
|
||||
/// The members of `failures` (§3.2.1).
|
||||
pub const FAILURE_MEMBERS: [&str; 6] = ["hops", "from", "to", "failed", "billed_price", "failed_ms"];
|
||||
|
||||
impl Failures {
|
||||
/// Each member of its form (§3.2.1's table): what a candidate's restated `failures` is
|
||||
/// held to at §7.2 step 4. The relations between the members are [`Failures::read`]'s.
|
||||
fn form(v: &Value) -> Option<Failures> {
|
||||
let o = exactly(v, &FAILURE_MEMBERS)?;
|
||||
Some(Failures {
|
||||
hops: int(&o["hops"]).filter(|h| *h >= 1)?,
|
||||
from: int(&o["from"])?,
|
||||
to: int(&o["to"])?,
|
||||
failed: int(&o["failed"])?,
|
||||
billed_price: int(&o["billed_price"])?,
|
||||
failed_ms: int(&o["failed_ms"])?,
|
||||
})
|
||||
}
|
||||
|
||||
/// A price book's `failures` (§3.2.1), measured no later than the book was `issued`.
|
||||
fn read(v: &Value, issued: u64) -> Option<Failures> {
|
||||
let f = Failures::form(v)?;
|
||||
let none_failed_states_nothing = f.failed > 0 || (f.billed_price == 0 && f.failed_ms == 0);
|
||||
(f.failed <= f.hops && f.from <= f.to && f.to <= issued && none_failed_states_nothing).then_some(f)
|
||||
}
|
||||
|
||||
/// Whether the basis means anything (§3.2.1): at least [`MIN_BASIS_HOPS`] hops.
|
||||
pub fn known(&self) -> bool {
|
||||
self.hops >= MIN_BASIS_HOPS
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry of a price book (§3.2).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Entry {
|
||||
pub endpoint: String,
|
||||
/// The operation it prices: `Some` in version 2, `None` in version 1.
|
||||
pub operation: Option<String>,
|
||||
pub provider: String,
|
||||
pub capability: String,
|
||||
pub region: Option<String>,
|
||||
|
|
@ -407,6 +472,8 @@ pub struct Entry {
|
|||
pub energy_uj: Option<u64>,
|
||||
pub energy_class: Option<String>,
|
||||
pub quality: Option<u64>,
|
||||
/// Its measured failures (version 2), or `None` when none were measured or in version 1.
|
||||
pub failures: Option<Failures>,
|
||||
/// The entry as the price book states it.
|
||||
pub value: Value,
|
||||
}
|
||||
|
|
@ -417,9 +484,25 @@ pub const ENTRY_MEMBERS: [&str; 16] = [
|
|||
"energy_uj", "energy_class", "quality",
|
||||
];
|
||||
|
||||
/// The members of an entry of version 2 (§3.2): version 1's, `operation` and `failures`.
|
||||
pub fn entry_members(version: u64) -> Vec<&'static str> {
|
||||
let mut m = ENTRY_MEMBERS.to_vec();
|
||||
if version == VERSION_2 {
|
||||
m.extend(["operation", "failures"]);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn from_value(v: &Value) -> Option<Entry> {
|
||||
let o = exactly(v, &ENTRY_MEMBERS)?;
|
||||
fn from_value(v: &Value, version: u64, issued: u64) -> Option<Entry> {
|
||||
let o = exactly(v, &entry_members(version))?;
|
||||
let (operation, failures) = if version == VERSION_2 {
|
||||
let op = o["operation"].as_str().filter(|s| operation(s))?.to_string();
|
||||
let f = if o["failures"].is_null() { None } else { Some(Failures::read(&o["failures"], issued)?) };
|
||||
(Some(op), f)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let energy_uj = opt_int(&o["energy_uj"])?;
|
||||
let energy_class = match &o["energy_class"] {
|
||||
Value::Null => None,
|
||||
|
|
@ -434,6 +517,7 @@ impl Entry {
|
|||
}
|
||||
Some(Entry {
|
||||
endpoint: tok(&o["endpoint"])?.to_string(),
|
||||
operation,
|
||||
provider: tok(&o["provider"])?.to_string(),
|
||||
capability: o["capability"].as_str().filter(|s| capability(s))?.to_string(),
|
||||
region: opt_tok(&o["region"])?,
|
||||
|
|
@ -449,6 +533,7 @@ impl Entry {
|
|||
energy_uj,
|
||||
energy_class,
|
||||
quality,
|
||||
failures,
|
||||
value: v.clone(),
|
||||
})
|
||||
}
|
||||
|
|
@ -457,6 +542,8 @@ impl Entry {
|
|||
/// A price book (§3.2).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PriceBook {
|
||||
/// 1 or 2 (§3.4).
|
||||
pub version: u64,
|
||||
pub currency: String,
|
||||
pub issued: u64,
|
||||
pub entries: Vec<Entry>,
|
||||
|
|
@ -472,21 +559,31 @@ impl PriceBook {
|
|||
}
|
||||
|
||||
fn from_value(v: Value) -> Option<PriceBook> {
|
||||
let o = exactly(&v, &["kind", "currency", "issued", "entries"])?;
|
||||
// A price book of version 1 has no `version` member; one of version 2 states 2 (§3.4).
|
||||
let version = match v.get("version") {
|
||||
None => VERSION,
|
||||
Some(n) => int(n).filter(|n| *n == VERSION_2)?,
|
||||
};
|
||||
let members: &[&str] = if version == VERSION_2 { &["kind", "version", "currency", "issued", "entries"] } else { &["kind", "currency", "issued", "entries"] };
|
||||
let o = exactly(&v, members)?;
|
||||
if o["kind"] != KIND_PRICEBOOK {
|
||||
return None;
|
||||
}
|
||||
let issued = int(&o["issued"])?;
|
||||
let es = o["entries"].as_array()?;
|
||||
if es.is_empty() || es.len() > 1024 {
|
||||
return None;
|
||||
}
|
||||
let entries: Vec<Entry> = es.iter().map(Entry::from_value).collect::<Option<_>>()?;
|
||||
if !entries.windows(2).all(|w| w[0].endpoint.as_bytes() < w[1].endpoint.as_bytes()) {
|
||||
let entries: Vec<Entry> = es.iter().map(|e| Entry::from_value(e, version, issued)).collect::<Option<_>>()?;
|
||||
// Version 1: by endpoint; version 2: by endpoint and then operation (`None` in version 1).
|
||||
let key = |e: &Entry| (e.endpoint.as_bytes().to_vec(), e.operation.as_ref().map(|o| o.as_bytes().to_vec()));
|
||||
if !entries.windows(2).all(|w| key(&w[0]) < key(&w[1])) {
|
||||
return None;
|
||||
}
|
||||
Some(PriceBook {
|
||||
version,
|
||||
currency: o["currency"].as_str().filter(|s| currency(s))?.to_string(),
|
||||
issued: int(&o["issued"])?,
|
||||
issued,
|
||||
entries,
|
||||
digest: digest(&v),
|
||||
doc: v,
|
||||
|
|
@ -555,18 +652,31 @@ pub struct Candidate {
|
|||
pub via_fallback: bool,
|
||||
pub cost: Option<u64>,
|
||||
pub energy: Option<u64>,
|
||||
/// Version 2: the expected cost per successful job (§4.2); `None` in version 1.
|
||||
pub expected_cost: Option<u64>,
|
||||
/// Version 2: the expected time to first byte per successful job, in ms (§4.2).
|
||||
pub expected_ttfb_ms: Option<u64>,
|
||||
/// `(limit, value)`, in §4.3's order.
|
||||
pub removed: Vec<(&'static str, Value)>,
|
||||
pub rank: Option<u64>,
|
||||
}
|
||||
|
||||
impl Candidate {
|
||||
/// Whether its entry is of a price book of version 2, which names the operation it prices.
|
||||
fn v2(&self) -> bool {
|
||||
self.entry.operation.is_some()
|
||||
}
|
||||
|
||||
/// The candidate as the record states it (§5.1).
|
||||
pub fn to_value(&self) -> Value {
|
||||
let mut o = self.entry.value.as_object().expect("an entry is an object").clone();
|
||||
o.insert("via_fallback".into(), json!(self.via_fallback));
|
||||
o.insert("cost".into(), json!(self.cost));
|
||||
o.insert("energy".into(), json!(self.energy));
|
||||
if self.v2() {
|
||||
o.insert("expected_cost".into(), json!(self.expected_cost));
|
||||
o.insert("expected_ttfb_ms".into(), json!(self.expected_ttfb_ms));
|
||||
}
|
||||
o.insert("removed".into(), Value::Array(self.removed.iter().map(|(l, v)| json!({"limit": l, "value": v})).collect()));
|
||||
o.insert("rank".into(), json!(self.rank));
|
||||
Value::Object(o)
|
||||
|
|
@ -607,6 +717,20 @@ fn estimate(q: u64, x: u64, d: u64) -> Option<u64> {
|
|||
(e <= MAX as u128).then_some(e as u64)
|
||||
}
|
||||
|
||||
/// The expected cost and time to first byte per successful job (§4.2): the list figure plus
|
||||
/// what the failures met before a success add, `failed / (hops − failed)` of them on average.
|
||||
/// `None` when the failures are unknown (none, or a thin basis), when the list figure is
|
||||
/// unknown, when every hop of the basis failed, and past 2^53 − 1.
|
||||
fn expected(q: Option<u64>, e: &Entry, cost: Option<u64>) -> (Option<u64>, Option<u64>) {
|
||||
let Some(f) = e.failures.filter(|f| f.known() && f.failed < f.hops) else { return (None, None) };
|
||||
let plus = |list: u64, per_failure: u64| {
|
||||
let t = list as u128 + (per_failure as u128) * (f.failed as u128) / ((f.hops - f.failed) as u128);
|
||||
(t <= MAX as u128).then_some(t as u64)
|
||||
};
|
||||
let failure_cost = q.and_then(|q| estimate(q, f.billed_price, e.divisor));
|
||||
(cost.zip(failure_cost).and_then(|(c, fc)| plus(c, fc)), e.ttfb_ms.and_then(|t| plus(t, f.failed_ms)))
|
||||
}
|
||||
|
||||
fn limits(r: &Request, b: &PriceBook, p: &Policy, e: &Entry, cost: Option<u64>) -> Vec<(&'static str, Value)> {
|
||||
let mut out = Vec::new();
|
||||
let not_in = |set: &Option<Vec<String>>, v: &Option<String>| set.as_ref().is_some_and(|s| v.as_ref().is_none_or(|v| !s.contains(v)));
|
||||
|
|
@ -657,13 +781,22 @@ fn nulls_last(a: Option<u64>, b: Option<u64>, rev: bool) -> Ordering {
|
|||
}
|
||||
}
|
||||
|
||||
/// The time to first byte a key reads: in version 2 the expected one per successful job (§4.4).
|
||||
fn ttfb_read(c: &Candidate) -> Option<u64> {
|
||||
if c.v2() { c.expected_ttfb_ms } else { c.entry.ttfb_ms }
|
||||
}
|
||||
|
||||
fn meets(r: &Request, c: &Candidate) -> bool {
|
||||
r.ttfb_ms.is_none_or(|t| c.entry.ttfb_ms.is_some_and(|x| x <= t)) && r.quality.is_none_or(|q| c.entry.quality.is_some_and(|x| x >= q))
|
||||
r.ttfb_ms.is_none_or(|t| ttfb_read(c).is_some_and(|x| x <= t)) && r.quality.is_none_or(|q| c.entry.quality.is_some_and(|x| x >= q))
|
||||
}
|
||||
|
||||
fn by_key(r: &Request, key: &str, a: &Candidate, b: &Candidate) -> Ordering {
|
||||
// Version 2 (§4.4): the expected figure per successful job, a `null` last, then the list one.
|
||||
let v2 = a.v2();
|
||||
match key {
|
||||
"targets" => meets(r, b).cmp(&meets(r, a)),
|
||||
"price" if v2 => nulls_last(a.expected_cost, b.expected_cost, false).then(nulls_last(a.cost, b.cost, false)),
|
||||
"ttfb" if v2 => nulls_last(a.expected_ttfb_ms, b.expected_ttfb_ms, false).then(nulls_last(a.entry.ttfb_ms, b.entry.ttfb_ms, false)),
|
||||
"price" => nulls_last(a.cost, b.cost, false),
|
||||
"ttfb" => nulls_last(a.entry.ttfb_ms, b.entry.ttfb_ms, false),
|
||||
"energy" => nulls_last(a.energy, b.energy, false),
|
||||
|
|
@ -692,6 +825,10 @@ pub fn key_list(r: &Request, p: &Policy) -> Vec<String> {
|
|||
pub fn route(r: &Request, b: &PriceBook, p: &Policy) -> Routed {
|
||||
let mut candidates: Vec<Candidate> = Vec::new();
|
||||
for e in &b.entries {
|
||||
// Version 2: only an entry of the request's operation is a candidate (§4.1).
|
||||
if e.operation.as_ref().is_some_and(|o| *o != r.operation) {
|
||||
continue;
|
||||
}
|
||||
let via_fallback = if e.capability == r.capability {
|
||||
false
|
||||
} else if r.fallback.as_deref() == Some(e.capability.as_str()) {
|
||||
|
|
@ -703,7 +840,8 @@ pub fn route(r: &Request, b: &PriceBook, p: &Policy) -> Routed {
|
|||
let cost = q.and_then(|q| estimate(q, e.price, e.divisor));
|
||||
let energy = q.and_then(|q| e.energy_uj.and_then(|x| estimate(q, x, e.divisor)));
|
||||
let removed = limits(r, b, p, e, cost);
|
||||
candidates.push(Candidate { entry: e.clone(), via_fallback, cost, energy, removed, rank: None });
|
||||
let (expected_cost, expected_ttfb_ms) = expected(q, e, cost);
|
||||
candidates.push(Candidate { entry: e.clone(), via_fallback, cost, energy, expected_cost, expected_ttfb_ms, removed, rank: None });
|
||||
}
|
||||
let key_list = key_list(r, p);
|
||||
let mut order: Vec<usize> = (0..candidates.len()).filter(|&i| candidates[i].removed.is_empty()).collect();
|
||||
|
|
@ -787,8 +925,16 @@ pub fn resign(mut body: Value, key: &SigningKey) -> Value {
|
|||
/// The route record of a job (§5): routed from the three documents, with its attempts
|
||||
/// made in the order and billed by §5.2, signed by `key`. The attempts are the router's;
|
||||
/// `seal` writes what §5 requires of them and does not check that they keep the order —
|
||||
/// [`verify`] does.
|
||||
/// [`verify`] does. A record of version 2 states the outputs of the attempt that served,
|
||||
/// which [`seal_delivered`] takes.
|
||||
pub fn seal(key: &SigningKey, r: &Request, b: &PriceBook, p: &Policy, accepted: u64, attempts: &[Attempt]) -> Value {
|
||||
seal_delivered(key, r, b, p, accepted, attempts, &[])
|
||||
}
|
||||
|
||||
/// [`seal`], writing a record of its price book's version (§3.4); in version 2 the attempt
|
||||
/// that served states `outputs`, the content hashes of what it delivered in their order
|
||||
/// (§5.2), and a failed attempt none. `outputs` is not read in version 1.
|
||||
pub fn seal_delivered(key: &SigningKey, r: &Request, b: &PriceBook, p: &Policy, accepted: u64, attempts: &[Attempt], outputs: &[[u8; 32]]) -> Value {
|
||||
let routed = route(r, b, p);
|
||||
let deadline = r.deadline_us.map(|d| accepted.saturating_add(d));
|
||||
let attempts: Vec<Value> = attempts
|
||||
|
|
@ -796,15 +942,20 @@ pub fn seal(key: &SigningKey, r: &Request, b: &PriceBook, p: &Policy, accepted:
|
|||
.enumerate()
|
||||
.map(|(i, a)| {
|
||||
let (billed, not_billed) = billing(a.error.is_none(), a.end, deadline);
|
||||
json!({
|
||||
let mut v = json!({
|
||||
"hop": i + 1, "endpoint": a.endpoint, "outcome": if a.error.is_none() { "served" } else { "failed" }, "error": a.error,
|
||||
"start": a.start, "end": a.end, "billed": billed, "not_billed": not_billed, "receipt": a.receipt,
|
||||
})
|
||||
});
|
||||
if b.version == VERSION_2 {
|
||||
let delivered: Vec<String> = if a.error.is_none() { outputs.iter().map(|o| hex(o)).collect() } else { vec![] };
|
||||
v["outputs"] = json!(delivered);
|
||||
}
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
let body = json!({
|
||||
"kind": KIND_RECORD,
|
||||
"version": VERSION,
|
||||
"version": b.version,
|
||||
"job": r.job,
|
||||
"operation": r.operation,
|
||||
"capability": r.capability,
|
||||
|
|
@ -832,14 +983,64 @@ pub const RECORD_MEMBERS: [&str; 18] = [
|
|||
/// The members of an attempt (§5.2).
|
||||
pub const ATTEMPT_MEMBERS: [&str; 9] = ["hop", "endpoint", "outcome", "error", "start", "end", "billed", "not_billed", "receipt"];
|
||||
|
||||
/// The candidate's members: an entry's and the five §5.1 adds.
|
||||
fn candidate_members() -> Vec<&'static str> {
|
||||
let mut m: Vec<&str> = ENTRY_MEMBERS.to_vec();
|
||||
/// The candidate's members: its entry's and the five §5.1 adds, and in version 2 the two
|
||||
/// expected figures.
|
||||
fn candidate_members(version: u64) -> Vec<&'static str> {
|
||||
let mut m = entry_members(version);
|
||||
m.extend(["via_fallback", "cost", "energy", "removed", "rank"]);
|
||||
if version == VERSION_2 {
|
||||
m.extend(["expected_cost", "expected_ttfb_ms"]);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// A content hash's text (§2): 64 lowercase hexadecimal digits.
|
||||
fn content_hash_text(v: &Value) -> bool {
|
||||
v.as_str().is_some_and(|s| s.len() == 64 && s.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)))
|
||||
}
|
||||
|
||||
/// An object's content hash (jwp-receipts §2.1): BLAKE3-256 over the domain and its bytes.
|
||||
pub fn content_hash(object: &[u8]) -> [u8; 32] {
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(CONTENT_DOMAIN);
|
||||
h.update(object);
|
||||
*h.finalize().as_bytes()
|
||||
}
|
||||
|
||||
/// The commitment a hop's receipt names its outputs by (oer-receipts §3 *Output*): the content
|
||||
/// hash of one, or the jwp-receipts §2.2 Merkle root over several in their order, every leaf
|
||||
/// unlabelled, an odd level padded with a copy of its last node. (No output: §2.2's empty
|
||||
/// root, all zeros; a served attempt states at least one.)
|
||||
pub fn output_commitment(outputs: &[[u8; 32]]) -> [u8; 32] {
|
||||
let b3 = |parts: &[&[u8]]| -> [u8; 32] {
|
||||
let mut h = blake3::Hasher::new();
|
||||
for p in parts {
|
||||
h.update(p);
|
||||
}
|
||||
*h.finalize().as_bytes()
|
||||
};
|
||||
match outputs {
|
||||
[] => [0; 32],
|
||||
[one] => *one,
|
||||
_ => {
|
||||
let mut level: Vec<[u8; 32]> = outputs.iter().map(|o| b3(&[LEAF_DOMAIN, o])).collect();
|
||||
while level.len() > 1 {
|
||||
if !level.len().is_multiple_of(2) {
|
||||
level.push(*level.last().expect("a node"));
|
||||
}
|
||||
level = level.as_chunks::<2>().0.iter().map(|[l, r]| b3(&[NODE_DOMAIN, l, r])).collect();
|
||||
}
|
||||
level[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_form(v: &Value) -> bool {
|
||||
let version = v.get("version").and_then(int).unwrap_or(0);
|
||||
let mut attempt_members = ATTEMPT_MEMBERS.to_vec();
|
||||
if version == VERSION_2 {
|
||||
attempt_members.push("outputs");
|
||||
}
|
||||
let Some(o) = exactly(v, &RECORD_MEMBERS) else { return false };
|
||||
let hexn = |v: &Value, n: usize| v.as_str().is_some_and(|s| s.len() == n && s.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)));
|
||||
let cap_or_null = |v: &Value| v.is_null() || v.as_str().is_some_and(capability);
|
||||
|
|
@ -848,8 +1049,13 @@ fn record_form(v: &Value) -> bool {
|
|||
let tok_or_null = |v: &Value| v.is_null() || tok(v).is_some();
|
||||
let scalar = |v: &Value| !(v.is_array() || v.is_object());
|
||||
let candidate = |c: &Value| -> bool {
|
||||
let Some(c) = exactly(c, &candidate_members()) else { return false };
|
||||
tok(&c["endpoint"]).is_some()
|
||||
let Some(c) = exactly(c, &candidate_members(version)) else { return false };
|
||||
let v2 = version != VERSION_2
|
||||
|| (c["operation"].as_str().is_some_and(operation)
|
||||
&& (c["failures"].is_null() || Failures::form(&c["failures"]).is_some())
|
||||
&& int_or_null(&c["expected_cost"])
|
||||
&& int_or_null(&c["expected_ttfb_ms"]));
|
||||
v2 && tok(&c["endpoint"]).is_some()
|
||||
&& tok(&c["provider"]).is_some()
|
||||
&& c["capability"].as_str().is_some_and(capability)
|
||||
&& tok_or_null(&c["region"])
|
||||
|
|
@ -875,14 +1081,22 @@ fn record_form(v: &Value) -> bool {
|
|||
&& (c["rank"].is_null() || int(&c["rank"]).is_some_and(|n| n >= 1))
|
||||
};
|
||||
let attempt = |i: usize, a: &Value| -> bool {
|
||||
let Some(a) = exactly(a, &ATTEMPT_MEMBERS) else { return false };
|
||||
let Some(a) = exactly(a, &attempt_members) else { return false };
|
||||
let served = match a["outcome"].as_str() {
|
||||
Some("served") => true,
|
||||
Some("failed") => false,
|
||||
_ => return false,
|
||||
};
|
||||
let error_ok = if served { a["error"].is_null() } else { a["error"].as_str().is_some_and(|e| ERRORS.contains(&e)) };
|
||||
int(&a["hop"]) == Some(i as u64 + 1)
|
||||
// Version 2 (§5.2): a served attempt's outputs, 1 to 1024 content hashes, no two the
|
||||
// same; a failed attempt's, none.
|
||||
let outputs_ok = version != VERSION_2
|
||||
|| a["outputs"].as_array().is_some_and(|xs| {
|
||||
let distinct = xs.iter().filter_map(Value::as_str).collect::<std::collections::BTreeSet<_>>().len() == xs.len();
|
||||
xs.iter().all(content_hash_text) && distinct && if served { (1..=MAX_OUTPUTS).contains(&xs.len()) } else { xs.is_empty() }
|
||||
});
|
||||
outputs_ok
|
||||
&& int(&a["hop"]) == Some(i as u64 + 1)
|
||||
&& tok(&a["endpoint"]).is_some()
|
||||
&& error_ok
|
||||
&& int(&a["start"]).is_some()
|
||||
|
|
@ -891,7 +1105,7 @@ fn record_form(v: &Value) -> bool {
|
|||
&& (a["not_billed"].is_null() || a["not_billed"].as_str().is_some_and(|x| x == "failed" || x == "deadline_missed"))
|
||||
&& a["receipt"].as_str().is_some_and(receipt_id)
|
||||
};
|
||||
int(&o["version"]) == Some(VERSION)
|
||||
(version == VERSION || version == VERSION_2)
|
||||
&& tok(&o["job"]).is_some()
|
||||
&& o["operation"].as_str().is_some_and(operation)
|
||||
&& o["capability"].as_str().is_some_and(capability)
|
||||
|
|
@ -923,6 +1137,9 @@ pub struct Accepted {
|
|||
pub operation: String,
|
||||
/// `(endpoint, hop)` of the attempt that served.
|
||||
pub served: Option<(String, u64)>,
|
||||
/// In a record of version 2, the content hashes of the served attempt's outputs, in their
|
||||
/// order; `None` in version 1 and for a job no hop served.
|
||||
pub outputs: Option<Vec<String>>,
|
||||
pub billed: Option<u64>,
|
||||
/// `none`, `met` or `missed`.
|
||||
pub deadline: &'static str,
|
||||
|
|
@ -988,6 +1205,7 @@ pub fn verify_document(d: &Document<'_>) -> Result<Accepted, Refusal> {
|
|||
}
|
||||
if let Some(v) = rec.get("version").and_then(int)
|
||||
&& v != VERSION
|
||||
&& v != VERSION_2
|
||||
{
|
||||
return Err(Refusal::new(Reason::UnknownVersion, at(&[json!("record"), json!("version")])));
|
||||
}
|
||||
|
|
@ -1022,6 +1240,9 @@ pub fn verify_document(d: &Document<'_>) -> Result<Accepted, Refusal> {
|
|||
if rec["policy"] != json!({"name": p.name, "version": p.version, "digest": hex(&p.digest)}) {
|
||||
return Err(Refusal::new(Reason::PolicyMismatch, field("policy")));
|
||||
}
|
||||
if rec["version"] != json!(b.version) {
|
||||
return Err(Refusal::new(Reason::PricebookMismatch, field("version")));
|
||||
}
|
||||
if rec["pricebook"] != json!({"digest": hex(&b.digest), "currency": b.currency, "issued": b.issued}) {
|
||||
return Err(Refusal::new(Reason::PricebookMismatch, field("pricebook")));
|
||||
}
|
||||
|
|
@ -1098,6 +1319,9 @@ pub fn verify_document(d: &Document<'_>) -> Result<Accepted, Refusal> {
|
|||
_ => None,
|
||||
};
|
||||
let served = served_hop.map(|a| (a["endpoint"].as_str().expect("a token").to_string(), a["hop"].as_u64().expect("a hop")));
|
||||
let outputs = served_hop
|
||||
.and_then(|a| a.get("outputs"))
|
||||
.map(|xs| xs.as_array().expect("a list").iter().map(|x| x.as_str().expect("a content hash").to_string()).collect());
|
||||
let billed = attempts.iter().find(|a| a["billed"] == true).map(|a| a["hop"].as_u64().expect("a hop"));
|
||||
let deadline_verdict = match (deadline, served_hop) {
|
||||
(Some(dl), Some(a)) => if a["end"].as_u64().expect("a time") > dl { "missed" } else { "met" },
|
||||
|
|
@ -1108,6 +1332,7 @@ pub fn verify_document(d: &Document<'_>) -> Result<Accepted, Refusal> {
|
|||
job: r.job.clone(),
|
||||
operation: r.operation.clone(),
|
||||
served,
|
||||
outputs,
|
||||
billed,
|
||||
deadline: deadline_verdict,
|
||||
order: routed.order_endpoints(),
|
||||
|
|
@ -1142,6 +1367,14 @@ fn check_receipts(rec: &Value, texts: &[&str], keys: &Map<String, Value>) -> Res
|
|||
{
|
||||
return Err(Refusal::new(Reason::ReceiptMismatch, here));
|
||||
}
|
||||
// Version 2: the served hop's receipt names the outputs the attempt states (§6).
|
||||
if let Some(xs) = a.get("outputs").and_then(Value::as_array).filter(|_| a["outcome"] == "served") {
|
||||
let outs: Vec<[u8; 32]> = xs.iter().map(|x| unhex(x.as_str().expect("a content hash")).expect("a content hash")).collect();
|
||||
let named = r.get("subject").and_then(|s| s.get("output")).and_then(Value::as_str);
|
||||
if named != Some(hex(&output_commitment(&outs)).as_str()) {
|
||||
return Err(Refusal::new(Reason::ReceiptOutputMismatch, here));
|
||||
}
|
||||
}
|
||||
if crate::oer::is_unmetered(r) {
|
||||
unmetered.push(a["hop"].as_u64().expect("a hop"));
|
||||
}
|
||||
|
|
@ -1158,7 +1391,10 @@ pub fn outcome_json(r: &Result<Accepted, Refusal>) -> Value {
|
|||
"digest": hex(&a.digest),
|
||||
"job": a.job,
|
||||
"operation": a.operation,
|
||||
"served": a.served.as_ref().map(|(e, h)| json!({"endpoint": e, "hop": h})),
|
||||
"served": a.served.as_ref().map(|(e, h)| match &a.outputs {
|
||||
Some(o) => json!({"endpoint": e, "hop": h, "outputs": o}),
|
||||
None => json!({"endpoint": e, "hop": h}),
|
||||
}),
|
||||
"billed": a.billed,
|
||||
"deadline": a.deadline,
|
||||
"order": a.order,
|
||||
|
|
@ -1210,6 +1446,88 @@ mod tests {
|
|||
assert_eq!(nulls_last(Some(900), Some(800), true), Ordering::Less);
|
||||
}
|
||||
|
||||
fn entry_with(failures: Option<Failures>, ttfb_ms: Option<u64>) -> Entry {
|
||||
let v = json!({});
|
||||
Entry {
|
||||
endpoint: "e".into(),
|
||||
operation: Some("media.encode".into()),
|
||||
provider: "p".into(),
|
||||
capability: "wai.video.int_motion".into(),
|
||||
region: None,
|
||||
residency: None,
|
||||
trains_on_inputs: None,
|
||||
retention_days: None,
|
||||
ga: None,
|
||||
needs_consent: None,
|
||||
unit: "subpixels".into(),
|
||||
divisor: 1_000_000,
|
||||
price: 30_000,
|
||||
ttfb_ms,
|
||||
energy_uj: None,
|
||||
energy_class: None,
|
||||
quality: None,
|
||||
failures,
|
||||
value: v,
|
||||
}
|
||||
}
|
||||
|
||||
fn basis(hops: u64, failed: u64, billed_price: u64, failed_ms: u64) -> Option<Failures> {
|
||||
Some(Failures { hops, from: 0, to: 0, failed, billed_price, failed_ms })
|
||||
}
|
||||
|
||||
/// §4.2: the list figure plus `failed / (hops − failed)` failures' worth, each floored once.
|
||||
#[test]
|
||||
fn the_expected_figures_count_the_failures_met_before_a_success() {
|
||||
let q = Some(34_560_000);
|
||||
let cost = Some(1_036_800);
|
||||
// A fifth of the hops failed, each billed in full and 2 s long: a quarter of a failure per
|
||||
// successful job.
|
||||
assert_eq!(expected(q, &entry_with(basis(1_000, 200, 30_000, 2_000), Some(200)), cost), (Some(1_296_000), Some(700)));
|
||||
// 30% failed: 3/7 of a failure per success, floored once over the whole term.
|
||||
assert_eq!(expected(q, &entry_with(basis(1_000, 300, 27_000, 1_000), Some(220)), Some(933_120)), (Some(933_120 + 399_908), Some(220 + 428)));
|
||||
// None failed: the list figures, exactly.
|
||||
assert_eq!(expected(q, &entry_with(basis(1_000, 0, 0, 0), Some(300)), cost), (cost, Some(300)));
|
||||
// Unbilled failures cost the router nothing and the job their time.
|
||||
assert_eq!(expected(q, &entry_with(basis(1_000, 500, 0, 10_000), Some(100)), cost), (cost, Some(10_100)));
|
||||
}
|
||||
|
||||
/// §3.2.1 and §4.2: a basis under 100 hops, no failures stated, and every hop failed give no
|
||||
/// expected figure; nor does an unknown list figure, or one past the subset.
|
||||
#[test]
|
||||
fn an_unknown_or_unbounded_expectation_is_null() {
|
||||
let q = Some(34_560_000);
|
||||
let cost = Some(1_036_800);
|
||||
assert_eq!(expected(q, &entry_with(basis(99, 0, 0, 0), Some(300)), cost), (None, None));
|
||||
assert_eq!(expected(q, &entry_with(basis(100, 0, 0, 0), Some(300)), cost), (cost, Some(300)));
|
||||
assert_eq!(expected(q, &entry_with(None, Some(300)), cost), (None, None));
|
||||
assert_eq!(expected(q, &entry_with(basis(500, 500, 0, 30_000), Some(250)), cost), (None, None));
|
||||
assert_eq!(expected(q, &entry_with(basis(1_000, 0, 0, 0), None), None), (None, None));
|
||||
assert_eq!(expected(Some(MAX), &entry_with(basis(1_000, 1, 2_000_000, 0), Some(1)), Some(1)), (None, Some(1)), "the failure's cost past the subset");
|
||||
assert_eq!(expected(q, &entry_with(basis(1_000, 999, 0, MAX), Some(1)), cost), (cost, None), "the time past the subset");
|
||||
}
|
||||
|
||||
/// oer-receipts §3 *Output*: one output is its own content hash; several, jwp-receipts §2.2's
|
||||
/// root over unlabelled leaves, an odd level padded with its last node.
|
||||
#[test]
|
||||
fn the_output_commitment_is_the_content_hash_or_the_group_root() {
|
||||
let b3 = |parts: &[&[u8]]| -> [u8; 32] {
|
||||
let mut h = blake3::Hasher::new();
|
||||
parts.iter().for_each(|p| {
|
||||
h.update(p);
|
||||
});
|
||||
*h.finalize().as_bytes()
|
||||
};
|
||||
let (a, b, c) = (content_hash(b"a"), content_hash(b"b"), content_hash(b"c"));
|
||||
assert_eq!(a, b3(&[b"moq-jwp:object\x01", b"a"]));
|
||||
assert_eq!(output_commitment(&[a]), a);
|
||||
let leaf = |x: &[u8; 32]| b3(&[b"moq-jwp:merkle-leaf\x01", x]);
|
||||
let node = |x: &[u8; 32], y: &[u8; 32]| b3(&[b"moq-jwp:merkle-node\x01", x, y]);
|
||||
assert_eq!(output_commitment(&[a, b]), node(&leaf(&a), &leaf(&b)));
|
||||
assert_eq!(output_commitment(&[a, b, c]), node(&node(&leaf(&a), &leaf(&b)), &node(&leaf(&c), &leaf(&c))));
|
||||
assert_ne!(output_commitment(&[a, b]), output_commitment(&[b, a]), "the order is committed");
|
||||
assert_eq!(output_commitment(&[]), [0; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_8_lists_exactly_the_reasons() {
|
||||
let text = include_str!("../../extensions/route-record.md");
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
//! defect is not in the signature, and each declares the reason and the place it must be
|
||||
//! refused at: [`generate`] refuses to write a corpus whose control reaches another, so a
|
||||
//! control cannot be mistaken for the defect it names. The hop receipts are OER/2 receipts
|
||||
//! of minor version 1 (oer-receipts §3.1), sealed here under the published hop key.
|
||||
//! of minor version 1 (oer-receipts §3.1), sealed here under the published hop key. The
|
||||
//! vectors of version 2 (§3.4) follow those of version 1, which are unchanged but for the
|
||||
//! control of an unknown version.
|
||||
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use serde_json::{json, Value};
|
||||
|
|
@ -406,7 +408,7 @@ fn vectors() -> Vec<Vector> {
|
|||
let mut req_float = d.clone();
|
||||
req_float["request"] = json!(canonical(&b.request).replacen("\"deadline_us\":30000000", "\"deadline_us\":3e7", 1));
|
||||
v.push(refuse("request_with_an_exponent", req_float, "malformed", json!(["request"])));
|
||||
v.push(refuse("record_version_2", b.edited(|r| r["version"] = json!(2)).document(), "unknown_version", rec(&[json!("version")])));
|
||||
v.push(refuse("record_version_3", b.edited(|r| r["version"] = json!(3)).document(), "unknown_version", rec(&[json!("version")])));
|
||||
|
||||
// ── refused: the documents ──
|
||||
let mut x = d.clone();
|
||||
|
|
@ -465,7 +467,7 @@ fn vectors() -> Vec<Vector> {
|
|||
let opus = {
|
||||
let pb = PriceBook::read(&canonical(&pricebook())).unwrap();
|
||||
let e = pb.entries.iter().find(|e| e.endpoint == "opus.alpha").unwrap().clone();
|
||||
super::Candidate { entry: e, via_fallback: false, cost: Some(4_000), energy: None, removed: vec![], rank: None }.to_value()
|
||||
super::Candidate { entry: e, via_fallback: false, cost: Some(4_000), energy: None, expected_cost: None, expected_ttfb_ms: None, removed: vec![], rank: None }.to_value()
|
||||
};
|
||||
v.push(refuse("another_capability_listed", b.edited(|r| r["candidates"].as_array_mut().unwrap().push(opus)).document(), "candidate_mismatch", rec(&[json!("candidates"), json!(8)])));
|
||||
v.push(refuse("a_removal_left_out", b.edited(|r| { r["candidates"][6]["removed"].as_array_mut().unwrap().remove(1); }).document(), "removal_mismatch", rec(&[json!("candidates"), json!(6)])));
|
||||
|
|
@ -560,7 +562,7 @@ fn vectors() -> Vec<Vector> {
|
|||
/// The corpus, as `vectors.json` holds it.
|
||||
pub fn generate() -> Result<String, String> {
|
||||
let mut out = Vec::new();
|
||||
for vec in vectors() {
|
||||
for vec in vectors().into_iter().chain(vectors_2()) {
|
||||
let got = outcome_json(&verify(&vec.document));
|
||||
if let Some((reason, at)) = &vec.expect
|
||||
&& (got["outcome"] != "refuse" || got["reason"] != *reason || got["at"] != *at)
|
||||
|
|
@ -599,3 +601,290 @@ pub fn verify_corpus(text: &str) -> Vec<String> {
|
|||
}
|
||||
fails
|
||||
}
|
||||
|
||||
// ───────────────────────────── version 2 (§3.4) ─────────────────────────────
|
||||
|
||||
/// When version 2's price book was issued, and its failures measured: the 30 days before.
|
||||
const ISSUED: u64 = ACCEPTED - 3_600_000_000;
|
||||
const MEASURED_FROM: u64 = ISSUED - 30 * 86_400 * 1_000_000;
|
||||
const ENC: &str = "media.encode";
|
||||
const DEC: &str = "media.decode";
|
||||
|
||||
/// An entry's failures over the 30 days before the book was issued (§3.2.1).
|
||||
fn failures(hops: u64, failed: u64, billed_price: u64, failed_ms: u64) -> Value {
|
||||
json!({"hops": hops, "from": MEASURED_FROM, "to": ISSUED, "failed": failed, "billed_price": billed_price, "failed_ms": failed_ms})
|
||||
}
|
||||
|
||||
/// An entry of version 2: one operation of an endpoint, with its failures.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn entry_2(
|
||||
endpoint: &str,
|
||||
operation: &str,
|
||||
provider: &str,
|
||||
cap: &str,
|
||||
place: (Option<&str>, Option<&str>),
|
||||
price: (&str, u64, u64),
|
||||
ttfb_ms: Option<u64>,
|
||||
energy: Option<(u64, &str)>,
|
||||
quality: Option<u64>,
|
||||
failures: Value,
|
||||
) -> Value {
|
||||
let mut e = entry(endpoint, provider, cap, place, CLEAN, price, ttfb_ms, energy, quality);
|
||||
e["operation"] = json!(operation);
|
||||
e["failures"] = failures;
|
||||
e
|
||||
}
|
||||
|
||||
/// The entries of version 2. Priced per 10^6 subpixels, a job of 34,560,000 costs 34.56 × the
|
||||
/// price. `im.alpha` prices its encode and its decode apart. Of the encodes: `im.alpha` never
|
||||
/// failed in 1,000 hops; `im.beta` has its list price and fails a fifth of the time, each failure
|
||||
/// billed in full; `im.zeta` is cheaper and fails 30%, billed; `im.eta` fails half the time,
|
||||
/// unbilled but slowly; `im.iota`'s basis is exactly 100 hops and `im.delta`'s 99, too thin;
|
||||
/// `im.epsilon` failed every hop; `im.gamma`, the cheapest, was never measured; `im.theta` is
|
||||
/// the dearest and the fastest that never fails.
|
||||
fn entries_2() -> Vec<Value> {
|
||||
let sp = |p: u64| ("subpixels", 1_000_000, p);
|
||||
vec![
|
||||
entry_2("av1.alpha", ENC, "prov-alpha", AV1, EU_W, ("seconds", 1, 120_000_000), Some(900), Some((2_500_000, "ModelBased")), Some(820), Value::Null),
|
||||
entry_2("im.alpha", DEC, "prov-alpha", IM, EU_C, sp(12_000), Some(150), Some((300_000, "OnChipCounter")), Some(950), failures(1_000, 10, 0, 500)),
|
||||
entry_2("im.alpha", ENC, "prov-alpha", IM, EU_C, sp(30_000), Some(300), Some((900_000, "OnChipCounter")), Some(950), failures(1_000, 0, 0, 0)),
|
||||
entry_2("im.beta", DEC, "prov-beta", IM, EU_W, sp(10_000), Some(100), None, Some(900), Value::Null),
|
||||
entry_2("im.beta", ENC, "prov-beta", IM, EU_W, sp(30_000), Some(200), None, Some(950), failures(1_000, 200, 30_000, 2_000)),
|
||||
entry_2("im.delta", ENC, "prov-delta", IM, EU_W, sp(28_000), Some(250), None, Some(900), failures(99, 0, 0, 0)),
|
||||
entry_2("im.epsilon", ENC, "prov-epsilon", IM, EU_W, sp(26_000), Some(250), None, Some(900), failures(500, 500, 0, 30_000)),
|
||||
entry_2("im.eta", ENC, "prov-eta", IM, EU_W, sp(30_500), Some(100), None, Some(950), failures(1_000, 500, 0, 10_000)),
|
||||
entry_2("im.gamma", ENC, "prov-gamma", IM, EU_W, sp(25_000), Some(150), None, Some(900), Value::Null),
|
||||
entry_2("im.iota", ENC, "prov-iota", IM, EU_W, sp(31_000), Some(280), None, Some(900), failures(100, 0, 0, 0)),
|
||||
entry_2("im.theta", ENC, "prov-theta", IM, EU_W, sp(40_000), Some(240), None, Some(900), failures(1_000, 0, 0, 0)),
|
||||
entry_2("im.zeta", ENC, "prov-zeta", IM, EU_W, sp(27_000), Some(220), None, Some(900), failures(1_000, 300, 27_000, 1_000)),
|
||||
]
|
||||
}
|
||||
|
||||
fn pricebook_2_of(entries: Vec<Value>) -> Value {
|
||||
json!({"kind": "wai-route-pricebook", "version": 2, "currency": "EUR", "issued": ISSUED, "entries": entries})
|
||||
}
|
||||
|
||||
fn pricebook_2() -> Value {
|
||||
pricebook_2_of(entries_2())
|
||||
}
|
||||
|
||||
/// A request of version 2's jobs: version 1's under `job` and `operation`, with no unit-price
|
||||
/// cap, so every entry of its operation survives its limits.
|
||||
fn request_2(job: &str, operation: &str, prefer: Value) -> Value {
|
||||
let mut r = request();
|
||||
r["job"] = json!(job);
|
||||
r["operation"] = json!(operation);
|
||||
r["limits"]["max_price"] = json!([]);
|
||||
r["prefer"] = prefer;
|
||||
r
|
||||
}
|
||||
|
||||
fn no_preference() -> Value {
|
||||
json!({"sort": null, "ttfb_ms": null, "quality": null})
|
||||
}
|
||||
|
||||
/// What a job delivered: the content hashes of `n` fixed byte strings, standing for its envelopes
|
||||
/// (the verifier reads only their content hashes).
|
||||
fn delivered(job: &str, n: usize) -> Vec<[u8; 32]> {
|
||||
(0..n).map(|i| super::content_hash(format!("wai route corpus: {job}, output {i}").as_bytes())).collect()
|
||||
}
|
||||
|
||||
/// A receipt naming `outputs` by their commitment (oer-receipts §3 *Output*), sealed again.
|
||||
fn naming(mut r: Value, output: String) -> Value {
|
||||
r["subject"]["output"] = json!(output);
|
||||
reseal_receipt(&mut r);
|
||||
r
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// A job of version 2: its hops' receipts bound to the job as version 1's are, the one that
|
||||
/// served naming `outputs`; its record sealed with them.
|
||||
fn new_2(request: Value, pricebook: Value, policy: Value, attempts: &[(&str, Option<&'static str>, u64, u64, Value)], outputs: &[[u8; 32]], receipts: bool) -> Job {
|
||||
let r = Request::read(&canonical(&request)).expect("the request reads");
|
||||
let b = PriceBook::read(&canonical(&pricebook)).expect("the price book reads");
|
||||
let p = Policy::read(&canonical(&policy)).expect("the policy reads");
|
||||
let commitment = hex(&super::output_commitment(outputs));
|
||||
let bound: Vec<Value> = attempts
|
||||
.iter()
|
||||
.map(|(e, err, _, _, rc)| {
|
||||
let mut rc = rc.clone();
|
||||
rc["subject"]["inputs"] = json!([sha(&format!("{}/{e}", r.job))]);
|
||||
if err.is_none() {
|
||||
rc["subject"]["output"] = json!(commitment);
|
||||
}
|
||||
reseal_receipt(&mut rc);
|
||||
rc
|
||||
})
|
||||
.collect();
|
||||
let atts: Vec<Attempt> = attempts
|
||||
.iter()
|
||||
.zip(&bound)
|
||||
.map(|((e, err, s, t, _), rc)| Attempt { endpoint: e.to_string(), error: *err, start: ACCEPTED + s, end: ACCEPTED + t, receipt: id(rc) })
|
||||
.collect();
|
||||
let record = super::seal_delivered(&router(), &r, &b, &p, ACCEPTED, &atts, outputs);
|
||||
Job { request, pricebook, policy, record, receipts: receipts.then_some(bound) }
|
||||
}
|
||||
|
||||
/// The same job with the receipt of attempt `i` replaced by `receipt`, which the record names.
|
||||
fn with_receipt(&self, i: usize, receipt: Value) -> Job {
|
||||
let rid = id(&receipt);
|
||||
let mut j = self.edited(|r| r["attempts"][i]["receipt"] = json!(rid));
|
||||
if let Some(rs) = j.receipts.as_mut() {
|
||||
rs[i] = receipt;
|
||||
}
|
||||
j
|
||||
}
|
||||
}
|
||||
|
||||
fn vectors_2() -> Vec<Vector> {
|
||||
let mut v = Vec::new();
|
||||
let rec = |p: &[Value]| {
|
||||
let mut a = vec![json!("record")];
|
||||
a.extend_from_slice(p);
|
||||
Value::Array(a)
|
||||
};
|
||||
|
||||
// ── accepted ──
|
||||
// Ranked by the expected cost: `im.alpha` first; `im.zeta`, cheaper by its list price, after
|
||||
// `im.beta`, which its failures make dearer than `im.alpha` at the same list price; the
|
||||
// unmeasured `im.gamma`, the cheapest, and the thin and the failed bases after every measured
|
||||
// endpoint, by their list costs.
|
||||
let b = Job::new_2(request_2("job-2001", ENC, no_preference()), pricebook_2(), policy(3), &[("im.alpha", None, 1_000, 4_000_000, metered("im.alpha"))], &delivered("job-2001", 1), true);
|
||||
v.push(accept("v2_ranked_by_the_expected_cost", &b));
|
||||
let decode = Job::new_2(
|
||||
{
|
||||
let mut r = request_2("job-2002", DEC, no_preference());
|
||||
r["limits"]["max_price"] = json!([{"unit": "subpixels", "price": 20_000, "divisor": 1_000_000}]);
|
||||
r
|
||||
},
|
||||
pricebook_2(),
|
||||
policy(3),
|
||||
&[("im.alpha", None, 1_000, 2_000_000, receipt(DEC, "im.alpha", 1, Some(("4.147", counter(), "measured"))))],
|
||||
&delivered("job-2002", 1),
|
||||
true,
|
||||
);
|
||||
v.push(accept("v2_a_decode_at_its_own_price", &decode));
|
||||
let fo = Job::new_2(
|
||||
request_2("job-2003", ENC, no_preference()),
|
||||
pricebook_2(),
|
||||
policy(3),
|
||||
&[("im.alpha", Some("server_error"), 1_000, 2_000_000, failed("im.alpha")), ("im.eta", None, 2_000_100, 9_000_000, model("im.eta"))],
|
||||
&delivered("job-2003", 1),
|
||||
true,
|
||||
);
|
||||
v.push(accept("v2_failover_whose_failed_hop_delivered_nothing", &fo));
|
||||
let three = Job::new_2(request_2("job-2004", ENC, no_preference()), pricebook_2(), policy(3), &[("im.alpha", None, 1_000, 4_000_000, metered("im.alpha"))], &delivered("job-2004", 3), true);
|
||||
v.push(accept("v2_served_three_outputs", &three));
|
||||
v.push(accept(
|
||||
"v2_ranked_by_the_expected_time_to_first_byte",
|
||||
&Job::new_2(request_2("job-2005", ENC, json!({"sort": "ttfb", "ttfb_ms": null, "quality": null})), pricebook_2(), policy(3), &[("im.theta", None, 1_000, 4_000_000, metered("im.theta"))], &delivered("job-2005", 1), true),
|
||||
));
|
||||
v.push(accept(
|
||||
"v2_the_targets_read_the_expected_time_to_first_byte",
|
||||
&Job::new_2(request_2("job-2006", ENC, json!({"sort": null, "ttfb_ms": 250, "quality": null})), pricebook_2(), policy(3), &[("im.theta", None, 1_000, 4_000_000, metered("im.theta"))], &delivered("job-2006", 1), true),
|
||||
));
|
||||
let mut ceiling = request_2("job-2007", ENC, no_preference());
|
||||
ceiling["limits"]["spend_ceiling"]["amount"] = json!(1_300_000);
|
||||
v.push(accept("v2_the_ceiling_reads_the_list_cost", &Job::new_2(ceiling, pricebook_2(), policy(3), &[("im.alpha", None, 1_000, 4_000_000, metered("im.alpha"))], &delivered("job-2007", 1), true)));
|
||||
let unmeasured = pricebook_2_of(
|
||||
entries_2()
|
||||
.into_iter()
|
||||
.map(|mut e| {
|
||||
e["failures"] = Value::Null;
|
||||
e
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
v.push(accept(
|
||||
"v2_no_failure_measured_ranks_by_the_list_cost",
|
||||
&Job::new_2(request_2("job-2008", ENC, no_preference()), unmeasured, policy(3), &[("im.gamma", None, 1_000, 4_000_000, metered("im.gamma"))], &delivered("job-2008", 1), true),
|
||||
));
|
||||
let late = Job::new_2(request_2("job-2010", ENC, no_preference()), pricebook_2(), policy(3), &[("im.alpha", None, 1_000, 31_000_000, metered("im.alpha"))], &delivered("job-2010", 1), true);
|
||||
v.push(accept("v2_served_after_the_deadline", &late));
|
||||
v.push(accept(
|
||||
"v2_receipts_not_checked",
|
||||
&Job::new_2(request_2("job-2001", ENC, no_preference()), pricebook_2(), policy(3), &[("im.alpha", None, 1_000, 4_000_000, metered("im.alpha"))], &delivered("job-2001", 1), false),
|
||||
));
|
||||
|
||||
// ── refused: the price book ──
|
||||
let d = b.document();
|
||||
let book = |name: &'static str, edit: &dyn Fn(&mut Value)| {
|
||||
let mut x = d.clone();
|
||||
set(&mut x, "pricebook", |p| edit(p));
|
||||
refuse(name, x, "pricebook_invalid", json!(["pricebook"]))
|
||||
};
|
||||
v.push(book("v2_price_book_out_of_order_by_operation", &|p| p["entries"].as_array_mut().unwrap().swap(1, 2)));
|
||||
v.push(book("v2_an_entry_without_its_operation", &|p| {
|
||||
p["entries"][2].as_object_mut().unwrap().remove("operation");
|
||||
}));
|
||||
v.push(book("v2_an_entry_without_its_failures", &|p| {
|
||||
p["entries"][2].as_object_mut().unwrap().remove("failures");
|
||||
}));
|
||||
v.push(book("price_book_version_3", &|p| p["version"] = json!(3)));
|
||||
v.push(book("v2_more_hops_failed_than_were_sent", &|p| p["entries"][4]["failures"]["failed"] = json!(1_001)));
|
||||
v.push(book("v2_failures_measured_after_the_price_book_was_issued", &|p| p["entries"][4]["failures"]["to"] = json!(ISSUED + 1)));
|
||||
v.push(book("v2_failures_over_a_reversed_period", &|p| {
|
||||
p["entries"][4]["failures"]["from"] = json!(ISSUED);
|
||||
p["entries"][4]["failures"]["to"] = json!(ISSUED - 1);
|
||||
}));
|
||||
v.push(book("v2_a_billed_price_with_no_failed_hop", &|p| p["entries"][2]["failures"]["billed_price"] = json!(1)));
|
||||
v.push(book("v2_a_failure_time_with_no_failed_hop", &|p| p["entries"][2]["failures"]["failed_ms"] = json!(1)));
|
||||
v.push(book("v2_failures_of_no_hop", &|p| p["entries"][2]["failures"]["hops"] = json!(0)));
|
||||
let mut x = base().document();
|
||||
set(&mut x, "pricebook", |p| p["entries"][2]["operation"] = json!(ENC));
|
||||
v.push(refuse("an_operation_in_a_price_book_of_version_1", x, "pricebook_invalid", json!(["pricebook"])));
|
||||
|
||||
// ── refused: the record's version against its price book's ──
|
||||
let digest_of = |p: &Value| json!({"digest": hex(&super::digest(p)), "currency": "EUR", "issued": p["issued"]});
|
||||
let mut one_on_two = base().edited(|r| r["pricebook"] = digest_of(&pricebook_2()));
|
||||
one_on_two.pricebook = pricebook_2();
|
||||
v.push(refuse("a_record_of_version_1_on_a_price_book_of_version_2", one_on_two.document(), "pricebook_mismatch", rec(&[json!("version")])));
|
||||
let mut two_on_one = b.edited(|r| r["pricebook"] = digest_of(&pricebook()));
|
||||
two_on_one.pricebook = pricebook();
|
||||
v.push(refuse("a_record_of_version_2_on_a_price_book_of_version_1", two_on_one.document(), "pricebook_mismatch", rec(&[json!("version")])));
|
||||
|
||||
// ── refused: the candidates ──
|
||||
v.push(refuse("v2_an_expected_cost_restated_as_the_list_cost", b.edited(|r| r["candidates"][9]["expected_cost"] = json!(933_120)).document(), "candidate_mismatch", rec(&[json!("candidates"), json!(9)])));
|
||||
v.push(refuse("v2_an_expected_time_restated_as_the_list_time", b.edited(|r| r["candidates"][2]["expected_ttfb_ms"] = json!(200)).document(), "candidate_mismatch", rec(&[json!("candidates"), json!(2)])));
|
||||
v.push(refuse("v2_a_failure_basis_restated", b.edited(|r| r["candidates"][2]["failures"]["hops"] = json!(10_000)).document(), "candidate_mismatch", rec(&[json!("candidates"), json!(2)])));
|
||||
let other_operation = {
|
||||
let pb = PriceBook::read(&canonical(&pricebook_2())).unwrap();
|
||||
let e = pb.entries.iter().find(|e| e.endpoint == "im.alpha" && e.operation.as_deref() == Some(DEC)).unwrap().clone();
|
||||
super::Candidate { entry: e, via_fallback: false, cost: Some(414_720), energy: Some(10_368_000), expected_cost: Some(414_720), expected_ttfb_ms: Some(155), removed: vec![], rank: None }.to_value()
|
||||
};
|
||||
v.push(refuse(
|
||||
"v2_the_other_operations_entry_listed",
|
||||
b.edited(|r| r["candidates"].as_array_mut().unwrap().insert(1, other_operation)).document(),
|
||||
"candidate_mismatch",
|
||||
rec(&[json!("candidates"), json!(1)]),
|
||||
));
|
||||
|
||||
// ── refused: the outputs' form ──
|
||||
v.push(refuse("v2_served_with_no_output", b.edited(|r| r["attempts"][0]["outputs"] = json!([])).document(), "record_invalid", json!(["record"])));
|
||||
v.push(refuse("v2_a_failed_hop_with_an_output", fo.edited(|r| r["attempts"][0]["outputs"] = json!([hex(&delivered("job-2003", 1)[0])])).document(), "record_invalid", json!(["record"])));
|
||||
v.push(refuse("v2_an_output_twice", three.edited(|r| r["attempts"][0]["outputs"][2] = r["attempts"][0]["outputs"][0].clone()).document(), "record_invalid", json!(["record"])));
|
||||
v.push(refuse(
|
||||
"v2_an_output_in_capitals",
|
||||
b.edited(|r| r["attempts"][0]["outputs"][0] = json!(r["attempts"][0]["outputs"][0].as_str().unwrap().to_uppercase())).document(),
|
||||
"record_invalid",
|
||||
json!(["record"]),
|
||||
));
|
||||
|
||||
// ── refused: a receipt held to its outputs ──
|
||||
let ro = |i: usize| rec(&[json!("attempts"), json!(i), json!("receipt")]);
|
||||
let moved = fo.receipts.as_ref().unwrap()[1].clone();
|
||||
v.push(refuse("v2_a_receipt_moved_from_another_job", b.with_receipt(0, moved.clone()).document(), "receipt_output_mismatch", ro(0)));
|
||||
v.push(refuse("v2_a_late_hops_receipt_moved_from_another_job", late.with_receipt(0, moved).document(), "receipt_output_mismatch", ro(0)));
|
||||
v.push(refuse("v2_outputs_in_another_order", three.edited(|r| r["attempts"][0]["outputs"].as_array_mut().unwrap().reverse()).document(), "receipt_output_mismatch", ro(0)));
|
||||
let one = delivered("job-2001", 1)[0];
|
||||
let leaf = {
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(super::LEAF_DOMAIN);
|
||||
h.update(&one);
|
||||
hex(h.finalize().as_bytes())
|
||||
};
|
||||
v.push(refuse("v2_one_output_named_by_its_leaf", b.with_receipt(0, naming(b.receipts.as_ref().unwrap()[0].clone(), leaf)).document(), "receipt_output_mismatch", ro(0)));
|
||||
let first = hex(&delivered("job-2004", 3)[0]);
|
||||
v.push(refuse("v2_three_outputs_named_by_the_first", three.with_receipt(0, naming(three.receipts.as_ref().unwrap()[0].clone(), first)).document(), "receipt_output_mismatch", ro(0)));
|
||||
v
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
//! `live-conformance/routes/vectors.json` regenerates byte for byte from its seeds and is the
|
||||
//! only file there besides its README; every vector gets the outcome it states; every reason
|
||||
//! the extension adds and every code it reuses refuses a vector; the accepted vectors are the
|
||||
//! cases §10 names; a record a router seals verifies, and an edit of any of its documents or of
|
||||
//! the record is refused; and §8 lists exactly the reasons the reference reports.
|
||||
//! cases §10 names, of version 1 and of version 2; a record a router seals verifies, and an
|
||||
//! edit of any of its documents or of the record is refused; and §8 lists exactly the reasons
|
||||
//! the reference reports.
|
||||
#![cfg(feature = "route_record")]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
|
@ -95,22 +96,55 @@ fn the_accepted_vectors_are_the_cases_section_10_names() {
|
|||
assert_eq!(limits, route_record::LIMITS.iter().map(|l| l.to_string()).collect::<BTreeSet<_>>(), "every limit removes a candidate");
|
||||
}
|
||||
|
||||
/// Version 2's accepted vectors are §10's: each endpoint ranked by what one served job costs at
|
||||
/// it, failures included (an endpoint that fails ranked after one of the same list price, a
|
||||
/// cheaper one that fails more after both, the unmeasured, the thin basis and the failed one
|
||||
/// after every measured endpoint); a decode at its own price; the time to first byte and the
|
||||
/// targets read per successful job; the ceiling the list cost; a book with no failure measured
|
||||
/// ranked by the list cost; and the served hop's outputs, one or three, stated in the outcome.
|
||||
#[test]
|
||||
fn the_version_2_vectors_are_the_cases_section_10_names() {
|
||||
let d = doc();
|
||||
let o = |n: &str| vector(&d, n)["outcome"].clone();
|
||||
let order = |n: &str| -> Vec<String> { o(n)["order"].as_array().unwrap().iter().map(|x| x.as_str().unwrap().to_string()).collect() };
|
||||
let cost = o("v2_ranked_by_the_expected_cost");
|
||||
assert_eq!(order("v2_ranked_by_the_expected_cost"), ["im.alpha", "im.eta", "im.iota", "im.beta", "im.zeta", "im.theta", "im.gamma", "im.epsilon", "im.delta", "av1.alpha"]);
|
||||
assert_eq!(cost["decision"], serde_json::json!({"decided_by": "price", "runner_up": "im.eta"}));
|
||||
assert_eq!(cost["served"]["outputs"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(order("v2_a_decode_at_its_own_price"), ["im.alpha", "im.beta"], "the decodes alone, each at its own price, under a cap the encodes exceed");
|
||||
assert_eq!(order("v2_ranked_by_the_expected_time_to_first_byte")[..6], ["im.theta", "im.iota", "im.alpha", "im.zeta", "im.beta", "im.eta"]);
|
||||
assert_eq!(o("v2_the_targets_read_the_expected_time_to_first_byte")["decision"], serde_json::json!({"decided_by": "targets", "runner_up": "im.alpha"}));
|
||||
assert!(order("v2_the_ceiling_reads_the_list_cost").contains(&"im.zeta".to_string()), "a list cost under the ceiling survives whatever its failures add");
|
||||
assert!(!order("v2_the_ceiling_reads_the_list_cost").contains(&"im.theta".to_string()));
|
||||
assert_eq!(order("v2_no_failure_measured_ranks_by_the_list_cost")[..4], ["im.gamma", "im.epsilon", "im.zeta", "im.delta"]);
|
||||
assert_eq!(o("v2_served_three_outputs")["served"]["outputs"].as_array().unwrap().len(), 3);
|
||||
let fo = o("v2_failover_whose_failed_hop_delivered_nothing");
|
||||
assert_eq!((fo["served"]["endpoint"].as_str(), fo["billed"].as_u64()), (Some("im.eta"), Some(2)));
|
||||
let late = o("v2_served_after_the_deadline");
|
||||
assert_eq!((late["billed"].clone(), late["deadline"].as_str(), late["served"]["outputs"].as_array().map(Vec::len)), (Value::Null, Some("missed"), Some(1)));
|
||||
assert_eq!(o("v2_receipts_not_checked")["receipts"], "not_checked");
|
||||
// Version 1's outcomes state no outputs.
|
||||
assert!(o("served_first_try")["served"].get("outputs").is_none());
|
||||
}
|
||||
|
||||
/// A record verifies only against the documents it was routed from: each of them, and the
|
||||
/// record, changed by one byte is refused.
|
||||
#[test]
|
||||
fn a_record_is_bound_to_its_documents() {
|
||||
let d = doc();
|
||||
let good = vector(&d, "served_first_try")["document"].clone();
|
||||
assert!(route_record::verify(&good).is_ok());
|
||||
for member in ["request", "pricebook", "policy", "record"] {
|
||||
let t = good[member].as_str().unwrap();
|
||||
// A digit of a figure, changed: still a document, no longer the one routed.
|
||||
let at = t.find(|c: char| c.is_ascii_digit() && c != '0').unwrap();
|
||||
let mut b = t.as_bytes().to_vec();
|
||||
b[at] = if b[at] == b'9' { b'8' } else { b[at] + 1 };
|
||||
let mut x = good.clone();
|
||||
x[member] = Value::from(String::from_utf8(b).unwrap());
|
||||
assert!(route_record::verify(&x).is_err(), "{member} changed");
|
||||
for name in ["served_first_try", "v2_ranked_by_the_expected_cost"] {
|
||||
let good = vector(&d, name)["document"].clone();
|
||||
assert!(route_record::verify(&good).is_ok());
|
||||
for member in ["request", "pricebook", "policy", "record"] {
|
||||
let t = good[member].as_str().unwrap();
|
||||
// A digit of a figure, changed: still a document, no longer the one routed.
|
||||
let at = t.find(|c: char| c.is_ascii_digit() && c != '0').unwrap();
|
||||
let mut b = t.as_bytes().to_vec();
|
||||
b[at] = if b[at] == b'9' { b'8' } else { b[at] + 1 };
|
||||
let mut x = good.clone();
|
||||
x[member] = Value::from(String::from_utf8(b).unwrap());
|
||||
assert!(route_record::verify(&x).is_err(), "{name}: {member} changed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +155,7 @@ fn route_verify_prints_the_outcome_and_exits_by_it() {
|
|||
let d = doc();
|
||||
let dir = std::env::temp_dir().join(format!("wai-route-verify-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
for (name, code) in [("failover_after_a_server_error", 0), ("a_failed_hop_billed", 1), ("a_hop_receipt_not_given", 1)] {
|
||||
for (name, code) in [("failover_after_a_server_error", 0), ("a_failed_hop_billed", 1), ("a_hop_receipt_not_given", 1), ("v2_served_three_outputs", 0), ("v2_a_receipt_moved_from_another_job", 1)] {
|
||||
let v = vector(&d, name);
|
||||
let p = dir.join(format!("{name}.json"));
|
||||
std::fs::write(&p, serde_json::to_vec(&v["document"]).unwrap()).unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue