route-record: version 2 prices per operation, ranks by measured failure, binds a hop receipt to its output #552

Merged
dcharlot merged 4 commits from wai/route-physics into main 2026-10-31 08:05:26 -04:00
12 changed files with 2258 additions and 133 deletions

View file

@ -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

View file

@ -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

View file

@ -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).

View file

@ -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

View file

@ -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:

View file

@ -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.

View file

@ -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"]

View file

@ -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

View file

@ -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");

View file

@ -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
}

View file

@ -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();