route-record: version 3 states what a hop delivered by its count and root, so a hop may deliver any number of objects #563
15 changed files with 2047 additions and 85 deletions
|
|
@ -115,7 +115,8 @@ mod tests {
|
|||
assert_eq!(Keys::from_km(&km, "another passphrase").unwrap_err(), KmError::BadSecret);
|
||||
let mut p = vec![0x47u8; 1316];
|
||||
assert!(tx.apply(1, 77, &mut p));
|
||||
assert_ne!(p[0], 0x47);
|
||||
// The whole payload: under a random key, one byte keeps its value one time in 256.
|
||||
assert_ne!(p, vec![0x47u8; 1316]);
|
||||
assert!(rx.apply(1, 77, &mut p));
|
||||
assert_eq!(p, vec![0x47u8; 1316]);
|
||||
assert!(!rx.apply(2, 77, &mut p), "no odd key is held");
|
||||
|
|
|
|||
|
|
@ -290,6 +290,8 @@ async fn contribution_at_twenty_megabits_passes_the_door() {
|
|||
/// follow the group's last object, so it reads each group whole off its stream before it
|
||||
/// releases any of it, which the relay's stream window (`limits.quic_stream_window_bytes`)
|
||||
/// must hold: under QUIC's default of about 1.25 MB the stream stops and the door refuses.
|
||||
/// The link's window is two seconds: the door is the subject, and on a loaded runner a stall
|
||||
/// past 300 ms had the gateway drop access units as too late (§4.6) before they reached it.
|
||||
async fn big_groups() {
|
||||
let w = World::start(|c| {
|
||||
c.contrib.srt.listen = Some("127.0.0.1:0".parse().unwrap());
|
||||
|
|
@ -299,15 +301,21 @@ async fn big_groups() {
|
|||
.await;
|
||||
let auth = w.auth(&w.publisher, 0);
|
||||
let s = open_srt(&w, &auth).await;
|
||||
let link = timeout(STEP, srt::call(w.gateway.srt_addr.unwrap(), call_cfg(&s))).await.unwrap().unwrap();
|
||||
let cfg = CallConfig { latency_ms: 2_000, ..call_cfg(&s) };
|
||||
let link = timeout(STEP, srt::call(w.gateway.srt_addr.unwrap(), cfg)).await.unwrap().unwrap();
|
||||
assert!(contrib_live(&w, s["session"].as_str().unwrap()).await);
|
||||
let watcher = watch(&w).await;
|
||||
let chunks = joule_contrib::cli::synthetic(4, 20_000);
|
||||
joule_contrib::cli::pace(&link, &chunks, 800).await.unwrap();
|
||||
assert!(until(STEP, || link.sent.lock().unwrap().unacknowledged == 0).await, "the gateway acknowledged every packet before the publisher closed");
|
||||
link.close();
|
||||
assert!(until(STEP, || watcher.ended.lock().unwrap().len() == 2).await, "PUBLISH_DONE on both media tracks");
|
||||
settle(&watcher, Duration::from_millis(500)).await;
|
||||
assert_eq!(w.relay.relay().stats().refused_at_door, 0, "the door released every group");
|
||||
assert!(until(STEP, || w.gateway.gateway.link_records().iter().any(|b| LinkRecord::read(b).is_ok_and(|r| r.last))).await, "the last part is signed");
|
||||
let ps = parts(&w);
|
||||
assert!(ps.iter().all(|p| p.window == 2_000_000), "the caller's window, the larger of the two ends'");
|
||||
assert_eq!(ps.iter().map(|p| p.account.dropped).sum::<u64>(), 0, "no packet dropped as too late");
|
||||
let all: Vec<u8> = chunks.iter().flat_map(|c| c.1.clone()).collect();
|
||||
let mut d = ts::Demux::new();
|
||||
let mut pes = d.push(&all);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,40 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## Unreleased
|
||||
|
||||
### The route record states what a hop delivered by its count and root, so a hop may deliver any number of objects
|
||||
|
||||
- **Route record version 3** ([extensions/route-record.md](extensions/route-record.md) §3.4, §5.2, §6, §6.1), on a
|
||||
price book of version 2: the attempt that served states `delivered`, the count of the objects it delivered and their
|
||||
commitment (the root its hop's receipt already names), not their list; a failed attempt, `0` and zeros. Version 2's
|
||||
list states at most 1024 outputs and grows with them, and a package of a long title holds an object per piece and
|
||||
rung, thousands; a record of version 3 is the same size whatever was delivered, and nothing a hop signs changes. The
|
||||
served hop's receipt is held to the root (`receipt_output_mismatch`). A record of version 1 rests on a price book of
|
||||
version 1, and one of version 2 or 3 on one of version 2 (`pricebook_mismatch`). Records of versions 1 and 2 read and
|
||||
verify exactly as before.
|
||||
- **An output's inclusion proof** (§6.1): `index` and `siblings` from the leaf up, held as jwp-receipts §3 step 5
|
||||
holds an object's: the index below the count, exactly ⌈log₂ count⌉ siblings, the walk ending at the root, so the last
|
||||
node of an odd level is proved at its own index only. `route_record::output_proof` makes one and
|
||||
`route_record::verify_output_proof` checks it; `route_record::seal_committed` writes a record of version 3.
|
||||
- **The receipt ledger** ([extensions/receipt-ledger.md](extensions/receipt-ledger.md) §5.1, §4.8): a job source whose
|
||||
route record is of version 3 gives each output `proof`, and an output is one its hop delivered when its proof holds
|
||||
its object to the served attempt's count and root (`output_not_delivered` otherwise; a missing or misshapen proof,
|
||||
or one under a record of version 1 or 2, `malformed`); a decode of version 5 is held to its places and its count. The
|
||||
count is the router's statement, which only the objects check; a count stated larger lets one object be proved at a
|
||||
second place, and no source bills one object twice. Every tariff version that reads job sources reads them.
|
||||
- **The corpora.** Routes 109 → 126 vectors: six accepted of version 3 (one output, three, 5,000, a failover, a late
|
||||
hop, receipts not checked) and eleven controls, the unknown-version control now of version 4.
|
||||
`route_record_verify.py` reads version 3 and has the proof check (`inclusion_proof`); two more checks dropped
|
||||
(`delivered-form`, `receipt-root`). Ledger 108 → 120 vectors: one accepted (an encode, a decode of version 5 and a
|
||||
package's run of 1,501 objects, each output proved) and eleven controls; `receipt_ledger_verify.py` checks every proof
|
||||
with the route verifier's `inclusion_proof`, one more check dropped (`output-proof`), three more mutated copies.
|
||||
- **Breaking:** `record:corpus:wai/live-conformance/routes/vectors.json`: the control `record_version_3` is now
|
||||
`record_version_4`, a record of version 4 refused `unknown_version` as before; every other vector keeps its outcome,
|
||||
and the new vectors are added.
|
||||
- **Breaking:** `api:rust:wai::route_record::Accepted::delivered`: an accepted record of version 3 states what its
|
||||
served hop delivered by its count and root, which `Accepted` carries in this new field (`None` for records of
|
||||
versions 1 and 2, as `outputs` is `None` for version 3). A struct literal or an exhaustive pattern of `Accepted`
|
||||
outside the crate adds the field, or matches with `..`.
|
||||
|
||||
### The receipt ledger holds a decode to the frames it delivered, as one animated PNG
|
||||
|
||||
- **Tariff and invoice version 5** ([extensions/receipt-ledger.md](extensions/receipt-ledger.md) §4.8): version 4's
|
||||
|
|
|
|||
|
|
@ -314,7 +314,9 @@ of version 5 a decode's output is held to what it delivered:
|
|||
decode's animations, by content hash, in the order of its outputs, and nothing else: an
|
||||
animation not stated at its place is refused `output_not_delivered` at `outputs`, the
|
||||
output's index and `frames`, and an object stated past the last, the codestream among them,
|
||||
at `outputs`.
|
||||
at `outputs`. Under a route record of version 3 an animation is at its place when its proof
|
||||
holds it there (§5.1, its `index` its place), and there is none past the last when the
|
||||
attempt's `count` is the number of animations.
|
||||
|
||||
A decode delivers its frames as one object because a clip's frames are many and repeat: a route
|
||||
record states each of a hop's outputs once, and at most 1024 of them (route-record §5.2), while
|
||||
|
|
@ -347,7 +349,7 @@ extension's verifier is given. Bytes are lowercase hexadecimal strings.
|
|||
| `package` | `receipt` (the text of `package.receipt.json`), `indexes` (the index files in the receipt's component order) | packaging §9, a receipt of version 2 against its indexes |
|
||||
| `ads` | `descriptor`, `grants`, `key_log`, `chains`, `policy`, `pt0_ms`, `receipts` (form-2 receipts in their JSON carriage), `pods`, `claims`, `creatives`, `issuers` | live-ads §9, with ad-presentation §9 under a policy that requires claims |
|
||||
| `stitched` | `records` (pod-input records), `claims` (stitched claims), `manifests`, `responses` (VAST and VMAP documents), `receipts` (form-1 receipts in their JSON carriage), `creatives`, `issuers` | stitched-presentation §7, its signers the tariff's (§4.4) |
|
||||
| `job` (versions 2 to 4) | `request`, `pricebook`, `policy`, `record`, `receipts`, `keys` (a route record's verification document, route-record §7.1, its `receipts` and `keys` not `null`) and `outputs`, a list of objects of exactly `receipt` (the text of a `wai.video.int_motion` decode receipt's JSON form, integer-payloads §15.2) and `codestream` (the `WIV1` payload it describes: the object the hop delivered, named by its content hash; in version 5 a decode's output also has `frames`, the animation of its frames it delivered, and its codestream is what it read, §4.8), or, in version 4, of exactly `package` (the text of a package receipt of version 2, `package.receipt.json`: the object the hop delivered) and `indexes` (its index files in its component order) | route-record §7, accepted with its hop receipts checked; then each output receipt read by integer-payloads §15.2 and re-derived from its `codestream` (§15.1 there), each package verified against its indexes by packaging §9, or `output_invalid` at `outputs` and its index; then each output held to what its hop delivered, or `output_not_delivered` there |
|
||||
| `job` (versions 2 to 4) | `request`, `pricebook`, `policy`, `record`, `receipts`, `keys` (a route record's verification document, route-record §7.1, its `receipts` and `keys` not `null`) and `outputs`, a list of objects of exactly `receipt` (the text of a `wai.video.int_motion` decode receipt's JSON form, integer-payloads §15.2) and `codestream` (the `WIV1` payload it describes: the object the hop delivered, named by its content hash; in version 5 a decode's output also has `frames`, the animation of its frames it delivered, and its codestream is what it read, §4.8), or, in version 4, of exactly `package` (the text of a package receipt of version 2, `package.receipt.json`: the object the hop delivered) and `indexes` (its index files in its component order); under a route record of version 3, each output also has `proof`, its object's inclusion proof (route-record §6.1) | route-record §7, accepted with its hop receipts checked; then each output receipt read by integer-payloads §15.2 and re-derived from its `codestream` (§15.1 there), each package verified against its indexes by packaging §9, or `output_invalid` at `outputs` and its index; then each output held to what its hop delivered, or `output_not_delivered` there |
|
||||
|
||||
A source is accepted when its extension's verifier accepts it; for `ads`, when the outcome is
|
||||
**accept** (live-ads §9.4), a **gap** being `source_gap`. A `stitched` source is always
|
||||
|
|
@ -381,9 +383,25 @@ the hop did not deliver bills subpixels nobody delivered for this job, as a rece
|
|||
twice bills them twice. So the chain closes: the hop's receipt names its outputs, the record
|
||||
names the hop's receipt and the outputs, and each output receipt is of one of them. Under a
|
||||
tariff of version 5 a decode's object is the animation of the frames it delivered, not the
|
||||
codestream it read (§4.8). Under a tariff of version 4, a record that states no outputs (a record of version 1, or one no attempt
|
||||
served) holds every output to none, and a job it routed is billed by the job alone; under a
|
||||
tariff of version 3, a record of version 1's outputs are read as before.
|
||||
codestream it read (§4.8). Under a tariff of version 4, a record that states no outputs (a
|
||||
record of version 1, or one no attempt served) holds every output to none, and a job it
|
||||
routed is billed by the job alone; under a tariff of version 3, a record of version 1's
|
||||
outputs are read as before.
|
||||
|
||||
A route record of version 3 states what its served attempt delivered by its count and root,
|
||||
not by a list (route-record §3.4, §5.2), so a hop may deliver any number of objects. Each
|
||||
output of a job source under one gives `proof`, the inclusion proof of its object (route-record
|
||||
§6.1), and is one of the objects the hop delivered when its proof holds its object's content
|
||||
hash to the served attempt's `count` and `root`; an output under a record of version 1 or 2
|
||||
gives none. An output without its proof, or with one under a record of version 1 or 2, or a
|
||||
proof that is not an object of exactly an integer `index` and a list of content hashes'
|
||||
texts, `siblings`, is `malformed`; a proof that does not hold is `output_not_delivered`, at
|
||||
`outputs` and the output's index, as an object a list does not state is; and no two outputs
|
||||
are one object, as before. The ledger does not hold the count to the objects, which it is not
|
||||
given: the hop's receipt names the root alone (route-record §6). A count stated larger than
|
||||
the objects lets the last of an odd level be proved at a second place, and that place is one
|
||||
object, which no source bills twice; a decode is held to its places and its count (§4.8).
|
||||
Under a record of version 3 no attempt served, the outputs are held to none.
|
||||
|
||||
**Routing figures are never charges.** A route record's candidates' `cost`, `energy`,
|
||||
`expected_cost` and `expected_ttfb_ms`, and its price book's `price` and `failures`, are the
|
||||
|
|
@ -852,7 +870,10 @@ outcome §7.3 gives it:
|
|||
work billed again and its receipt cited again, beside a second package listed; and, under a
|
||||
tariff of version 5, an `int_motion` decode whose served hop states it delivered one animated
|
||||
PNG of the clip's frames, its receipt re-derived from the codestream it read, beside an encode
|
||||
held to its codestream.
|
||||
held to its codestream; and jobs whose route records are of version 3, each output proved
|
||||
among what its hop delivered: an encode whose clip is the second of two objects, a decode of
|
||||
version 5 whose animation is its one object, and a package's run whose receipt is the first
|
||||
of 1,501 objects.
|
||||
- **Refused**: each reason of §10 at least once, each vector a control of one defect; a source of
|
||||
each kind outside the scope, a room receipt and a statement under other prices, and a source of
|
||||
each timed kind outside the period; a refusal of each source kind by its own extension; and, for
|
||||
|
|
@ -882,7 +903,11 @@ outcome §7.3 gives it:
|
|||
the picture, skips a sequence number, keeps its default image apart from its frames, counts a
|
||||
frame more than it holds, or has a frame with no data (`output_invalid`), a decode in
|
||||
version 5's form under a tariff of version 4, and an invoice of version 4 under a tariff of
|
||||
version 5.
|
||||
version 5; and, for jobs routed under the route record's version 3, an output without its proof,
|
||||
a proof under a record of version 2 and a proof whose index is text (`malformed`), a proof of
|
||||
another place, one past the count, one a sibling short, an output its hop did not deliver, one
|
||||
output twice, a decode's animation at another place, a decode with an object past its animation,
|
||||
and an output of a job no hop served (`output_not_delivered`).
|
||||
|
||||
`wai_receipt_ledger_vectors verify` and the test `tests/receipt_ledger.rs` check that the file
|
||||
regenerates byte for byte, that `wai-rs` gives every vector its outcome, that every reason of §10
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@
|
|||
> its outputs, to which its hop's receipt is held (§6). Documents and records of version 1 are
|
||||
> read and verified exactly as before.
|
||||
>
|
||||
> This revision adds **version 3** of the record (§3.4), on a price book of version 2: the
|
||||
> attempt that served states what it delivered by its count and the root its hop's receipt
|
||||
> names (§5.2, §6), not by a list, so a hop may deliver any number of objects and the record is
|
||||
> the same size whatever it delivered; an object is shown to be one of them by its inclusion
|
||||
> proof (§6.1). Records of versions 1 and 2 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; the BLAKE3 specification.
|
||||
|
||||
|
|
@ -35,13 +41,15 @@
|
|||
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, and in version 2 to the outputs it delivered
|
||||
(§6); verification (§7); and the reference implementation and corpus (§10).
|
||||
attempt is held to its hop's OER/2 receipt, and in versions 2 and 3 to the outputs it
|
||||
delivered (§6); an output's inclusion proof (§6.1); verification (§7); and the reference
|
||||
implementation and corpus (§10).
|
||||
|
||||
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
|
||||
define (a record of version 2 names the output by its content hash and nothing more, and one
|
||||
of version 3 by its count and root); 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).
|
||||
|
|
@ -214,16 +222,26 @@ 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).
|
||||
The request and the policy have one form. The price book has two: version 1, this
|
||||
extension's first, and version 2, which adds the members that price an operation and state an
|
||||
endpoint's failures. The record has three: version 1; version 2, which adds those members and
|
||||
names a hop's outputs by their list; and version 3, version 2's but for naming them by their
|
||||
count and root (§5.2). A record of version 1 rests on a price book of version 1, and one of
|
||||
version 2 or 3 on a price book of version 2 (§7.2 step 5). A router writes one version of
|
||||
record over a price book of version 2; a reader holds each as its version says.
|
||||
|
||||
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`.
|
||||
of version 2 is `unknown_version`; a reader of version 2 refuses a record of version 3 so. A
|
||||
price book whose `version` is other than 2 is `pricebook_invalid`; a record whose `version` is
|
||||
other than 1, 2 and 3 is `unknown_version`.
|
||||
|
||||
Version 3 exists because a list bounds what a hop may deliver and grows with it: version 2
|
||||
states at most 1024 outputs, and a package of a long title holds an object per piece and rung,
|
||||
thousands of them. The count and the root are an integer and 32 bytes whatever was delivered,
|
||||
and the root is the commitment the hop's receipt already names (§6), so nothing a hop signs
|
||||
changes.
|
||||
|
||||
## 4. Routing
|
||||
|
||||
|
|
@ -414,6 +432,7 @@ An attempt is one hop the router sent the job to:
|
|||
| `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) |
|
||||
| `delivered` | version 3: an object of exactly `count`, an integer, and `root`, 64 lowercase hexadecimal digits: when served, the number of objects the hop delivered, at least 1, no two the same, and their commitment in the order it delivered them (§6); when failed, `0` and 64 zeros |
|
||||
|
||||
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
|
||||
|
|
@ -461,16 +480,44 @@ holds each attempt to its receipt:
|
|||
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`.
|
||||
hexadecimal digits: otherwise `receipt_output_mismatch`;
|
||||
- in a record of version 3, its `subject.output` is the attempt's `root`: 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
|
||||
objects' content hashes are the record's outputs (§7.3), or in version 3 that their count is
|
||||
the record's and their commitment its root: the receipt names the root alone, so the count is
|
||||
the router's statement, which the objects check. 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.
|
||||
|
||||
### 6.1 An output's inclusion proof (version 3)
|
||||
|
||||
A record of version 3 does not list what the hop delivered. A reader that must show one
|
||||
object to be among them, as a receipt ledger shows a billed output to be (receipt-ledger
|
||||
§5.1), gives that object's **inclusion proof**: an object of exactly `index`, an integer, the
|
||||
object's place in the order the hop delivered, from 0, and `siblings`, a list of content
|
||||
hashes' texts (§2). The proof holds the object whose content hash is *h* to an attempt that
|
||||
delivered `count` objects under `root` when:
|
||||
|
||||
- `index` is below `count`, and `siblings` holds exactly ⌈log₂ `count`⌉ hashes, none for
|
||||
one object;
|
||||
- for one object, *h* is the root (oer-receipts §3 *Output*); otherwise, from *h*'s leaf
|
||||
(jwp-receipts §2.2, the unlabelled form), each sibling in turn combined with the value so far
|
||||
by jwp-receipts §2.2's node, the sibling on the left where bit *i* of `index` is 1 and on the
|
||||
right where it is 0, at level *i* from 0, ends at `root`.
|
||||
|
||||
A router makes it from the tree §6 commits to: the sibling at each level, the last node of an
|
||||
odd level paired with itself. The count and the bits fix the place, as jwp-receipts §3 step 5
|
||||
fixes an object's, so a node paired with itself is proved at its own index only, and two
|
||||
proofs at one index are of one object. The leaf and node domains alone keep a walk of another
|
||||
length from ending at the root; the number of siblings is checked all the same, as jwp-receipts
|
||||
checks it, so such a proof is refused before anything is hashed. This extension's verification (§7) is given no object
|
||||
and checks no proof; the reader that is given them checks it, with its own refusal.
|
||||
|
||||
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
|
||||
it from their receipts, and an invoice bills a job from them (receipt-ledger §5).
|
||||
|
|
@ -495,21 +542,22 @@ 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` 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
|
||||
`wai-route-record`, is `malformed`; a `version` that is an integer other than `1`, `2` and
|
||||
`3` 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 for the record's price book version, 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`, `outputs` or `delivered` 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 `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`).
|
||||
(`policy_mismatch`); its `version` is not one that rests on the price book's (§3.4:
|
||||
`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
|
||||
|
|
@ -537,7 +585,8 @@ and where (`at`, a list: the member, then indexes or members within it):
|
|||
- **refuse**: the reason and where.
|
||||
- **accept**: the record's digest; the job and its operation; the served endpoint and the hop
|
||||
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,
|
||||
order, or in one of version 3 their `delivered` count and root, 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
|
||||
|
|
@ -618,8 +667,17 @@ verification document with the outcome §7.3 gives it:
|
|||
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`.
|
||||
its leaf and three by the first of them (`receipt_output_mismatch`).
|
||||
- **Accepted, version 3**, on version 2's price book: a hop that served one output, whose root
|
||||
is its content hash; three; and 5,000, more than a list of version 2 holds, its record the
|
||||
size of the others; a failover whose failed hop delivered nothing; a hop that served past the
|
||||
deadline; and a record verified without its receipts.
|
||||
- **Refused, version 3**: a served hop's count of 0; a failed hop with a count, and with a root;
|
||||
a root in capitals; a count as text; a member besides the count and the root; outputs listed
|
||||
as version 2 lists them; `delivered` in a record of version 2 (`record_invalid`); a record of
|
||||
version 3 on a price book of version 1 (`pricebook_mismatch`); and a served hop's receipt
|
||||
moved from another job, and a root not its receipt's (`receipt_output_mismatch`). A record of
|
||||
version 4 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
|
||||
|
|
|
|||
|
|
@ -78,6 +78,13 @@ The accepted vectors:
|
|||
version 4. The animation is written here by `png_still::encode_animation` from the clip the
|
||||
decode reads, and each refused animation is that one with one rule broken, its chunks
|
||||
rewritten with good CRCs.
|
||||
- `route_record_3_outputs_proved_among_what_was_delivered` (route-record §3.4, §6.1; §5.1
|
||||
here): jobs routed here under the route record's version 3, whose served hops state what they
|
||||
delivered by its count and root, each output giving its inclusion proof: an encode
|
||||
(`job_v3r_encode`) whose clip is the second of two objects, a decode of version 5
|
||||
(`job_v3r_decode`) whose animation is its one object, and a package's run
|
||||
(`package_run_v3r`) whose receipt is the first of 1,501 objects, the rest standing for the
|
||||
package's own; billed under the tariff of version 5.
|
||||
|
||||
Every other vector is refused, a control of one defect: each reason of §10, the reused codes, and a
|
||||
refusal of each source kind by its own extension.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -635,6 +635,30 @@ def job_source(s, version):
|
|||
delivered = None
|
||||
if "output-delivered" in SKIP:
|
||||
delivered = None
|
||||
# §5.1: under a route record of version 3 the served hop states what it delivered by its
|
||||
# count and root, and each output proves its place by its inclusion proof (route-record
|
||||
# §6.1); one no attempt served holds every output to none.
|
||||
served_att = next((a for a in rec["attempts"] if a["outcome"] == "served"), None)
|
||||
committed = rec["version"] == 3 and served_att is not None
|
||||
if rec["version"] == 3 and not committed and "output-delivered" not in SKIP:
|
||||
delivered = set()
|
||||
count, root = (served_att["delivered"]["count"], bytes.fromhex(served_att["delivered"]["root"])) if committed else (0, b"")
|
||||
held = "output-delivered" not in SKIP and (delivered is not None or committed)
|
||||
|
||||
def holds(h, proof, place):
|
||||
"""Whether the object `h` is one the hop delivered, at `place` where one is required."""
|
||||
if committed:
|
||||
if "output-proof" in SKIP:
|
||||
return True
|
||||
return proof is not None and (place is None or proof[0] == place) and RRV.inclusion_proof(h, proof[0], proof[1], count, root)
|
||||
if place is not None:
|
||||
return place < len(stated) and stated[place] == h
|
||||
return h in delivered
|
||||
|
||||
def is_index(v):
|
||||
return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= 2 ** 53 - 1
|
||||
|
||||
hex64 = re.compile(r"[0-9a-f]{64}")
|
||||
seen = set()
|
||||
outputs, packages = [], []
|
||||
hexs = re.compile(r"(?:[0-9a-f]{2})*")
|
||||
|
|
@ -644,6 +668,18 @@ def job_source(s, version):
|
|||
stated = [bytes.fromhex(h) for a in rec["attempts"] if a["outcome"] == "served" for h in a["outputs"]] if rec["version"] == 2 else []
|
||||
nxt = 0
|
||||
for i, x in enumerate(s["outputs"]):
|
||||
# Under a record of version 3 each output gives `proof`, and under one of version 1 or 2
|
||||
# none (§5.1).
|
||||
proof = None
|
||||
if committed:
|
||||
if not isinstance(x, dict) or "proof" not in x:
|
||||
raise Refuse("malformed", [])
|
||||
x = dict(x)
|
||||
pr = x.pop("proof")
|
||||
if not (isinstance(pr, dict) and set(pr) == {"index", "siblings"} and is_index(pr["index"]) and isinstance(pr["siblings"], list)
|
||||
and all(isinstance(h, str) and hex64.fullmatch(h) for h in pr["siblings"])):
|
||||
raise Refuse("malformed", [])
|
||||
proof = (pr["index"], [bytes.fromhex(h) for h in pr["siblings"]])
|
||||
if decode:
|
||||
if not isinstance(x, dict) or set(x) != {"receipt", "codestream", "frames"} or not isinstance(x["receipt"], str) \
|
||||
or not isinstance(x["codestream"], str) or not hexs.fullmatch(x["codestream"]) \
|
||||
|
|
@ -671,7 +707,7 @@ def job_source(s, version):
|
|||
raise Refuse("output_invalid", ["outputs", i, "frames", k])
|
||||
outputs.append((IMR.receipt_hash(r), r["work"]))
|
||||
if "output-delivered" not in SKIP:
|
||||
if nxt >= len(stated) or stated[nxt] != content_hash(anim):
|
||||
if not holds(content_hash(anim), proof, nxt):
|
||||
raise Refuse("output_not_delivered", ["outputs", i, "frames"])
|
||||
nxt += 1
|
||||
continue
|
||||
|
|
@ -699,12 +735,12 @@ def job_source(s, version):
|
|||
raise Refuse("output_invalid", ["outputs", i])
|
||||
obj = bytes.fromhex(x["codestream"])
|
||||
outputs.append((IMR.receipt_hash(r), r["work"]))
|
||||
if delivered is not None:
|
||||
if held:
|
||||
h = content_hash(obj)
|
||||
if h not in delivered or h in seen:
|
||||
if not holds(h, proof, None) or h in seen:
|
||||
raise Refuse("output_not_delivered", ["outputs", i])
|
||||
seen.add(h)
|
||||
if decode and "output-delivered" not in SKIP and nxt != len(stated):
|
||||
if decode and "output-delivered" not in SKIP and nxt != (count if committed else len(stated)):
|
||||
raise Refuse("output_not_delivered", ["outputs"]) # §4.8, 3: an object past the animations
|
||||
return {"kind": JOB, "digest": bytes.fromhex(RRV.digest(rec)), "router": bytes.fromhex(rec["signer"]), "job": rec["job"],
|
||||
"operation": rec["operation"].encode("utf-8"), "accepted": rec["accepted"], "hops": [oer_identity(a["receipt"]) for a in rec["attempts"]], "billed": billed,
|
||||
|
|
@ -718,7 +754,7 @@ def verify_source(s, signers, version=1):
|
|||
"""§5.1: the source verified by its own extension, a stitched source with `signers`, a job
|
||||
source under a tariff of version 2 or 3, or Refuse with that extension's place."""
|
||||
key = json.dumps([s, sorted(k.hex() for k in signers), version if isinstance(s, dict) and s.get("kind") == "job" else version in JOB_VERSIONS,
|
||||
sorted(SKIP & {"job-figure", "output", "output-delivered", "unbilled-hops", "run-work", "decode-frames"})], sort_keys=True)
|
||||
sorted(SKIP & {"job-figure", "output", "output-delivered", "unbilled-hops", "run-work", "decode-frames", "output-proof"})], sort_keys=True)
|
||||
if key not in _SOURCES:
|
||||
try:
|
||||
_SOURCES[key] = read_source(s, signers, version)
|
||||
|
|
@ -1364,6 +1400,16 @@ def mutations(doc):
|
|||
m = copy.deepcopy(doc)
|
||||
m["sources"]["job_v5_decode"]["outputs"][0]["frames"] = doc["sources"][other]["outputs"][0]["frames"]
|
||||
out.append((f"job_v5_decode: {what}", m))
|
||||
if "job_v3r_encode" in doc["sources"]: # §5.1: an output proved at its place
|
||||
m = copy.deepcopy(doc)
|
||||
m["sources"]["job_v3r_encode"]["outputs"][0]["proof"]["index"] = 0
|
||||
out.append(("job_v3r_encode: its proof stating another place", m))
|
||||
m = copy.deepcopy(doc)
|
||||
m["sources"]["job_v3r_encode"]["outputs"][0]["proof"]["siblings"][0] = "00" * 32
|
||||
out.append(("job_v3r_encode: its proof's sibling replaced", m))
|
||||
m = copy.deepcopy(doc)
|
||||
m["sources"]["package_run_v3r"]["outputs"][0]["proof"]["index"] = 1
|
||||
out.append(("package_run_v3r: its receipt proved at the second place", m))
|
||||
for k, v in enumerate(doc["vectors"]):
|
||||
if v["outcome"]["outcome"] == "refuse":
|
||||
m = copy.deepcopy(doc)
|
||||
|
|
@ -1377,7 +1423,7 @@ CHECKS = ["signatures", "buyer-signature", "tariff-rules", "invoice-rules", "row
|
|||
"previous-given", "previous-overlap", "scope", "conflict", "period", "duplicate", "overlap", "rebilled", "quantity",
|
||||
"summed", "citations", "mismatch", "job-figure", "output", "job-duplicate", "private-kind", "operation", "output-once",
|
||||
"output-delivered", "unbilled-hops", "storage-held", "package-again", "run-work", "bytes-out", "conflict-bytes",
|
||||
"decode-frames"]
|
||||
"decode-frames", "output-proof"]
|
||||
|
||||
|
||||
def main(corpus_dir):
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ else is here:
|
|||
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.
|
||||
(BLAKE3 written from its specification) as oer-receipts §3 *Output* defines it;
|
||||
- version 3 of the record (§3.4), on a price book of version 2: what the served hop delivered
|
||||
stated by its count and root, the served hop's receipt held to the root; and an output's
|
||||
inclusion proof (§6.1, `inclusion_proof`), which the receipt ledger's verifier checks a job
|
||||
source's outputs with.
|
||||
|
||||
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
|
||||
|
|
@ -71,6 +75,11 @@ ATTEMPT = ["hop", "endpoint", "outcome", "error", "start", "end", "billed", "not
|
|||
LEAF, NODE = b"moq-jwp:merkle-leaf\x01", b"moq-jwp:merkle-node\x01" # jwp-receipts §2.2
|
||||
|
||||
|
||||
def book_of(version):
|
||||
"""§3.4: the price book version a record of `version` rests on: 1, or 2 for versions 2 and 3."""
|
||||
return 2 if version in (2, 3) else 1
|
||||
|
||||
|
||||
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"] \
|
||||
|
|
@ -558,11 +567,23 @@ def outputs_form(a):
|
|||
return 1 <= len(xs) <= 1024 if a["outcome"] == "served" else not xs
|
||||
|
||||
|
||||
def delivered_form(a):
|
||||
"""§5.2, version 3: a served attempt's count, at least 1, and the root of what it delivered,
|
||||
64 lowercase hexadecimal digits; a failed one's, 0 and 64 zeros."""
|
||||
d = a["delivered"]
|
||||
if not (exactly(d, ["count", "root"]) and is_int(d["count"]) and is_hex(d["root"], 64)):
|
||||
return False
|
||||
return d["count"] >= 1 if a["outcome"] == "served" else d["count"] == 0 and d["root"] == "00" * 32
|
||||
|
||||
|
||||
def attempt_form(i, a, version=1):
|
||||
if not exactly(a, ATTEMPT + (["outputs"] if version == 2 else [])) or a["outcome"] not in ("served", "failed"):
|
||||
extra = {2: ["outputs"], 3: ["delivered"]}.get(version, [])
|
||||
if not exactly(a, ATTEMPT + extra) or a["outcome"] not in ("served", "failed"):
|
||||
return False
|
||||
if version == 2 and on("outputs-form") and not outputs_form(a):
|
||||
return False
|
||||
if version == 3 and on("delivered-form") and not delivered_form(a):
|
||||
return False
|
||||
served = a["outcome"] == "served"
|
||||
error_ok = a["error"] is None if served else a["error"] in ERRORS
|
||||
return (is_int(a["hop"]) and a["hop"] == i + 1 and is_token(a["endpoint"]) and error_ok and is_int(a["start"])
|
||||
|
|
@ -574,7 +595,7 @@ def record_form(r):
|
|||
if not exactly(r, RECORD):
|
||||
return False
|
||||
v = r["version"]
|
||||
return (is_int(v) and v in (1, 2) and is_token(r["job"]) and isinstance(r["operation"], str)
|
||||
return (is_int(v) and v in (1, 2, 3) 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"])
|
||||
|
|
@ -582,7 +603,7 @@ 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, v) for c in r["candidates"])
|
||||
and isinstance(r["candidates"], list) and len(r["candidates"]) <= 1024 and all(candidate_form(c, book_of(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)
|
||||
|
|
@ -640,7 +661,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"] not in (1, 2):
|
||||
if on("version") and is_int(rec.get("version")) and rec["version"] not in (1, 2, 3):
|
||||
raise Refuse("unknown_version", ["record", "version"])
|
||||
if on("record-form") and not record_form(rec):
|
||||
raise Refuse("record_invalid", ["record"])
|
||||
|
|
@ -658,7 +679,7 @@ 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)):
|
||||
if on("record-version") and not book_of(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"])
|
||||
|
|
@ -720,7 +741,8 @@ def verify_steps(doc):
|
|||
"operation": req["operation"],
|
||||
"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 {})),
|
||||
**({"outputs": served_attempt["outputs"]} if "outputs" in served_attempt else {}),
|
||||
**({"delivered": served_attempt["delivered"]} if "delivered" in served_attempt else {})),
|
||||
"billed": billed_hop,
|
||||
"deadline": dl,
|
||||
"order": order,
|
||||
|
|
@ -745,6 +767,22 @@ def commitment(outputs):
|
|||
return level[0].hex() if level else "00" * 32
|
||||
|
||||
|
||||
def inclusion_proof(h, index, siblings, count, root):
|
||||
"""§6.1: whether `siblings` proves the object whose content hash is `h` (bytes) the one at
|
||||
`index` of `count` objects delivered under `root` (bytes): `index` below `count`, exactly
|
||||
ceil(log2 count) siblings (none for one object, whose root is its content hash), and the walk
|
||||
from its unlabelled leaf, the sibling on the left where bit i of `index` is 1, ends at
|
||||
`root`."""
|
||||
if not (0 <= index < count) or len(siblings) != (count - 1).bit_length():
|
||||
return False
|
||||
if count == 1:
|
||||
return h == root
|
||||
node = blake3(LEAF + h)
|
||||
for i, sib in enumerate(siblings):
|
||||
node = blake3(NODE + sib + node) if (index >> i) & 1 else blake3(NODE + node + sib)
|
||||
return node == root
|
||||
|
||||
|
||||
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."""
|
||||
|
|
@ -775,6 +813,11 @@ def hop_receipts(rec, texts, keys):
|
|||
subject = r.get("subject") if isinstance(r.get("subject"), dict) else {}
|
||||
if subject.get("output") != commitment(a["outputs"]):
|
||||
raise Refuse("receipt_output_mismatch", at)
|
||||
# Version 3 (§6): it names the root the attempt states.
|
||||
if on("receipt-root") and a["outcome"] == "served" and "delivered" in a:
|
||||
subject = r.get("subject") if isinstance(r.get("subject"), dict) else {}
|
||||
if subject.get("output") != a["delivered"]["root"]:
|
||||
raise Refuse("receipt_output_mismatch", at)
|
||||
if OER.is_unmetered(r):
|
||||
unmetered.append(a["hop"])
|
||||
return unmetered
|
||||
|
|
@ -851,6 +894,10 @@ def mutations(doc):
|
|||
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"))
|
||||
if o["served"] and "delivered" in o["served"]:
|
||||
copy_with("its stated count one more", k, lambda x: x["outcome"]["served"]["delivered"].__setitem__("count", o["served"]["delivered"]["count"] + 1))
|
||||
copy_with("its stated root another", k, lambda x: x["outcome"]["served"]["delivered"].__setitem__("root", "00" * 32))
|
||||
copy_with("its served hop's delivered dropped from the outcome", k, lambda x: x["outcome"]["served"].pop("delivered"))
|
||||
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"))
|
||||
|
|
@ -865,7 +912,8 @@ CHECKS = ["request_invalid", "pricebook_invalid", "policy_invalid", "version", "
|
|||
"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",
|
||||
"expected-ttfb", "expected-targets", "record-version", "outputs-form", "receipt-output", "delivered-form",
|
||||
"receipt-root",
|
||||
"limit-policy", "limit-unit", "limit-region", "limit-residency", "limit-training", "limit-retention", "limit-ga",
|
||||
"limit-consent", "limit-unit-price", "limit-spend"]
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ a price book that prices each operation of an endpoint and states its measured f
|
|||
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.
|
||||
hashes. Then those of version 3 (§3.4), on version 2's price book, whose names begin `v3_` but
|
||||
for the control that pairs one with a price book of version 1: the served hop's outputs stated
|
||||
by their count and root, to which its receipt is held, 5,000 of them in one record the size of
|
||||
the others.
|
||||
|
||||
```sh
|
||||
# wai-rs: regenerates byte for byte, and every vector gets its outcome
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1471,6 +1471,48 @@ fn decode_output(x: &Map<String, Value>, i: usize) -> Result<DecodeOutput, Refus
|
|||
Ok(DecodeOutput { receipt: (r.receipt_hash(), r.work_pixel_ops), object: crate::route_record::content_hash(&frames) })
|
||||
}
|
||||
|
||||
/// What the hop that served delivered, as its route record states it (route-record §5.2).
|
||||
enum Delivered {
|
||||
/// The content hashes of its outputs, in their order (version 2), or none.
|
||||
Listed(Vec<[u8; 32]>),
|
||||
/// Their count and root (version 3): each output gives its inclusion proof
|
||||
/// (route-record §6.1).
|
||||
Committed { count: u64, root: [u8; 32] },
|
||||
}
|
||||
|
||||
impl Delivered {
|
||||
/// Whether `object` (a content hash) is one the hop delivered, at `place` where the rule
|
||||
/// holds it to one: in a list, by its place in it; under a root, by its proof.
|
||||
fn holds(&self, object: &[u8; 32], proof: Option<&(u64, Vec<[u8; 32]>)>, place: Option<usize>) -> bool {
|
||||
match self {
|
||||
Delivered::Listed(d) => match place {
|
||||
Some(k) => d.get(k) == Some(object),
|
||||
None => d.contains(object),
|
||||
},
|
||||
Delivered::Committed { count, root } => {
|
||||
proof.is_some_and(|(index, siblings)| place.is_none_or(|k| *index == k as u64) && crate::route_record::verify_output_proof(object, *index, siblings, *count, root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How many objects it delivered.
|
||||
fn count(&self) -> u64 {
|
||||
match self {
|
||||
Delivered::Listed(d) => d.len() as u64,
|
||||
Delivered::Committed { count, .. } => *count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An output's inclusion proof (route-record §6.1): exactly `index`, an integer, and `siblings`,
|
||||
/// content hashes as 64 lowercase hexadecimal digits.
|
||||
fn read_proof(v: &Value) -> Option<(u64, Vec<[u8; 32]>)> {
|
||||
let o = v.as_object().filter(|o| has_exactly(o, &["index", "siblings"]))?;
|
||||
let index = o["index"].as_u64().filter(|i| *i <= crate::jcs::MAX_INTEGER)?;
|
||||
let siblings = o["siblings"].as_array()?.iter().map(|h| h.as_str().filter(|h| h.len() == 64).and_then(unhex).and_then(|b| b.try_into().ok())).collect::<Option<Vec<[u8; 32]>>>()?;
|
||||
Some((index, siblings))
|
||||
}
|
||||
|
||||
fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
|
||||
use crate::route_record as rr;
|
||||
if !has_exactly(o, &["kind", "request", "pricebook", "policy", "record", "receipts", "keys", "outputs"]) {
|
||||
|
|
@ -1507,23 +1549,25 @@ fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
|
|||
unbilled.push(job_figure(&texts[j]).ok_or((Reason::JobFigureInvalid.code(), json!(["receipts", j])))?);
|
||||
}
|
||||
}
|
||||
// What the hop that served states it delivered (route-record §5.2, version 2), by content
|
||||
// hash, in its order; a record of version 1 states nothing, and under a tariff of version 4
|
||||
// or 5 holds its outputs to nothing (§5.1). Under version 3 a record of version 1's outputs
|
||||
// are read as before.
|
||||
let delivered: Option<Vec<[u8; 32]>> = if record["version"] == 2 {
|
||||
Some(
|
||||
attempts
|
||||
.iter()
|
||||
.filter(|a| a["outcome"] == "served")
|
||||
.flat_map(|a| a["outputs"].as_array().expect("a list").iter().map(|h| unhex(h.as_str().expect("a hash")).expect("hex").try_into().expect("32 bytes")))
|
||||
.collect(),
|
||||
)
|
||||
} else if bills_spending(version) {
|
||||
Some(Vec::new())
|
||||
} else {
|
||||
None
|
||||
// What the hop that served states it delivered (route-record §5.2): in version 2 by content
|
||||
// hash, in its order; in version 3 by its count and root, each output proving its place; a
|
||||
// record of version 1 states nothing, and under a tariff of version 4 or 5 holds its outputs
|
||||
// to nothing (§5.1), as one no attempt served does. Under version 3 a record of version 1's
|
||||
// outputs are read as before.
|
||||
let served_attempt = attempts.iter().find(|a| a["outcome"] == "served");
|
||||
let delivered: Option<Delivered> = match (record["version"].as_u64(), served_attempt) {
|
||||
(Some(rr::VERSION_3), Some(a)) => Some(Delivered::Committed {
|
||||
count: a["delivered"]["count"].as_u64().expect("a count"),
|
||||
root: unhex(a["delivered"]["root"].as_str().expect("a root")).expect("hex").try_into().expect("32 bytes"),
|
||||
}),
|
||||
(Some(rr::VERSION_3), None) => Some(Delivered::Listed(Vec::new())),
|
||||
(Some(rr::VERSION_2), a) => Some(Delivered::Listed(
|
||||
a.into_iter().flat_map(|a| a["outputs"].as_array().expect("a list").iter().map(|h| unhex(h.as_str().expect("a hash")).expect("hex").try_into().expect("32 bytes"))).collect(),
|
||||
)),
|
||||
_ if bills_spending(version) => Some(Delivered::Listed(Vec::new())),
|
||||
_ => None,
|
||||
};
|
||||
let committed = matches!(delivered, Some(Delivered::Committed { .. }));
|
||||
// Under a tariff of version 5 a decode delivers its reconstruction, one animation of its
|
||||
// frames, and its codestream is what it read (§4.8).
|
||||
let decode = holds_decode_to_frames(version) && record["operation"] == DECODE;
|
||||
|
|
@ -1535,11 +1579,21 @@ fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
|
|||
for (i, x) in outputs.iter().enumerate() {
|
||||
let bad = || (Reason::OutputInvalid.code(), json!(["outputs", i]));
|
||||
let x = x.as_object().ok_or_else(bad_source)?;
|
||||
// With a record of version 3 each output also gives its inclusion proof, and with one of
|
||||
// version 1 or 2 none (§5.1).
|
||||
let (x, proof) = if committed {
|
||||
let mut m = x.clone();
|
||||
let proof = m.remove("proof").as_ref().and_then(read_proof).ok_or_else(bad_source)?;
|
||||
(m, Some(proof))
|
||||
} else {
|
||||
(x.clone(), None)
|
||||
};
|
||||
let x = &x;
|
||||
if decode {
|
||||
let out = decode_output(x, i)?;
|
||||
outs.push(out.receipt);
|
||||
// The animation is the object the record states at its place (§4.8, 3).
|
||||
if delivered.as_deref().unwrap_or(&[]).get(next) != Some(&out.object) {
|
||||
if !delivered.as_ref().is_some_and(|d| d.holds(&out.object, proof.as_ref(), Some(next))) {
|
||||
return Err((Reason::OutputNotDelivered.code(), json!(["outputs", i, "frames"])));
|
||||
}
|
||||
next += 1;
|
||||
|
|
@ -1567,14 +1621,14 @@ fn job_source(o: &Map<String, Value>, version: u8) -> Result<Facts, Refusal> {
|
|||
// The output is its object: one the served hop delivered, once.
|
||||
if let Some(d) = &delivered {
|
||||
let h = rr::content_hash(&object);
|
||||
if !d.contains(&h) || !seen.insert(h) {
|
||||
if !d.holds(&h, proof.as_ref(), None) || !seen.insert(h) {
|
||||
return Err((Reason::OutputNotDelivered.code(), json!(["outputs", i])));
|
||||
}
|
||||
}
|
||||
}
|
||||
// A decode delivered its animations and nothing else: an object the record states past them,
|
||||
// the codestream it read among them, is one its hop did not deliver (§4.8, 3).
|
||||
if decode && delivered.as_ref().is_some_and(|d| d.len() != next) {
|
||||
if decode && delivered.as_ref().is_some_and(|d| d.count() != next as u64) {
|
||||
return Err((Reason::OutputNotDelivered.code(), json!(["outputs"])));
|
||||
}
|
||||
Ok(Facts::Job(JobFacts {
|
||||
|
|
|
|||
|
|
@ -666,6 +666,77 @@ pub fn generate(wai: &Path) -> Result<String, String> {
|
|||
cs.remove(last);
|
||||
}));
|
||||
|
||||
// Jobs routed under the route record's version 3 (route-record §3.4): the hop that served
|
||||
// states what it delivered by its count and root, and each output proves its place by its
|
||||
// inclusion proof (§6.1). An encode whose hop delivered a sidecar and its clip, the clip
|
||||
// second; a decode of version 5, its animation at its place; and a package's run whose hop
|
||||
// delivered the package's receipt among 1,500 more objects, standing for the package's own,
|
||||
// more than a list of version 2 holds. Then an output given without its proof; a proof given
|
||||
// under a record of version 2; a proof that is not one; a proof of another place; one past
|
||||
// the count, its walk the clip's own (the index's bits below the depth are the place's); a
|
||||
// proof a sibling short; an output its hop did not deliver, proved in another tree; one output given
|
||||
// twice; a decode's animation proved at another place than its own; a decode with an
|
||||
// object past its animation; and an output of a job no hop served, which delivered nothing.
|
||||
let proved = |x: &Value, objects: &[[u8; 32]], index: usize| -> Value {
|
||||
let mut x = x.clone();
|
||||
let siblings = crate::route_record::output_proof(objects, index).expect("a place");
|
||||
x["proof"] = json!({"index": index, "siblings": siblings.iter().map(|h| hex(h)).collect::<Vec<_>>()});
|
||||
x
|
||||
};
|
||||
let routed_3 = |job: &str, operation: &str, hops: &[rr::Hop], objects: &[[u8; 32]], given: Vec<Value>| routed(rr::job_document_3(job, operation, accepted, hops, objects), given);
|
||||
let with_outputs_of = |source: &Value, outputs: Vec<Value>| {
|
||||
let mut j = source.clone();
|
||||
j["outputs"] = Value::Array(outputs);
|
||||
j
|
||||
};
|
||||
let sidecar = rr_hash(b"wai receipt-ledger corpus: job-6001, a sidecar");
|
||||
let enc_objects = [sidecar, rr_hash(&clip)];
|
||||
let enc_out = out("job-6001").remove(0);
|
||||
let encode_3 = routed_3("job-6001", ENC, &[served("im.alpha", rr::hop_receipt(ENC, "job-6001", "im.alpha", Some("30.5")))], &enc_objects, vec![proved(&enc_out, &enc_objects, 1)]);
|
||||
pool.insert("job_v3r_encode", encode_3.clone());
|
||||
let dec_hop = |job: &str| served("im.alpha", rr::hop_receipt(DEC, job, "im.alpha", Some("12.0")));
|
||||
let anim_object = [rr_hash(&anim)];
|
||||
pool.insert("job_v3r_decode", routed_3("job-6002", DEC, &[dec_hop("job-6002")], &anim_object, vec![proved(&output_5("job-6002", &anim)[0], &anim_object, 0)]));
|
||||
let mut pack_objects = vec![base_object[0]];
|
||||
pack_objects.extend((0..1_500).map(|i| rr_hash(format!("wai receipt-ledger corpus: pack-0002, object {i}").as_bytes())));
|
||||
pool.insert(
|
||||
"package_run_v3r",
|
||||
routed(rr::package_job_document_3("pack-0002", accepted, &[pack_hop(1_000, rr::hop_receipt("package.build", "pack-0002", "pack.alpha", Some("4.75")))], &pack_objects), vec![proved(&base_output, &pack_objects, 0)]),
|
||||
);
|
||||
pool.insert("job_v3r_without_its_proof", with_outputs_of(&encode_3, vec![enc_out.clone()]));
|
||||
let encode_2_listed = pool["job_v2_encode"].clone();
|
||||
pool.insert("job_v2_given_a_proof", with_outputs_of(&encode_2_listed, vec![proved(&out("job-3001")[0], &[rr_hash(&clip)], 0)]));
|
||||
let mut not_one = proved(&enc_out, &enc_objects, 1);
|
||||
not_one["proof"]["index"] = json!("1");
|
||||
pool.insert("job_v3r_a_proof_that_is_not_one", with_outputs_of(&encode_3, vec![not_one]));
|
||||
pool.insert("job_v3r_a_proof_of_another_place", with_outputs_of(&encode_3, vec![proved(&enc_out, &enc_objects, 0)]));
|
||||
let mut short = proved(&enc_out, &enc_objects, 1);
|
||||
short["proof"]["siblings"].as_array_mut().ok_or("siblings")?.pop();
|
||||
pool.insert("job_v3r_a_proof_a_sibling_short", with_outputs_of(&encode_3, vec![short]));
|
||||
let mut past = proved(&enc_out, &enc_objects, 1);
|
||||
past["proof"]["index"] = json!(3);
|
||||
pool.insert("job_v3r_a_proof_past_the_count", with_outputs_of(&encode_3, vec![past]));
|
||||
pool.insert("job_v3r_an_output_its_hop_did_not_deliver", with_outputs_of(&encode_3, vec![proved(&output_of(&other_clip, &other_clip, "job-6001"), &[sidecar, rr_hash(&other_clip)], 1)]));
|
||||
pool.insert("job_v3r_an_output_twice", with_outputs_of(&encode_3, vec![proved(&enc_out, &enc_objects, 1), proved(&enc_out, &enc_objects, 1)]));
|
||||
let clip_then_anim = [rr_hash(&clip), rr_hash(&anim)];
|
||||
pool.insert("job_v3r_a_decode_at_another_place", routed_3("job-6003", DEC, &[dec_hop("job-6003")], &clip_then_anim, vec![proved(&output_5("job-6003", &anim)[0], &clip_then_anim, 1)]));
|
||||
let anim_then_clip = [rr_hash(&anim), rr_hash(&clip)];
|
||||
pool.insert("job_v3r_a_decode_with_an_object_past_it", routed_3("job-6004", DEC, &[dec_hop("job-6004")], &anim_then_clip, vec![proved(&output_5("job-6004", &anim)[0], &anim_then_clip, 0)]));
|
||||
pool.insert(
|
||||
"job_v3r_no_hop_served",
|
||||
routed_3(
|
||||
"job-6005",
|
||||
ENC,
|
||||
&[
|
||||
failed("im.alpha", 1_000, 2_000_000, rr::failed_hop_receipt(ENC, "job-6005", "im.alpha", Some("1.5"))),
|
||||
failed("im.eta", 2_000_100, 3_000_000, rr::failed_hop_receipt(ENC, "job-6005", "im.eta", None)),
|
||||
failed("im.iota", 3_000_100, 4_000_000, rr::failed_hop_receipt(ENC, "job-6005", "im.iota", Some("0.75"))),
|
||||
],
|
||||
&[],
|
||||
out("job-6005"),
|
||||
),
|
||||
);
|
||||
|
||||
// The rooms' prices are the lesson receipts'; the tariff's scope is every party of the
|
||||
// accepted invoices.
|
||||
let room_prices = {
|
||||
|
|
@ -1024,6 +1095,27 @@ pub fn generate(wai: &Path) -> Result<String, String> {
|
|||
Vector { name: "version_4_invoice_under_a_version_5_tariff", about: "an invoice of version 4 naming a tariff of version 5", tariff: t5b.clone(), invoice: sealed_after(&t_5, None, (MONTH.0, mid), MONTH.0, mid, &refs(&day_5), |i| i.version = VERSION_4).to_bytes(), previous: None, sources: day_5.to_vec() },
|
||||
]);
|
||||
|
||||
// Jobs routed under the route record's version 3 (route-record §3.4), under the tariff of
|
||||
// version 5: each output proved among what its hop delivered by its inclusion proof (§5.1).
|
||||
let day_r3 = ["job_v3r_encode", "job_v3r_decode", "package_run_v3r"];
|
||||
let inv_r3 = make_invoice(&seller, &t_5, None, MONTH.0, mid, &src(&day_r3)).map_err(|e| format!("invoice of records of version 3: {e:?}"))?;
|
||||
let ar3 = inv_r3.to_bytes();
|
||||
let refused_r3 = |name: &'static str, about: &'static str, source: &'static str| Vector { name, about, tariff: t5b.clone(), invoice: ar3.clone(), previous: None, sources: vec![source] };
|
||||
vectors.extend([
|
||||
Vector { name: "route_record_3_outputs_proved_among_what_was_delivered", about: "jobs whose route records are of version 3, stating what each served hop delivered by its count and root: an encode whose clip is the second of two objects, a decode of version 5 whose animation is its one object, and a package's run whose receipt is the first of 1,501 objects, each output proved at its place by its inclusion proof, billed under version 5", tariff: t5b.clone(), invoice: ar3.clone(), previous: None, sources: day_r3.to_vec() },
|
||||
refused_r3("route_record_3_an_output_without_its_proof", "a job whose route record is of version 3, its output given without its inclusion proof", "job_v3r_without_its_proof"),
|
||||
refused_r3("route_record_2_an_output_given_a_proof", "a job whose route record is of version 2, which lists what its hop delivered, its output given an inclusion proof", "job_v2_given_a_proof"),
|
||||
refused_r3("route_record_3_a_proof_that_is_not_one", "a job whose route record is of version 3, its output's proof stating its index as text", "job_v3r_a_proof_that_is_not_one"),
|
||||
refused_r3("route_record_3_a_proof_of_another_place", "a job whose route record is of version 3, its output proved with the siblings of another object's place", "job_v3r_a_proof_of_another_place"),
|
||||
refused_r3("route_record_3_a_proof_past_the_count", "a job whose route record is of version 3, its output's proof at index 3 of 2 objects, whose walk is that of its place, 1", "job_v3r_a_proof_past_the_count"),
|
||||
refused_r3("route_record_3_a_proof_a_sibling_short", "a job whose route record is of version 3, its output's proof one sibling short", "job_v3r_a_proof_a_sibling_short"),
|
||||
refused_r3("route_record_3_an_output_its_hop_did_not_deliver", "a job whose route record is of version 3, its output another clip, proved in another tree than the root its hop delivered under", "job_v3r_an_output_its_hop_did_not_deliver"),
|
||||
refused_r3("route_record_3_an_output_twice", "a job whose route record is of version 3, its one output given twice, each proved", "job_v3r_an_output_twice"),
|
||||
refused_r3("route_record_3_a_decode_at_another_place", "a decode of version 5 whose route record is of version 3 and states the codestream it read before its animation, the animation proved at its own place, the second", "job_v3r_a_decode_at_another_place"),
|
||||
refused_r3("route_record_3_a_decode_with_an_object_past_it", "a decode of version 5 whose route record is of version 3 and states the codestream it read after its animation, the animation proved at the first place", "job_v3r_a_decode_with_an_object_past_it"),
|
||||
refused_r3("route_record_3_an_output_of_a_job_no_hop_served", "a job whose route record is of version 3 and whose every allowed hop failed, given an output, which no hop delivered", "job_v3r_no_hop_served"),
|
||||
]);
|
||||
|
||||
let mut sources = Map::new();
|
||||
for (n, v) in &pool {
|
||||
sources.insert((*n).into(), v.clone());
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@
|
|||
//! by [`output_commitment`] (§6). [`seal_delivered`] writes a record of its price book's
|
||||
//! version. Version 1 reads, routes and verifies exactly as before.
|
||||
//!
|
||||
//! Version 3 of the record (§3.4), on a price book of version 2: the attempt that served states
|
||||
//! what it delivered by its count and root (`delivered`), the commitment its hop's receipt
|
||||
//! names, so a record is the same size whatever a hop delivered; [`seal_committed`] writes one.
|
||||
//! An object is shown to be one of them by its inclusion proof ([`output_proof`],
|
||||
//! [`verify_output_proof`], §6.1).
|
||||
//!
|
||||
//! 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.
|
||||
|
|
@ -37,9 +43,12 @@ pub const CAPABILITY: &str = "wai.route.record";
|
|||
pub const VERSION: u64 = 1;
|
||||
/// The second version of the price book and the record (§3.4).
|
||||
pub const VERSION_2: u64 = 2;
|
||||
/// The third version of the record (§3.4), on a price book of version 2: the attempt that served
|
||||
/// states what it delivered by its count and root.
|
||||
pub const VERSION_3: u64 = 3;
|
||||
/// 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).
|
||||
/// The most outputs an attempt of a record of version 2 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";
|
||||
|
|
@ -974,6 +983,26 @@ pub fn seal_delivered(key: &SigningKey, r: &Request, b: &PriceBook, p: &Policy,
|
|||
sign(body, key)
|
||||
}
|
||||
|
||||
/// [`seal`], writing a record of version 3 on a price book of version 2 (§3.4): the attempt that
|
||||
/// served states `delivered`, the count of `outputs` (the content hashes of what it delivered, in
|
||||
/// their order) and their commitment ([`output_commitment`]), and a failed attempt a count of 0
|
||||
/// and a root of zeros (§5.2). On a price book of version 1 it writes version 1's record, as
|
||||
/// [`seal`] does.
|
||||
pub fn seal_committed(key: &SigningKey, r: &Request, b: &PriceBook, p: &Policy, accepted: u64, attempts: &[Attempt], outputs: &[[u8; 32]]) -> Value {
|
||||
let mut record = seal_delivered(key, r, b, p, accepted, attempts, outputs);
|
||||
if b.version != VERSION_2 {
|
||||
return record;
|
||||
}
|
||||
let root = hex(&output_commitment(outputs));
|
||||
for a in record["attempts"].as_array_mut().expect("a list") {
|
||||
let served = a["outcome"] == "served";
|
||||
a.as_object_mut().expect("an attempt").remove("outputs");
|
||||
a["delivered"] = if served { json!({"count": outputs.len(), "root": root}) } else { json!({"count": 0, "root": hex(&[0; 32])}) };
|
||||
}
|
||||
record["version"] = json!(VERSION_3);
|
||||
resign(record, key)
|
||||
}
|
||||
|
||||
/// The members of a record (§5.1).
|
||||
pub const RECORD_MEMBERS: [&str; 18] = [
|
||||
"kind", "version", "job", "operation", "capability", "fallback", "quantities", "request", "policy", "pricebook", "candidates", "order", "decision", "accepted",
|
||||
|
|
@ -983,12 +1012,18 @@ 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: its entry's and the five §5.1 adds, and in version 2 the two
|
||||
/// expected figures.
|
||||
/// The version of the price book a record of `version` rests on (§3.4): 1 for version 1, 2 for
|
||||
/// versions 2 and 3.
|
||||
pub fn book_version(version: u64) -> u64 {
|
||||
if version >= VERSION_2 { VERSION_2 } else { VERSION }
|
||||
}
|
||||
|
||||
/// The candidate's members: its entry's and the five §5.1 adds, and on a price book of version 2
|
||||
/// the two expected figures.
|
||||
fn candidate_members(version: u64) -> Vec<&'static str> {
|
||||
let mut m = entry_members(version);
|
||||
let mut m = entry_members(book_version(version));
|
||||
m.extend(["via_fallback", "cost", "energy", "removed", "rank"]);
|
||||
if version == VERSION_2 {
|
||||
if book_version(version) == VERSION_2 {
|
||||
m.extend(["expected_cost", "expected_ttfb_ms"]);
|
||||
}
|
||||
m
|
||||
|
|
@ -1035,12 +1070,77 @@ pub fn output_commitment(outputs: &[[u8; 32]]) -> [u8; 32] {
|
|||
}
|
||||
}
|
||||
|
||||
/// The inclusion proof (§6.1) of the output at `index` among `outputs`, in their order: the
|
||||
/// sibling at each level of the tree [`output_commitment`] builds, from the leaves up, the last
|
||||
/// node of an odd level paired with itself; none for one output. `None` past the last.
|
||||
pub fn output_proof(outputs: &[[u8; 32]], index: usize) -> Option<Vec<[u8; 32]>> {
|
||||
if index >= outputs.len() {
|
||||
return None;
|
||||
}
|
||||
let b3 = |parts: &[&[u8]]| -> [u8; 32] {
|
||||
let mut h = blake3::Hasher::new();
|
||||
for p in parts {
|
||||
h.update(p);
|
||||
}
|
||||
*h.finalize().as_bytes()
|
||||
};
|
||||
let mut siblings = Vec::new();
|
||||
if outputs.len() == 1 {
|
||||
return Some(siblings);
|
||||
}
|
||||
let mut level: Vec<[u8; 32]> = outputs.iter().map(|o| b3(&[LEAF_DOMAIN, o])).collect();
|
||||
let mut at = index;
|
||||
while level.len() > 1 {
|
||||
if !level.len().is_multiple_of(2) {
|
||||
level.push(*level.last().expect("a node"));
|
||||
}
|
||||
siblings.push(level[at ^ 1]);
|
||||
level = level.as_chunks::<2>().0.iter().map(|[l, r]| b3(&[NODE_DOMAIN, l, r])).collect();
|
||||
at /= 2;
|
||||
}
|
||||
Some(siblings)
|
||||
}
|
||||
|
||||
/// Whether `siblings` proves the object whose content hash is `output` the one at `index` of
|
||||
/// `count` outputs committed to by `root` (§6.1): `index` below `count`, exactly ⌈log₂ `count`⌉
|
||||
/// siblings (none for one output, whose commitment is its content hash), and the walk from its
|
||||
/// leaf, node by node, each sibling on the side `index`'s bit at that level gives, ends at
|
||||
/// `root`. The count and the bits fix the position, so the last node of an odd level, paired
|
||||
/// with itself, is proved at one index only.
|
||||
pub fn verify_output_proof(output: &[u8; 32], index: u64, siblings: &[[u8; 32]], count: u64, root: &[u8; 32]) -> bool {
|
||||
if index >= count {
|
||||
return false;
|
||||
}
|
||||
let depth = if count <= 1 { 0 } else { 64 - (count - 1).leading_zeros() as usize };
|
||||
if siblings.len() != depth {
|
||||
return false;
|
||||
}
|
||||
if count == 1 {
|
||||
return output == root;
|
||||
}
|
||||
let b3 = |parts: &[&[u8]]| -> [u8; 32] {
|
||||
let mut h = blake3::Hasher::new();
|
||||
for p in parts {
|
||||
h.update(p);
|
||||
}
|
||||
*h.finalize().as_bytes()
|
||||
};
|
||||
let mut node = b3(&[LEAF_DOMAIN, output]);
|
||||
for (i, s) in siblings.iter().enumerate() {
|
||||
node = if (index >> i) & 1 == 1 { b3(&[NODE_DOMAIN, s, &node]) } else { b3(&[NODE_DOMAIN, &node, s]) };
|
||||
}
|
||||
&node == root
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
if version == VERSION_3 {
|
||||
attempt_members.push("delivered");
|
||||
}
|
||||
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);
|
||||
|
|
@ -1050,7 +1150,7 @@ fn record_form(v: &Value) -> bool {
|
|||
let scalar = |v: &Value| !(v.is_array() || v.is_object());
|
||||
let candidate = |c: &Value| -> bool {
|
||||
let Some(c) = exactly(c, &candidate_members(version)) else { return false };
|
||||
let v2 = version != VERSION_2
|
||||
let v2 = book_version(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"])
|
||||
|
|
@ -1095,7 +1195,15 @@ fn record_form(v: &Value) -> bool {
|
|||
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() }
|
||||
});
|
||||
// Version 3 (§5.2): a served attempt's count, at least 1, and the root of what it
|
||||
// delivered; a failed attempt's, 0 and zeros.
|
||||
let delivered_ok = version != VERSION_3
|
||||
|| exactly(&a["delivered"], &["count", "root"]).is_some_and(|d| {
|
||||
let zero = d["root"].as_str() == Some(hex(&[0; 32]).as_str());
|
||||
content_hash_text(&d["root"]) && int(&d["count"]).is_some_and(|n| if served { n >= 1 } else { n == 0 && zero })
|
||||
});
|
||||
outputs_ok
|
||||
&& delivered_ok
|
||||
&& int(&a["hop"]) == Some(i as u64 + 1)
|
||||
&& tok(&a["endpoint"]).is_some()
|
||||
&& error_ok
|
||||
|
|
@ -1105,7 +1213,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)
|
||||
};
|
||||
(version == VERSION || version == VERSION_2)
|
||||
(version == VERSION || version == VERSION_2 || version == VERSION_3)
|
||||
&& tok(&o["job"]).is_some()
|
||||
&& o["operation"].as_str().is_some_and(operation)
|
||||
&& o["capability"].as_str().is_some_and(capability)
|
||||
|
|
@ -1138,8 +1246,11 @@ pub struct Accepted {
|
|||
/// `(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.
|
||||
/// order; `None` in versions 1 and 3 and for a job no hop served.
|
||||
pub outputs: Option<Vec<String>>,
|
||||
/// In a record of version 3, the count and root of what the served attempt delivered;
|
||||
/// `None` in versions 1 and 2 and for a job no hop served.
|
||||
pub delivered: Option<(u64, String)>,
|
||||
pub billed: Option<u64>,
|
||||
/// `none`, `met` or `missed`.
|
||||
pub deadline: &'static str,
|
||||
|
|
@ -1206,6 +1317,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
|
||||
&& v != VERSION_3
|
||||
{
|
||||
return Err(Refusal::new(Reason::UnknownVersion, at(&[json!("record"), json!("version")])));
|
||||
}
|
||||
|
|
@ -1240,7 +1352,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) {
|
||||
// A record of version 1 rests on a price book of version 1, and one of version 2 or 3 on a
|
||||
// price book of version 2 (§3.4).
|
||||
if book_version(rec["version"].as_u64().expect("a version")) != b.version {
|
||||
return Err(Refusal::new(Reason::PricebookMismatch, field("version")));
|
||||
}
|
||||
if rec["pricebook"] != json!({"digest": hex(&b.digest), "currency": b.currency, "issued": b.issued}) {
|
||||
|
|
@ -1322,6 +1436,7 @@ pub fn verify_document(d: &Document<'_>) -> Result<Accepted, Refusal> {
|
|||
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 delivered = served_hop.and_then(|a| a.get("delivered")).map(|d| (d["count"].as_u64().expect("a count"), d["root"].as_str().expect("a root").to_string()));
|
||||
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" },
|
||||
|
|
@ -1333,6 +1448,7 @@ pub fn verify_document(d: &Document<'_>) -> Result<Accepted, Refusal> {
|
|||
operation: r.operation.clone(),
|
||||
served,
|
||||
outputs,
|
||||
delivered,
|
||||
billed,
|
||||
deadline: deadline_verdict,
|
||||
order: routed.order_endpoints(),
|
||||
|
|
@ -1375,6 +1491,13 @@ fn check_receipts(rec: &Value, texts: &[&str], keys: &Map<String, Value>) -> Res
|
|||
return Err(Refusal::new(Reason::ReceiptOutputMismatch, here));
|
||||
}
|
||||
}
|
||||
// Version 3: the served hop's receipt names the root the attempt states (§6).
|
||||
if let Some(d) = a.get("delivered").filter(|_| a["outcome"] == "served") {
|
||||
let named = r.get("subject").and_then(|s| s.get("output")).and_then(Value::as_str);
|
||||
if named != d["root"].as_str() {
|
||||
return Err(Refusal::new(Reason::ReceiptOutputMismatch, here));
|
||||
}
|
||||
}
|
||||
if crate::oer::is_unmetered(r) {
|
||||
unmetered.push(a["hop"].as_u64().expect("a hop"));
|
||||
}
|
||||
|
|
@ -1391,9 +1514,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)| match &a.outputs {
|
||||
Some(o) => json!({"endpoint": e, "hop": h, "outputs": o}),
|
||||
None => json!({"endpoint": e, "hop": h}),
|
||||
"served": a.served.as_ref().map(|(e, h)| match (&a.outputs, &a.delivered) {
|
||||
(Some(o), _) => json!({"endpoint": e, "hop": h, "outputs": o}),
|
||||
(None, Some((count, root))) => json!({"endpoint": e, "hop": h, "delivered": {"count": count, "root": root}}),
|
||||
(None, None) => json!({"endpoint": e, "hop": h}),
|
||||
}),
|
||||
"billed": a.billed,
|
||||
"deadline": a.deadline,
|
||||
|
|
@ -1410,6 +1534,48 @@ pub fn outcome_json(r: &Result<Accepted, Refusal>) -> Value {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An output's inclusion proof (§6.1) closes at the commitment for every output of every count
|
||||
/// to 37, and nowhere else: not at another index or one past the last, not with a sibling
|
||||
/// changed, more or fewer, not for another object or under a count of another depth; one
|
||||
/// output's root is its content hash. The last node of an odd level, paired with itself, is proved at its
|
||||
/// own index only.
|
||||
#[test]
|
||||
fn an_outputs_inclusion_proof_closes_at_its_commitment_and_nowhere_else() {
|
||||
for n in 1..=37usize {
|
||||
let outs: Vec<[u8; 32]> = (0..n).map(|i| content_hash(format!("output {i}").as_bytes())).collect();
|
||||
let (root, c) = (output_commitment(&outs), n as u64);
|
||||
if n == 1 {
|
||||
assert_eq!(root, outs[0]);
|
||||
}
|
||||
for (i, o) in outs.iter().enumerate() {
|
||||
let p = output_proof(&outs, i).unwrap();
|
||||
let at = i as u64;
|
||||
assert!(verify_output_proof(o, at, &p, c, &root), "{n}: {i}");
|
||||
assert!(!verify_output_proof(o, at + c, &p, c, &root), "{n}: {i} past the last");
|
||||
assert!(!verify_output_proof(&content_hash(b"another object"), at, &p, c, &root), "{n}: {i} another object");
|
||||
let mut more = p.clone();
|
||||
more.push(root);
|
||||
assert!(!verify_output_proof(o, at, &more, c, &root), "{n}: {i} a sibling more");
|
||||
assert!(!verify_output_proof(o, at, &p, 2 * c + 1, &root), "{n}: {i} a count of another depth");
|
||||
if n > 1 {
|
||||
assert!(!verify_output_proof(o, ((i + 1) % n) as u64, &p, c, &root), "{n}: {i} another index");
|
||||
assert!(!verify_output_proof(o, at, &p[..p.len() - 1], c, &root), "{n}: {i} a sibling fewer");
|
||||
let mut changed = p.clone();
|
||||
changed[0][0] ^= 1;
|
||||
assert!(!verify_output_proof(o, at, &changed, c, &root), "{n}: {i} a sibling changed");
|
||||
}
|
||||
}
|
||||
assert!(output_proof(&outs, n).is_none());
|
||||
}
|
||||
let outs: Vec<[u8; 32]> = (0u8..5).map(|i| content_hash(&[i])).collect();
|
||||
let root = output_commitment(&outs);
|
||||
let p = output_proof(&outs, 4).unwrap();
|
||||
assert!(verify_output_proof(&outs[4], 4, &p, 5, &root));
|
||||
for at in [5u64, 6, 7] {
|
||||
assert!(!verify_output_proof(&outs[4], at, &p, 5, &root), "the paired node at {at}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_is_read_strictly_and_held_to_the_subset() {
|
||||
assert!(read_document(r#"{"a":1}"#).is_some());
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@
|
|||
//! 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. The
|
||||
//! vectors of version 2 (§3.4) follow those of version 1, which are unchanged but for the
|
||||
//! control of an unknown version.
|
||||
//! vectors of version 2 (§3.4) follow those of version 1, and those of version 3 follow
|
||||
//! version 2's; each version's are unchanged by the next but for the control of an unknown
|
||||
//! version.
|
||||
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use serde_json::{json, Value};
|
||||
|
|
@ -408,7 +409,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_3", b.edited(|r| r["version"] = json!(3)).document(), "unknown_version", rec(&[json!("version")])));
|
||||
v.push(refuse("record_version_4", b.edited(|r| r["version"] = json!(4)).document(), "unknown_version", rec(&[json!("version")])));
|
||||
|
||||
// ── refused: the documents ──
|
||||
let mut x = d.clone();
|
||||
|
|
@ -666,8 +667,33 @@ pub(crate) fn package_job_document(job: &str, accepted: u64, attempts: &[Hop], o
|
|||
routed_2(request, pricebook_2_of(vec![entry]), accepted, attempts, outputs)
|
||||
}
|
||||
|
||||
/// [`job_document_2`], its record of version 3 (§3.4): the attempt that served states the count
|
||||
/// and root of `outputs`, which its receipt names, and a holder proves each output among them by
|
||||
/// [`super::output_proof`].
|
||||
#[cfg(feature = "receipt_ledger")]
|
||||
pub(crate) fn job_document_3(job: &str, operation: &str, accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]]) -> Value {
|
||||
routed(request_2(job, operation, no_preference()), pricebook_2(), accepted, attempts, outputs, true)
|
||||
}
|
||||
|
||||
/// [`package_job_document`], its record of version 3.
|
||||
#[cfg(feature = "receipt_ledger")]
|
||||
pub(crate) fn package_job_document_3(job: &str, accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]]) -> Value {
|
||||
let mut request = request_2(job, "package.build", no_preference());
|
||||
request["capability"] = json!(PACKAGE);
|
||||
request["fallback"] = Value::Null;
|
||||
request["quantities"] = json!([{"unit": "packages", "quantity": 1}]);
|
||||
let entry = entry_2("pack.alpha", "package.build", "prov-alpha", PACKAGE, EU_C, ("packages", 1, 400_000), Some(500), Some((4_500_000, "OnChipCounter")), Some(950), failures(1_000, 0, 0, 0));
|
||||
routed(request, pricebook_2_of(vec![entry]), accepted, attempts, outputs, true)
|
||||
}
|
||||
|
||||
#[cfg(feature = "receipt_ledger")]
|
||||
fn routed_2(request: Value, pricebook: Value, accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]]) -> Value {
|
||||
routed(request, pricebook, accepted, attempts, outputs, false)
|
||||
}
|
||||
|
||||
/// A job routed under `pricebook`, its record of version 2, or of version 3 when `committed`.
|
||||
#[cfg(feature = "receipt_ledger")]
|
||||
fn routed(request: Value, pricebook: Value, accepted: u64, attempts: &[Hop], outputs: &[[u8; 32]], committed: bool) -> Value {
|
||||
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(3))).expect("the policy reads");
|
||||
|
|
@ -688,7 +714,7 @@ fn routed_2(request: Value, pricebook: Value, accepted: u64, attempts: &[Hop], o
|
|||
.zip(&receipts)
|
||||
.map(|(h, rc)| Attempt { endpoint: h.endpoint.to_string(), error: h.error, start: accepted + h.start, end: accepted + h.end, receipt: id(rc) })
|
||||
.collect();
|
||||
let record = super::seal_delivered(&router(), &r, &b, &p, accepted, &atts, outputs);
|
||||
let record = if committed { super::seal_committed(&router(), &r, &b, &p, accepted, &atts, outputs) } else { super::seal_delivered(&router(), &r, &b, &p, accepted, &atts, outputs) };
|
||||
json!({
|
||||
"request": canonical(&request),
|
||||
"pricebook": canonical(&pricebook),
|
||||
|
|
@ -702,7 +728,7 @@ fn routed_2(request: Value, pricebook: Value, accepted: u64, attempts: &[Hop], o
|
|||
/// The corpus, as `vectors.json` holds it.
|
||||
pub fn generate() -> Result<String, String> {
|
||||
let mut out = Vec::new();
|
||||
for vec in vectors().into_iter().chain(vectors_2()) {
|
||||
for vec in vectors().into_iter().chain(vectors_2()).chain(vectors_3()) {
|
||||
let got = outcome_json(&verify(&vec.document));
|
||||
if let Some((reason, at)) = &vec.expect
|
||||
&& (got["outcome"] != "refuse" || got["reason"] != *reason || got["at"] != *at)
|
||||
|
|
@ -1028,3 +1054,85 @@ fn vectors_2() -> Vec<Vector> {
|
|||
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
|
||||
}
|
||||
|
||||
// ───────────────────────────── version 3 (§3.4) ─────────────────────────────
|
||||
|
||||
impl Job {
|
||||
/// A job whose record is of version 3: as [`Job::new_2`], its record sealed by
|
||||
/// [`super::seal_committed`], the served attempt stating the count and root of `outputs`.
|
||||
fn new_3(request: Value, attempts: &[(&str, Option<&'static str>, u64, u64, Value)], outputs: &[[u8; 32]], receipts: bool) -> Job {
|
||||
let j = Job::new_2(request, pricebook_2(), policy(3), attempts, outputs, receipts);
|
||||
let r = Request::read(&canonical(&j.request)).expect("the request reads");
|
||||
let b = PriceBook::read(&canonical(&j.pricebook)).expect("the price book reads");
|
||||
let p = Policy::read(&canonical(&j.policy)).expect("the policy reads");
|
||||
let atts: Vec<Attempt> = j.record["attempts"]
|
||||
.as_array()
|
||||
.expect("a list")
|
||||
.iter()
|
||||
.map(|a| Attempt {
|
||||
endpoint: a["endpoint"].as_str().expect("a token").to_string(),
|
||||
error: attempts[a["hop"].as_u64().expect("a hop") as usize - 1].1,
|
||||
start: a["start"].as_u64().expect("a time"),
|
||||
end: a["end"].as_u64().expect("a time"),
|
||||
receipt: a["receipt"].as_str().expect("an id").to_string(),
|
||||
})
|
||||
.collect();
|
||||
let record = super::seal_committed(&router(), &r, &b, &p, ACCEPTED, &atts, outputs);
|
||||
Job { record, ..j }
|
||||
}
|
||||
}
|
||||
|
||||
fn vectors_3() -> 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)
|
||||
};
|
||||
let alpha = || ("im.alpha", None, 1_000, 4_000_000, metered("im.alpha"));
|
||||
|
||||
// ── accepted ──
|
||||
// One output, whose root is its content hash; three; and more than a list of version 2 holds
|
||||
// (§5.2), its record the size of the others.
|
||||
let one = Job::new_3(request_2("job-3001", ENC, no_preference()), &[alpha()], &delivered("job-3001", 1), true);
|
||||
v.push(accept("v3_served_one_output", &one));
|
||||
let three = Job::new_3(request_2("job-3002", ENC, no_preference()), &[alpha()], &delivered("job-3002", 3), true);
|
||||
v.push(accept("v3_served_three_outputs", &three));
|
||||
v.push(accept("v3_served_more_outputs_than_a_list_holds", &Job::new_3(request_2("job-3003", ENC, no_preference()), &[alpha()], &delivered("job-3003", 5_000), true)));
|
||||
let fo = Job::new_3(
|
||||
request_2("job-3004", ENC, no_preference()),
|
||||
&[("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-3004", 2),
|
||||
true,
|
||||
);
|
||||
v.push(accept("v3_failover_whose_failed_hop_delivered_nothing", &fo));
|
||||
v.push(accept("v3_served_after_the_deadline", &Job::new_3(request_2("job-3005", ENC, no_preference()), &[("im.alpha", None, 1_000, 31_000_000, metered("im.alpha"))], &delivered("job-3005", 1), true)));
|
||||
v.push(accept("v3_receipts_not_checked", &Job::new_3(request_2("job-3001", ENC, no_preference()), &[alpha()], &delivered("job-3001", 1), false)));
|
||||
|
||||
// ── refused: the form of what was delivered ──
|
||||
let invalid = |name: &'static str, j: &Job, f: &dyn Fn(&mut Value)| refuse(name, j.edited(|r| f(r)).document(), "record_invalid", json!(["record"]));
|
||||
v.push(invalid("v3_served_with_a_count_of_0", &one, &|r| r["attempts"][0]["delivered"]["count"] = json!(0)));
|
||||
v.push(invalid("v3_a_failed_hop_with_a_count", &fo, &|r| r["attempts"][0]["delivered"]["count"] = json!(1)));
|
||||
v.push(invalid("v3_a_failed_hop_with_a_root", &fo, &|r| r["attempts"][0]["delivered"]["root"] = r["attempts"][1]["delivered"]["root"].clone()));
|
||||
v.push(invalid("v3_a_root_in_capitals", &one, &|r| r["attempts"][0]["delivered"]["root"] = json!(r["attempts"][0]["delivered"]["root"].as_str().unwrap().to_uppercase())));
|
||||
v.push(invalid("v3_a_count_as_text", &three, &|r| r["attempts"][0]["delivered"]["count"] = json!("3")));
|
||||
v.push(invalid("v3_a_member_besides_the_count_and_the_root", &three, &|r| r["attempts"][0]["delivered"]["outputs"] = json!(3)));
|
||||
v.push(invalid("v3_outputs_listed_as_version_2_lists_them", &one, &|r| {
|
||||
let a = r["attempts"][0].as_object_mut().unwrap();
|
||||
a.remove("delivered");
|
||||
a.insert("outputs".into(), json!([hex(&delivered("job-3001", 1)[0])]));
|
||||
}));
|
||||
v.push(invalid("v3_delivered_in_a_record_of_version_2", &one, &|r| r["version"] = json!(2)));
|
||||
|
||||
// ── refused: the record's version against its price book's ──
|
||||
let mut three_on_one = one.edited(|r| r["pricebook"] = json!({"digest": hex(&super::digest(&pricebook())), "currency": "EUR", "issued": pricebook()["issued"]}));
|
||||
three_on_one.pricebook = pricebook();
|
||||
v.push(refuse("a_record_of_version_3_on_a_price_book_of_version_1", three_on_one.document(), "pricebook_mismatch", rec(&[json!("version")])));
|
||||
|
||||
// ── refused: a receipt held to the root ──
|
||||
let ro = |i: usize| rec(&[json!("attempts"), json!(i), json!("receipt")]);
|
||||
v.push(refuse("v3_a_receipt_moved_from_another_job", one.with_receipt(0, fo.receipts.as_ref().unwrap()[1].clone()).document(), "receipt_output_mismatch", ro(0)));
|
||||
let two_of_three = hex(&super::output_commitment(&delivered("job-3002", 3)[..2]));
|
||||
v.push(refuse("v3_a_root_not_the_receipts", three.edited(|r| r["attempts"][0]["delivered"]["root"] = json!(two_of_three)).document(), "receipt_output_mismatch", ro(0)));
|
||||
v
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue